fdf76477e5
- ServerConfig: champ gauge_type (défaut "compact") - CSS: classes .gs-* pour la BatteryGauge standard (label + bar 9px + gloss interne) - Grid: helper renderGaugeRow() — sélectionne compact ou standard selon la config - Grid: rerenderAll() pour appliquer le changement sans recharger la page - Popup config serveur: select "Type de jauge" dans la section Affichage des tuiles Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
219 lines
9.2 KiB
JavaScript
219 lines
9.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 renderGaugeRow(faIcon, tip, label, pct, fillClass, valStr, extra) {
|
|
const standard = (App.serverConfig?.gauge_type ?? 'compact') === 'standard';
|
|
if (standard) {
|
|
return `<div class="gs-row">
|
|
<div class="gs-header">
|
|
<span class="gs-ico" data-tip="${tip}"><i class="fa-solid fa-${faIcon}"></i></span>
|
|
<span class="gs-lbl">${label}</span>
|
|
<span class="gs-val">${valStr}</span>${extra || ''}
|
|
</div>
|
|
<div class="gs-bar">
|
|
<div class="gs-fill ${fillClass}" style="width:${(pct ?? 0).toFixed(1)}%"></div>
|
|
<div class="gs-gloss"></div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
return `<div class="g-row">
|
|
<div class="g-ico" data-tip="${tip}"><i class="fa-solid fa-${faIcon}"></i></div>
|
|
<div class="g-bar"><div class="g-fill ${fillClass}" style="width:${(pct ?? 0).toFixed(0)}%"></div></div>
|
|
<span class="g-val">${valStr}</span>${extra || ''}
|
|
</div>`;
|
|
}
|
|
|
|
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 smartIco = !offline && metrics?.smart != null
|
|
? (metrics.smart.passed
|
|
? `<i class="fa-solid fa-shield-check" style="color:var(--ok);font-size:10px;flex-shrink:0" data-tip="SMART OK"></i>`
|
|
: `<i class="fa-solid fa-triangle-exclamation" style="color:var(--err);font-size:10px;flex-shrink:0" data-tip="SMART FAILED"></i>`)
|
|
: '';
|
|
|
|
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:none;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">
|
|
${renderGaugeRow('microchip', 'CPU', 'CPU',
|
|
offline ? 0 : (cpu ?? 0),
|
|
offline ? '' : gFill(cpu ?? 0),
|
|
offline ? '—' : fmtPct(cpu))}
|
|
${renderGaugeRow('memory', 'RAM', 'MÉMOIRE',
|
|
offline ? 0 : (memPct ?? 0),
|
|
offline ? '' : gFill(memPct ?? 0),
|
|
offline ? '—' : (metrics?.memory_used && metrics?.memory_total ? fmt(metrics.memory_used) + '/' + fmt(metrics.memory_total) : '—'))}
|
|
${renderGaugeRow('hard-drive', 'Disque', 'DISQUE',
|
|
offline ? 0 : (diskPct ?? 0),
|
|
offline ? '' : (diskPct >= (App.serverConfig?.warn_disk ?? 75) ? 'w' : ''),
|
|
offline ? '—' : (metrics?.hdd_used && metrics?.hdd_total ? fmt(metrics.hdd_used) + '/' + fmt(metrics.hdd_total) : '—'),
|
|
smartIco)}
|
|
</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) {
|
|
let entry = _agents.get(agentId);
|
|
if (!entry) {
|
|
// Nouvel agent découvert via WebSocket — on crée la tuile à la volée
|
|
const agent = {
|
|
id: agentId,
|
|
hostname: metrics.hostname || agentId,
|
|
ip: metrics.ip || '',
|
|
status: metrics.status || 'online',
|
|
};
|
|
_agents.set(agentId, { agent, metrics });
|
|
const grid = document.getElementById('agents-grid');
|
|
if (grid) grid.insertAdjacentHTML('beforeend', renderTile(agent, metrics));
|
|
updateStats();
|
|
return;
|
|
}
|
|
// Mettre à jour ip/status depuis les métriques fraîches
|
|
if (metrics.ip) entry.agent.ip = metrics.ip;
|
|
if (metrics.status) entry.agent.status = metrics.status;
|
|
// 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: a.last_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 rerenderAll() {
|
|
const grid = document.getElementById('agents-grid');
|
|
if (!grid) return;
|
|
grid.innerHTML = [..._agents.values()].map(({ agent, metrics }) => renderTile(agent, metrics)).join('');
|
|
}
|
|
|
|
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, rerenderAll, getAgent, fmt, fmtPct };
|
|
})();
|