feat(v0.1.5): SMART multi-disques — collecte tous les disques détectés

Agent:
- SmartMetrics + champ device (nom du disque ex: sda, nvme0)
- smart: Option<Vec<SmartMetrics>> — tous les disques, pas seulement le 1er
- collect() itère /sys/block, accumule les résultats de tous les disques valides

Serveur:
- SmartMetrics.Device + Smart []SmartMetrics dans AgentMetrics
- InsertMetrics: stocke smart_json (JSON array) au lieu de colonnes plates
- GetLastMetrics: désérialise smart_json
- Migration: smart_json TEXT ajoutée

Dashboard:
- Tuile: une icône shield/triangle par disque avec tooltip incluant le nom
- Popup détail: un bouton SMART par disque (couleur ok/err)
- showSmart(agentId, diskIdx): affiche le disque sélectionné

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Gilles Soulier
2026-05-23 05:23:23 +02:00
parent 1b9daae08a
commit a53923fd8e
9 changed files with 52 additions and 60 deletions
+5 -4
View File
@@ -77,10 +77,11 @@ const Grid = (() => {
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 smartIco = !offline && metrics?.smart?.length > 0
? metrics.smart.map(s => s.passed
? `<i class="fa-solid fa-shield-check" style="color:var(--ok);font-size:10px;flex-shrink:0" data-tip="SMART OK${s.device}"></i>`
: `<i class="fa-solid fa-triangle-exclamation" style="color:var(--err);font-size:10px;flex-shrink:0" data-tip="SMART FAILED${s.device}"></i>`
).join('')
: '';
const iconContent = `<img src="${API.iconUrl(id)}" alt=""
+13 -11
View File
@@ -80,15 +80,16 @@ const Popups = (() => {
? `<div class="chart-minmax"><span>min ${Grid.fmt(ramMin)}</span><span>max ${Grid.fmt(ramMax)}</span></div>`
: '';
const smartBtn = metrics?.smart
? `<div class="smart-btn ok" onclick="Popups.showSmart('${esc(agentId)}')" data-tip="Voir la santé complète du disque">
<div class="smart-dot"></div>
<span style="font-weight:600">SMART</span>
const smartBtn = metrics?.smart?.length > 0
? metrics.smart.map((s, i) => `
<div class="smart-btn ${s.passed ? 'ok' : 'err'}" onclick="Popups.showSmart('${esc(agentId)}',${i})" data-tip="Voir la santé du disque ${esc(s.device)}">
<div class="smart-dot" style="${s.passed ? '' : 'background:var(--err);box-shadow:0 0 5px var(--err)'}"></div>
<span style="font-weight:600">${esc(s.device) || 'disque'}</span>
<span>·</span>
<span>${metrics.smart.passed ? 'PASSED' : 'FAILED'}</span>
${metrics.smart.temperature ? `<span style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3)"><i class="fa-solid fa-temperature-half"></i> ${metrics.smart.temperature}°C</span>` : ''}
<span>${s.passed ? 'PASSED' : 'FAILED'}</span>
${s.temperature != null ? `<span style="font-family:var(--font-mono);font-size:10px;color:var(--ink-3)"><i class="fa-solid fa-temperature-half"></i> ${s.temperature}°C</span>` : ''}
<i class="fa-solid fa-chevron-right" style="font-size:10px;color:var(--ink-4);margin-left:auto"></i>
</div>`
</div>`).join('')
: '';
const protos = [
@@ -389,10 +390,11 @@ const Popups = (() => {
}
// ══ POPUP SMART ══
function showSmart(agentId) {
const m = Grid.getAgent(agentId)?.metrics?.smart;
if (!m) return;
document.getElementById('smart-sub').textContent = agentId;
function showSmart(agentId, diskIdx = 0) {
const smartList = Grid.getAgent(agentId)?.metrics?.smart;
if (!smartList?.length) return;
const m = smartList[diskIdx] ?? smartList[0];
document.getElementById('smart-sub').textContent = m.device ? `${agentId}${m.device}` : agentId;
const passColor = m.passed ? 'var(--ok)' : 'var(--err)';
const passText = m.passed ? 'Disque en bonne santé' : 'Disque en mauvais état';
const passSub = m.passed