Files
nano_metrics/dashboard/js/grid.js
T
Gilles Soulier e0ed96309c fix: conserver les métriques lentes (disque, smart) entre les paquets
Le disque est envoyé toutes les 60s mais les paquets arrivent toutes les 2s.
Chaque nouveau paquet écrasait les champs null, effaçant le disque affiché.
Correction : fusion avec les anciennes métriques, null ne remplace pas une valeur.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-22 20:14:45 +02:00

174 lines
7.2 KiB
JavaScript

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 = `<img src="${API.iconUrl(id)}" alt=""
style="width:100%;height:100%;object-fit:cover;border-radius:7px"
onerror="this.style.display='none';this.nextSibling.style.display='flex'">
<span style="display:flex;align-items:center;justify-content:center;width:100%;height:100%;color:var(--accent)">
<i class="fa-solid fa-server"></i></span>`;
return `<div class="tile ${sc}" id="tile-${id}" onclick="Popups.showDetail('${esc(id)}')">
<div class="tile-head">
<div class="t-icon">${iconContent}</div>
<div class="t-names">
<div class="t-host">${esc(agent.hostname)}</div>
<div class="t-ip">${esc(agent.ip) || '—'}</div>
</div>
<div class="t-led ${ledClass(agent.status)}"></div>
</div>
<div class="tile-gauges">
<div class="g-row">
<div class="g-ico" data-tip="CPU"><i class="fa-solid fa-microchip"></i></div>
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(cpu ?? 0)}"
style="width:${offline ? 0 : (cpu ?? 0).toFixed(0)}%"></div></div>
<span class="g-val">${offline ? '—' : fmtPct(cpu)}</span>
</div>
<div class="g-row">
<div class="g-ico" data-tip="RAM"><i class="fa-solid fa-memory"></i></div>
<div class="g-bar"><div class="g-fill ${offline ? '' : gFill(memPct ?? 0)}"
style="width:${offline ? 0 : (memPct ?? 0).toFixed(0)}%"></div></div>
<span class="g-val">${offline ? '—' : (metrics?.memory_used && metrics?.memory_total ? fmt(metrics.memory_used) + '/' + fmt(metrics.memory_total) : '—')}</span>
</div>
<div class="g-row">
<div class="g-ico" data-tip="Disque"><i class="fa-solid fa-hard-drive"></i></div>
<div class="g-bar"><div class="g-fill ${offline ? '' : (diskPct >= (App.serverConfig?.warn_disk ?? 75) ? 'w' : '')}"
style="width:${offline ? 0 : (diskPct ?? 0).toFixed(0)}%"></div></div>
<span class="g-val">${offline ? '—' : fmtPct(diskPct)}</span>
</div>
</div>
<div class="tile-foot">
<span class="tile-foot-info">
${offline
? '<i class="fa-solid fa-circle-xmark" style="color:var(--err)"></i><span style="color:var(--err)">Hors ligne</span>'
: `<i class="fa-solid fa-clock"></i><span>${uptimeStr || '—'}</span>`}
</span>
<button class="btn-del-agent" title="Supprimer cet agent"
onclick="event.stopPropagation();Popups.confirmDeleteAgent('${esc(id)}','${esc(agent.hostname)}')">
<i class="fa-solid fa-trash"></i>
</button>
</div>
</div>`;
}
function update(agentId, metrics) {
const entry = _agents.get(agentId);
if (!entry) return;
// Conserver les valeurs lentes (disque, smart) quand le paquet ne les contient pas
if (entry.metrics) {
for (const k of Object.keys(entry.metrics)) {
if (metrics[k] == null && entry.metrics[k] != null) metrics[k] = entry.metrics[k];
}
}
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 removeAgent(id) {
_agents.delete(id);
const el = document.getElementById('tile-' + id);
if (el) el.remove();
updateStats();
}
function getAgent(id) { return _agents.get(id); }
function updateStatus(agentId, status) {
const entry = _agents.get(agentId);
if (!entry) return;
entry.agent.status = status;
const el = document.getElementById('tile-' + agentId);
if (el) el.outerHTML = renderTile(entry.agent, entry.metrics);
updateStats();
}
return { refresh, update, updateStatus, removeAgent, getAgent, fmt, fmtPct };
})();