feat(dashboard): rendu grille + tuiles dynamiques
This commit is contained in:
@@ -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 };
|
||||
})();
|
||||
Reference in New Issue
Block a user