const Popups = (() => { let _currentAgentId = null; let _agentCfgData = null; let _resizeObs = 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; const hint = document.getElementById('icon-hint'); try { await API.uploadIcon(agentId, file); const ts = '?t=' + Date.now(); img.src = API.iconUrl(agentId) + ts; img.style.display = 'block'; document.getElementById('pop-icon-fa').style.display = 'none'; const tileImg = document.querySelector(`#tile-${CSS.escape(agentId)} .t-icon img`); if (tileImg) tileImg.src = API.iconUrl(agentId) + ts; } catch (err) { if (hint) { hint.style.color = 'var(--err)'; hint.textContent = 'Erreur : ' + (err.message || 'téléversement échoué'); setTimeout(() => { hint.style.color = ''; hint.textContent = 'Cliquer sur l\'icône pour personnaliser · JPG PNG WEBP · max 128×128 px'; }, 4000); } } finally { e.target.value = ''; } }; // 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); let ramMin = null, ramMax = null; for (const h of history) { if (h.memory_used != null) { if (ramMin === null || h.memory_used < ramMin) ramMin = h.memory_used; if (ramMax === null || h.memory_used > ramMax) ramMax = h.memory_used; } } const ramMinMax = ramMin !== null ? `
min ${Grid.fmt(ramMin)}max ${Grid.fmt(ramMax)}
` : ''; 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
${ramMinMax}
STOCKAGE
${Grid.fmt(metrics?.hdd_used)} / ${Grid.fmt(metrics?.hdd_total)}
${smartBtn}
INFORMATIONS
HOSTNAME
${esc(agent.hostname)}
ADRESSE IP
${esc(agent.ip) || '—'}
VERSION AGENT
${agent.version ? `v${esc(agent.version)}` : ''}
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 if (_resizeObs) _resizeObs.disconnect(); const pd = document.getElementById('popup-detail'); _resizeObs = new ResizeObserver(() => { API.putServerConfig({ ...App.serverConfig, popup_detail_w: pd.offsetWidth, popup_detail_h: pd.offsetHeight, }).catch(() => {}); }); _resizeObs.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
INSTALLATION AGENT
`; 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), gauge_type: document.getElementById('s-gauge-type')?.value ?? 'compact', }; const prevGaugeType = App.serverConfig?.gauge_type ?? 'compact'; 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'; if (cfg.gauge_type !== prevGaugeType) Grid.rerenderAll(); 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'; } // ══ SUPPRESSION AGENT ══ let _delAgentId = null; function confirmDeleteAgent(id, hostname) { _delAgentId = id; document.getElementById('del-agent-name').textContent = hostname || id; document.getElementById('overlay-del').style.display = 'flex'; } async function doDeleteAgent() { if (!_delAgentId) return; const id = _delAgentId; document.getElementById('overlay-del').style.display = 'none'; _delAgentId = null; try { await API.deleteAgent(id); Grid.removeAgent(id); } catch (e) { alert('Erreur lors de la suppression : ' + e.message); } } function _copyInstallCmd(btn) { const text = document.getElementById('s-install-cmd').value; const done = () => { btn.textContent = '✓ Copié'; setTimeout(() => btn.textContent = 'Copier', 1500); }; if (navigator.clipboard && navigator.clipboard.writeText) { navigator.clipboard.writeText(text).then(done).catch(() => _copyFallback(text, done)); } else { _copyFallback(text, done); } } function _copyFallback(text, cb) { const ta = document.createElement('textarea'); ta.value = text; ta.style.cssText = 'position:fixed;opacity:0;top:0;left:0'; document.body.appendChild(ta); ta.focus(); ta.select(); try { document.execCommand('copy'); cb(); } catch (_) {} document.body.removeChild(ta); } return { showDetail, hideDetail, showAgentCfg, sendAgentConfig, toggleCbox, showSrvCfg, hideSrvCfg, saveSrvCfg, confirmDeleteAgent, doDeleteAgent, showSmart, _copyInstallCmd, }; })();