const Popups = (() => {
let _currentAgentId = null;
let _agentCfgData = null;
let _resizeObs = null;
let _resizeTimer = 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 smartBadges = metrics?.smart?.length > 0
? '' +
metrics.smart.map((s, i) => {
const st = Grid.smartState(s);
const lbl = { ok: 'OK', old: 'USAGÉ', prefail: 'PREFAIL', hs: 'HS' }[st];
return `
${esc(s.device)} · ${lbl}
`;
}).join('') + '
'
: '';
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
−30min −15min now
${ramMinMax}
STOCKAGE
${Grid.fmt(metrics?.hdd_used)} / ${Grid.fmt(metrics?.hdd_total)}
${smartBadges}
${(() => {
const ni = entry?.agent?.network_info;
if (!ni?.length) return '';
const wol = v => v == null ? '—' : v ? 'Oui ' : 'Non ';
const spd = v => v == null ? '—' : v >= 1000 ? '1 Gb' : v + ' Mb';
const rows = ni.map(iface => `
${esc(iface.name)}
${spd(iface.speed_mbps)}
${esc(iface.mac)}
WoL : ${wol(iface.wol)}
${iface.iperf_mbps != null ? iface.iperf_mbps.toFixed(1) + ' Mb/s' : '—'}
`).join('');
return `
RÉSEAU
INTERFACE VITESSE MAC WAKE ON LAN IPERF3
${rows}
`;
})()}
${(() => {
const hw = entry?.agent?.hardware_info;
if (!hw) return '';
const row = (lbl, val) => val ? `` : '';
const ramSlots = hw.ram_slots_used != null && hw.ram_slots_total != null
? `${hw.ram_slots_used}/${hw.ram_slots_total} slots` : null;
const ramInfo = [hw.ram_type, hw.ram_speed_mhz ? hw.ram_speed_mhz + ' MHz' : null, ramSlots]
.filter(Boolean).join(' · ') || null;
return `
HARDWARE
${row('CARTE MÈRE', hw.motherboard_vendor && hw.motherboard_model ? hw.motherboard_vendor + ' ' + hw.motherboard_model : hw.motherboard_model || hw.motherboard_vendor)}
${row('PROCESSEUR', hw.cpu_model)}
${row('MÉMOIRE RAM', ramInfo)}
`;
})()}
`;
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(() => {
clearTimeout(_resizeTimer);
_resizeTimer = setTimeout(() => {
API.putServerConfig({
...App.serverConfig,
popup_detail_w: pd.offsetWidth,
popup_detail_h: pd.offsetHeight,
}).catch(() => {});
}, 600);
});
_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
${metrics.map(m => {
const udpOn = cfg.metrics?.[m]?.udp ? 'udp-on' : '';
const mqttOn = cfg.metrics?.[m]?.mqtt ? 'mqtt-on' : '';
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 = `
DONNÉES & RÉTENTION
Historique
${[7,30,90,365].map(d => `${d} jours `).join('')}
Courbes (durée)
${[[15,'15 min'],[30,'30 min'],[60,'1 heure'],[360,'6 heures']].map(([v,l]) =>
`${l} `).join('')}
`;
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, diskIdx = 0) {
const smartList = Grid.getAgent(agentId)?.metrics?.smart;
if (!smartList?.length) return;
const m = smartList[diskIdx] ?? smartList[0];
const state = Grid.smartState(m);
document.getElementById('smart-sub').textContent = m.device ? `${agentId} — ${m.device}` : agentId;
const stateInfo = {
ok: { color:'var(--ok)', bg:'rgba(77,187,38,.1)', border:'rgba(77,187,38,.3)', icon:'fa-circle-check',
title:'Disque en bonne santé',
desc:'Aucun problème détecté. Votre disque fonctionne normalement.' },
old: { color:'var(--warn)', bg:'rgba(250,189,47,.1)', border:'rgba(250,189,47,.3)', icon:'fa-clock-rotate-left',
title:'Disque ancien ou très utilisé',
desc:'Votre disque fonctionne encore, mais il a accumulé beaucoup d\'heures. Pensez à prévoir un remplacement.' },
prefail: { color:'var(--accent)', bg:'var(--accent-tint)', border:'rgba(254,128,25,.3)', icon:'fa-triangle-exclamation',
title:'Signes de défaillance imminente',
desc:'Ce disque présente des indicateurs préoccupants. Sauvegardez vos données dès maintenant et envisagez un remplacement rapide.' },
hs: { color:'var(--err)', bg:'rgba(251,73,52,.1)', border:'rgba(251,73,52,.3)', icon:'fa-circle-xmark',
title:'Disque défaillant',
desc:'Ce disque a échoué au test SMART. Il peut tomber en panne à tout moment. Sauvegardez immédiatement et remplacez-le.' },
};
const si = stateInfo[state];
const tempColor = m.temperature == null ? null
: m.temperature > 60 ? 'var(--err)' : m.temperature > 50 ? 'var(--warn)' : 'var(--ok)';
const tempLabel = m.temperature == null ? null
: m.temperature > 60 ? 'Critique' : m.temperature > 50 ? 'Élevée' : 'Normale';
const tempBg = tempColor === 'var(--ok)' ? 'rgba(77,187,38,.15)'
: tempColor === 'var(--warn)' ? 'rgba(250,189,47,.15)' : 'rgba(251,73,52,.15)';
const secColor = m.reallocated_sectors == null ? null
: m.reallocated_sectors === 0 ? 'var(--ok)' : m.reallocated_sectors < 10 ? 'var(--warn)' : 'var(--err)';
const secDesc = m.reallocated_sectors === 0
? 'Aucun secteur défectueux — parfait.'
: m.reallocated_sectors < 10 ? 'Quelques secteurs remplacés. Surveillez l\'évolution.'
: 'Nombreux secteurs défectueux — risque de panne élevé.';
const hoursColor = m.power_on_hours == null ? null
: m.power_on_hours > 40000 ? 'var(--err)' : m.power_on_hours > 25000 ? 'var(--warn)' : 'var(--ok)';
const wearColor = m.wear_level == null ? null
: m.wear_level < 20 ? 'var(--err)' : m.wear_level < 50 ? 'var(--warn)' : 'var(--ok)';
const wearDesc = m.wear_level == null ? ''
: m.wear_level >= 80 ? 'Très bonne durée de vie restante.'
: m.wear_level >= 50 ? 'Durée de vie acceptable, à surveiller.'
: m.wear_level >= 20 ? 'Durée de vie réduite — pensez au remplacement.'
: 'Durée de vie critique — remplacez ce SSD rapidement.';
document.getElementById('smart-body').innerHTML = `
POINTS DE CONTRÔLE
${m.temperature != null ? `
Température
${tempLabel}
${m.temperature}°C
Normale entre 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.
${secDesc}
` : ''}
${m.power_on_hours != null ? `
Durée de fonctionnement
${m.power_on_hours.toLocaleString('fr-FR')} h
≈${Math.floor(m.power_on_hours / 24)} jours d'utilisation. Un disque dur dure en moyenne 3 à 5 ans (25 000–40 000 h).
` : ''}
${m.wear_level != null ? `
Durée de vie SSD restante
${m.wear_level}%
${wearDesc}
` : ''}
`;
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 (window.isSecureContext && 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,
};
})();