diff --git a/dashboard/js/popups.js b/dashboard/js/popups.js new file mode 100644 index 0000000..6a7b3a1 --- /dev/null +++ b/dashboard/js/popups.js @@ -0,0 +1,397 @@ +const Popups = (() => { + let _currentAgentId = null; + let _agentCfgData = null; + + // ══ POPUP DÉTAIL ══ + async function showDetail(agentId) { + _currentAgentId = agentId; + const entry = Grid.getAgent(agentId); + if (!entry) return; + const { agent, metrics } = entry; + + document.getElementById('pop-host').textContent = agent.hostname; + document.getElementById('pop-ip').textContent = agent.ip || '—'; + const led = document.getElementById('pop-led'); + led.className = 'pop-led'; + led.style.background = agent.status === 'online' ? 'var(--ok)' : 'var(--err)'; + led.style.boxShadow = `0 0 8px ${agent.status === 'online' ? 'var(--ok)' : 'var(--err)'}`; + + // Icône + const img = document.getElementById('pop-icon-img'); + const fa = document.getElementById('pop-icon-fa'); + img.src = API.iconUrl(agentId) + '?t=' + Date.now(); + img.style.display = 'block'; + img.onerror = () => { img.style.display = 'none'; fa.style.display = 'flex'; }; + fa.style.display = 'none'; + + // Upload icône + document.getElementById('pop-icon-wrap').onclick = () => document.getElementById('icon-upload').click(); + document.getElementById('icon-upload').onchange = async (e) => { + const file = e.target.files[0]; + if (!file) return; + await API.uploadIcon(agentId, file); + img.src = API.iconUrl(agentId) + '?t=' + Date.now(); + }; + + // Uptime + const up = metrics?.uptime; + if (up) { + const d = Math.floor(up / 86400), h = Math.floor((up % 86400) / 3600); + document.getElementById('pop-uptime').innerHTML = + `En ligne depuis ${d}j ${h}h`; + } + + // Corps du popup + const now = Math.floor(Date.now() / 1000); + let history = []; + try { history = await API.getAgentHistory(agentId, now - 1800, now); } catch {} + + const cpuPts = Charts.historyToCpuPts(history); + const memPts = Charts.historyToMemPts(history); + + const smartBtn = metrics?.smart + ? `
+
+ SMART + · + ${metrics.smart.passed ? 'PASSED' : 'FAILED'} + ${metrics.smart.temperature ? ` ${metrics.smart.temperature}°C` : ''} + +
` + : ''; + + const protos = [ + metrics?.cpu_percent != null ? `UDP` : '', + ].filter(Boolean).join(''); + + document.getElementById('pop-body').innerHTML = ` +
+
MÉTRIQUES ACTUELLES
+
+
CPU
+
${(metrics?.cpu_percent ?? 0).toFixed(0)}%
+
MÉMOIRE
+
${Grid.fmt(metrics?.memory_used)}
+
/ ${Grid.fmt(metrics?.memory_total)}
+
DISQUE
+
${Grid.fmt(metrics?.hdd_used)}
+
/ ${Grid.fmt(metrics?.hdd_total)}
+
UPTIME
+
${document.getElementById('pop-uptime').textContent.replace(/.*depuis /,'')}
+
+
+
+
HISTORIQUE — 30 MIN
+
+
+
+
CPU
+ ${(metrics?.cpu_percent ?? 0).toFixed(0)}% +
+ +
−30min−15minnow
+
+
+
+
RAM
+ ${Grid.fmtPct(metrics?.memory_used && metrics?.memory_total ? metrics.memory_used / metrics.memory_total * 100 : null)} +
+ +
−30min−15minnow
+
+
+
+
+
STOCKAGE
+
+
+
+
+
+ ${Grid.fmt(metrics?.hdd_used)} / ${Grid.fmt(metrics?.hdd_total)} +
+ ${smartBtn} +
+
+
+
INFORMATIONS
+
+
HOSTNAME
${agent.hostname}
+
ADRESSE IP
${agent.ip || '—'}
+
PROTOCOLES ACTIFS
${protos || '—'}
+
DERNIER CONTACT
${new Date(agent.last_seen * 1000).toLocaleTimeString('fr-FR')}
+
+
`; + + requestAnimationFrame(() => { + Charts.renderChart(document.getElementById('det-cpu-chart'), cpuPts, '--accent'); + Charts.renderChart(document.getElementById('det-mem-chart'), memPts, '--blue'); + }); + + // Resize → sauvegarder sur serveur + const pd = document.getElementById('popup-detail'); + new ResizeObserver(() => { + API.putServerConfig({ + ...App.serverConfig, + popup_detail_w: pd.offsetWidth, + popup_detail_h: pd.offsetHeight, + }).catch(() => {}); + }).observe(pd); + + document.getElementById('overlay-detail').style.display = 'flex'; + } + + function hideDetail() { + document.getElementById('overlay-detail').style.display = 'none'; + } + + // ══ CONFIG AGENT ══ + async function showAgentCfg() { + if (!_currentAgentId) return; + let cfg = {}; + try { cfg = await API.getAgentConfig(_currentAgentId); } catch {} + _agentCfgData = cfg; + + document.getElementById('agentcfg-sub').textContent = + `${_currentAgentId} · config récupérée`; + + const metrics = ['cpu','memory','disk','smart','uptime','network','temperature']; + const icons = { + cpu:'fa-microchip',memory:'fa-memory',disk:'fa-hard-drive', + smart:'fa-shield-heart',uptime:'fa-clock',network:'fa-network-wired', + temperature:'fa-thermometer-half' + }; + const mqttCfg = cfg.protocols?.mqtt ?? {}; + + document.getElementById('agentcfg-body').innerHTML = ` +
+
MÉTRIQUES PAR PROTOCOLE
+
+
+ MÉTRIQUE + UDP + MQTT +
+ ${metrics.map(m => { + const udpOn = cfg.metrics?.[m]?.udp ? 'udp-on' : ''; + const mqttOn = cfg.metrics?.[m]?.mqtt ? 'mqtt-on' : ''; + return `
+
+
+ ${m} +
+
+
+
`; + }).join('')} +
+
+
+
PARAMÈTRES MQTT
+
+
+
+
+
+
+
+
+ ${['auto_discovery:Auto-discovery (Home Assistant):fa-satellite-dish', + 'birth_message:Birth message:fa-arrow-right-to-bracket', + 'last_will:Last Will message:fa-skull'].map(s => { + const [key, label, icon] = s.split(':'); + return `
+ + +
`; + }).join('')} +
+
+
+
+
+ COMMANDES DISTANTES — BIENTÔT +
+
+ ${[['fa-rotate-right','reboot'],['fa-power-off','shutdown'],['fa-display','screen off'], + ['fa-arrow-up-from-bracket','update'],['fa-arrow-up-right-dots','upgrade'],['fa-terminal','shell cmd']].map( + ([icon, label]) => `
+ + ${label} + bientôt +
`).join('')} +
+
`; + + document.getElementById('overlay-agentcfg').style.display = 'flex'; + } + + function toggleCbox(el, metric, proto) { + const isOn = el.classList.contains(proto + '-on'); + el.classList.toggle(proto + '-on', !isOn); + el.style.color = isOn ? 'transparent' : ''; + if (!_agentCfgData.metrics) _agentCfgData.metrics = {}; + if (!_agentCfgData.metrics[metric]) _agentCfgData.metrics[metric] = {}; + _agentCfgData.metrics[metric][proto] = !isOn; + } + + async function sendAgentConfig() { + if (!_currentAgentId || !_agentCfgData) return; + if (!_agentCfgData.protocols) _agentCfgData.protocols = {}; + _agentCfgData.protocols.mqtt = { + ..._agentCfgData.protocols.mqtt, + host: document.getElementById('mqtt-host')?.value ?? '10.0.0.3', + port: parseInt(document.getElementById('mqtt-port')?.value ?? '1883'), + topic_base: document.getElementById('mqtt-topic')?.value ?? 'nanometrics/agents', + auto_discovery: document.getElementById('mqtt-auto_discovery')?.checked ?? true, + birth_message: document.getElementById('mqtt-birth_message')?.checked ?? true, + last_will: document.getElementById('mqtt-last_will')?.checked ?? true, + }; + try { + await API.putAgentConfig(_currentAgentId, _agentCfgData); + document.getElementById('overlay-agentcfg').style.display = 'none'; + } catch (e) { + alert('Erreur lors de l\'envoi : ' + e.message); + } + } + + // ══ CONFIG SERVEUR ══ + async function showSrvCfg() { + const cfg = App.serverConfig ?? {}; + document.getElementById('btn-srvcfg').classList.add('active-btn'); + document.getElementById('srvcfg-body').innerHTML = ` +
+
AFFICHAGE DES TUILES
+
+ + ${cfg.tile_min_width ?? 220}px
+
+ + ${cfg.font_size ?? 13}px
+
+
+
SEUILS D'ALERTE
+
+ + ${cfg.warn_cpu ?? 70}%
+
+ + ${cfg.err_cpu ?? 85}%
+
+ + ${cfg.warn_disk ?? 75}%
+
+
+
DONNÉES & RÉTENTION
+
+
+
+
+
`; + document.getElementById('overlay-srvcfg').style.display = 'flex'; + } + + function hideSrvCfg() { + document.getElementById('overlay-srvcfg').style.display = 'none'; + document.getElementById('btn-srvcfg').classList.remove('active-btn'); + } + + async function saveSrvCfg() { + const cfg = { + ...App.serverConfig, + tile_min_width: parseInt(document.getElementById('s-tile-w')?.value ?? 220), + font_size: parseInt(document.getElementById('s-font')?.value ?? 13), + warn_cpu: parseInt(document.getElementById('s-warn-cpu')?.value ?? 70), + err_cpu: parseInt(document.getElementById('s-err-cpu')?.value ?? 85), + warn_disk: parseInt(document.getElementById('s-warn-disk')?.value ?? 75), + retention_days: parseInt(document.getElementById('s-retention')?.value ?? 30), + chart_duration_min: parseInt(document.getElementById('s-chart-dur')?.value ?? 30), + }; + await API.putServerConfig(cfg); + App.serverConfig = cfg; + document.documentElement.style.setProperty('--tile-min', cfg.tile_min_width + 'px'); + document.body.style.fontSize = cfg.font_size + 'px'; + hideSrvCfg(); + } + + // ══ POPUP SMART ══ + function showSmart(agentId) { + const m = Grid.getAgent(agentId)?.metrics?.smart; + if (!m) return; + document.getElementById('smart-sub').textContent = agentId; + const passColor = m.passed ? 'var(--ok)' : 'var(--err)'; + const passText = m.passed ? 'Disque en bonne santé' : 'Disque en mauvais état'; + const passSub = m.passed + ? 'Aucun problème détecté. Le disque fonctionne normalement.' + : 'Des problèmes ont été détectés. Envisagez un remplacement.'; + + document.getElementById('smart-body').innerHTML = ` +
+
+
${passText}
+
${passSub}
+
+
+
POINTS DE CONTRÔLE
+
+ ${m.temperature != null ? `
+
+ + Température + Normale +
+
${m.temperature}°C
+
Idéal : 20–50°C. Au-delà de 60°C le disque risque de s'abîmer.
+
` : ''} + ${m.reallocated_sectors != null ? `
+
+ + Secteurs défectueux +
+
${m.reallocated_sectors} sect.
+
S'ils apparaissent en grand nombre, une panne est imminente.
+
` : ''} + ${m.power_on_hours != null ? `
+
+ + Heures de fonctionnement +
+
${m.power_on_hours.toLocaleString('fr-FR')}h
+
≈${Math.floor(m.power_on_hours/24)} jours. Un disque dure en moyenne 3 à 5 ans.
+
` : ''} + ${m.wear_level != null ? `
+
+ + Durée de vie SSD +
+
${m.wear_level}%
+
100% = neuf · 0% = fin de vie recommandée.
+
` : ''} +
+
`; + + document.getElementById('overlay-smart').style.display = 'flex'; + } + + return { + showDetail, hideDetail, + showAgentCfg, sendAgentConfig, toggleCbox, + showSrvCfg, hideSrvCfg, saveSrvCfg, + showSmart, + }; +})();