Disks: one card per physical drive (NVMe namespaces folded under their controller), partitions summarised on one line instead of listed

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-18 14:18:20 -07:00
co-authored by Claude Fable 5.1
parent a372378098
commit 0af24f41c3
4 changed files with 112 additions and 23 deletions
+14 -10
View File
@@ -87,14 +87,13 @@ function DeviceCard({ d, live, forceOpen }: { d: StorageDevice; live?: { read_bp
<span className="num">{fmtBytes(d.size)}{d.temp ? ` · ${d.temp.toFixed(0)}°C` : ''}</span>
</div>
{live && <div className="sub-head num muted">read {fmtRate(live.read_bps)} · write {fmtRate(live.write_bps)} · {pct(live.util_pct)} busy</div>}
{d.parts.map((p) => (
<div className="row" key={p.name}>
<span className="k">{p.name}<small>{p.fstype || p.type}{p.label ? ` · ${p.label}` : ''}</small></span>
{p.fs_size ? <Meter frac={(p.used ?? 0) / p.fs_size} /> : <span className="muted">{p.mount === '[SWAP]' ? 'swap' : p.mount ? '' : 'not mounted'}</span>}
<span className="num">{p.fs_size ? `${fmtBytes(p.used ?? 0)} / ${fmtBytes(p.fs_size)} · ${p.mount}` : fmtBytes(p.size)}</span>
</div>
))}
{d.parts.length === 0 && <div className="muted">No partitions.</div>}
<div className="muted small disk-summary">
{d.serial && <span>S/N {d.serial}</span>}
{d.namespaces && d.namespaces.length > 1 && <span>{d.namespaces.length} namespaces</span>}
<span>{d.parts.length} partition{d.parts.length === 1 ? '' : 's'}</span>
{d.parts.filter((p) => p.mount && p.mount !== '[SWAP]').length > 0 && <span>mounted at {d.parts.filter((p) => p.mount && p.mount !== '[SWAP]').map((p) => p.mount).join(', ')}</span>}
{d.parts.some((p) => p.mount === '[SWAP]') && <span>swap</span>}
</div>
{forceOpen === null && <div className="svc-actions"><button className="btn" onClick={() => setOpen(!open)}>{open ? 'Hide SMART' : 'SMART'}</button></div>}
{open && <SmartPanel dev={d.name} rotational={d.rotational} />}
</section>
@@ -200,7 +199,12 @@ export default function Disks() {
}, [rate])
if (!devs) return <div className="dash">{err ? <div className="error">{err}</div> : <Loading label="Scanning disks…" />}</div>
const liveFor = (n: string) => live?.disks.find((d) => d.name === n)
const liveFor = (dev: StorageDevice) => {
const names = dev.namespaces?.length ? dev.namespaces : [dev.name]
const rows = (live?.disks ?? []).filter((x) => names.includes(x.name))
if (rows.length === 0) return undefined
return { read_bps: rows.reduce((a, x) => a + x.read_bps, 0), write_bps: rows.reduce((a, x) => a + x.write_bps, 0), util_pct: Math.max(...rows.map((x) => x.util_pct)) }
}
return (
<div className="dash disks">
@@ -215,7 +219,7 @@ export default function Disks() {
{!hasSmart && <div className="notice">SMART health needs smartmontools on this host. Install it with the button, then reload.</div>}
{task && <TaskOutput id={task} onDone={(t) => { toast(`${t.title}: ${t.exit_code === 0 ? 'finished' : `exited ${t.exit_code}`}`, t.exit_code === 0 ? 'ok' : 'err'); load() }} />}
<div className="cards">
{devs.map((d) => <DeviceCard key={d.name} d={d} live={liveFor(d.name)} forceOpen={allSmart ? true : null} />)}
{devs.map((d) => <DeviceCard key={d.name} d={d} live={liveFor(d)} forceOpen={allSmart ? true : null} />)}
</div>
<div className="dash-bar"><h2 className="section">ZFS</h2></div>