feat(dashboard): rendu grille + tuiles dynamiques

This commit is contained in:
Gilles Soulier
2026-05-22 12:41:26 +02:00
parent 5f7cf9f837
commit 8cd18b14b2
+145
View File
@@ -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 = `<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('${id}')">
<div class="tile-head">
<div class="t-icon">${iconContent}</div>
<div class="t-names">
<div class="t-host">${agent.hostname}</div>
<div class="t-ip">${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 ? '—' : fmtPct(memPct)}</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">
${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>`}
</div>
</div>`;
}
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 };
})();