From 8cd18b14b261f76e33b75bced263fffaf6955dd9 Mon Sep 17 00:00:00 2001 From: Gilles Soulier Date: Fri, 22 May 2026 12:41:26 +0200 Subject: [PATCH] feat(dashboard): rendu grille + tuiles dynamiques --- dashboard/js/grid.js | 145 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 dashboard/js/grid.js diff --git a/dashboard/js/grid.js b/dashboard/js/grid.js new file mode 100644 index 0000000..649f11b --- /dev/null +++ b/dashboard/js/grid.js @@ -0,0 +1,145 @@ +const Grid = (() => { + const _agents = new Map(); + + function statusClass(agent) { + if (agent.status === 'offline') return 't-off'; + const m = _agents.get(agent.id)?.metrics; + if (!m) return ''; + const cfg = App.serverConfig; + const errThreshold = cfg?.err_cpu ?? 85; + const warnThreshold = cfg?.warn_cpu ?? 70; + if ((m.cpu_percent ?? 0) >= errThreshold) return 't-err'; + if ((m.cpu_percent ?? 0) >= warnThreshold || (m.hdd_used && m.hdd_total && (m.hdd_used / m.hdd_total * 100) >= (cfg?.warn_disk ?? 75))) return 't-warn'; + return ''; + } + + function ledClass(status) { + return { online: 's-ok', warn: 's-warn', err: 's-err', offline: 's-off' }[status] ?? 's-off'; + } + + function fmt(bytes) { + if (!bytes) return '—'; + if (bytes < 1024) return bytes + 'o'; + if (bytes < 1024 ** 2) return (bytes / 1024).toFixed(1) + 'Ko'; + if (bytes < 1024 ** 3) return (bytes / 1024 ** 2).toFixed(1) + 'Mo'; + return (bytes / 1024 ** 3).toFixed(1) + 'Go'; + } + + function fmtPct(val) { + return val != null ? val.toFixed(0) + '%' : '—'; + } + + function gFill(pct) { + const cfg = App.serverConfig; + if (pct >= (cfg?.err_cpu ?? 85)) return 'e'; + if (pct >= (cfg?.warn_cpu ?? 70)) return 'w'; + return ''; + } + + function renderTile(agent, metrics) { + const id = agent.id; + const sc = statusClass(agent); + const offline = agent.status === 'offline'; + + const cpu = metrics?.cpu_percent ?? null; + const memPct = (metrics?.memory_used && metrics?.memory_total) + ? metrics.memory_used / metrics.memory_total * 100 : null; + const diskPct = (metrics?.hdd_used && metrics?.hdd_total) + ? metrics.hdd_used / metrics.hdd_total * 100 : null; + + const uptimeSec = metrics?.uptime; + let uptimeStr = ''; + if (uptimeSec) { + const d = Math.floor(uptimeSec / 86400); + const h = Math.floor((uptimeSec % 86400) / 3600); + uptimeStr = d > 0 ? `${d}j ${h}h` : `${h}h`; + } + + const iconContent = ` + + `; + + return `
+
+
${iconContent}
+
+
${agent.hostname}
+
${agent.ip || '—'}
+
+
+
+
+
+
+
+ ${offline ? '—' : fmtPct(cpu)} +
+
+
+
+ ${offline ? '—' : fmtPct(memPct)} +
+
+
+
+ ${offline ? '—' : fmtPct(diskPct)} +
+
+
+ ${offline + ? 'Hors ligne' + : `${uptimeStr}`} +
+
`; + } + + function update(agentId, metrics) { + const entry = _agents.get(agentId); + if (!entry) return; + entry.metrics = metrics; + const el = document.getElementById('tile-' + agentId); + if (el) { + el.outerHTML = renderTile(entry.agent, metrics); + } + updateStats(); + } + + function refresh(agents) { + agents.forEach(a => { + if (!_agents.has(a.id)) { + _agents.set(a.id, { agent: a, metrics: null }); + } else { + _agents.get(a.id).agent = a; + } + }); + const grid = document.getElementById('agents-grid'); + if (!grid) return; + grid.innerHTML = agents.map(a => renderTile(a, _agents.get(a.id)?.metrics)).join(''); + updateStats(); + } + + function updateStats() { + let total = 0, ok = 0, warn = 0, err = 0; + _agents.forEach(({ agent }) => { + total++; + const sc = statusClass(agent); + if (agent.status === 'offline') {} + else if (sc === 't-err') err++; + else if (sc === 't-warn') warn++; + else ok++; + }); + document.getElementById('stat-total').textContent = total; + document.getElementById('stat-ok').textContent = ok; + document.getElementById('stat-warn').textContent = warn; + document.getElementById('stat-err').textContent = err; + } + + function getAgent(id) { return _agents.get(id); } + + return { refresh, update, getAgent, fmt, fmtPct }; +})();