// public/views/cards/storage.js — Proxmox storage health: ZFS pools, dropped pools, // and per-container disk fill. Surfaces the two failure modes that have actually bitten // this homelab (a pool dropping off the SATA bus; a container rootfs filling up). import { el, mount } from '../../dom.js'; import { api } from '../../api.js'; let body, timer; const gb = b => (b >= 1e12 ? (b / 1e12).toFixed(1) + 'T' : Math.round(b / 1e9) + 'G'); const cls = s => (s === 'crit' ? 'bad' : s === 'warn' ? 'warn' : 'ok'); const dotClass = s => 'status-' + (s === 'crit' ? 'down' : s === 'warn' ? 'warn' : 'ok'); function meterRow(label, value, p, status) { const wrap = el('div', { class: dotClass(status) }); wrap.appendChild(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, el('span', { class: 'dot' }), label), el('span', { class: cls(status) }, value))); if (p != null) { wrap.appendChild(el('div', { class: 'st-meter' }, el('div', { class: 'st-fill ' + cls(status), style: { width: Math.min(p, 100) + '%' } }))); } return wrap; } async function load() { if (!body) return; try { const s = await api.get('/api/storage'); if (s.error) { mount(body, el('span', { class: 'muted' }, 'Storage: ' + s.error)); return; } const kids = []; // overall badge kids.push(el('div', { class: 'sv-row' }, el('span', { class: 'k' }, 'Status'), el('span', { class: 'cl-badge ' + (s.worst === 'ok' ? 'ok' : 'bad') }, s.worst === 'ok' ? 'HEALTHY' : s.worst === 'warn' ? 'WATCH' : 'ATTENTION'))); // dropped pools first (most urgent — e.g. donatello/leonardo off the bus) for (const d of (s.down || [])) kids.push(meterRow(d.name + ' · ' + d.node, '⚠ ' + String(d.state).toUpperCase(), null, 'crit')); // imported ZFS pools for (const p of (s.pools || [])) kids.push(meterRow(p.name + ' · ' + p.node, (p.health !== 'ONLINE' ? p.health + ' · ' : '') + (p.pct ?? '–') + '%', p.pct, p.status)); // container disk fill (top few by %) const top = (s.guests || []).slice(0, 5); if (top.length) kids.push(el('div', { class: 'sv-subhdr' }, 'Container disk')); for (const g of top) kids.push(meterRow('CT ' + g.vmid + ' ' + g.name, g.pct + '% · ' + gb(g.used) + '/' + gb(g.total), g.pct, g.status)); mount(body, el('div', { class: 'sv-cluster' }, ...kids)); } catch { mount(body, el('span', { class: 'muted' }, 'Storage unavailable')); } } export default { id: 'storage', title: 'Storage · capacity', size: 'm', mount(e) { body = e; load(); }, start() { timer = setInterval(load, 30000); }, stop() { clearInterval(timer); body = null; } };