The disks box is drives; the net box lets you pick the interface

Disks: one row per physical drive with its model, size, temperature and IO, and a usage
bar built from the filesystems the sampler can attribute to it. Attribution is new — a
mount's source device is resolved through sysfs to the drive behind it, following one
layer of device-mapper or md and folding NVMe namespaces the way the sampler does. A pool
member gets no usage bar, because one drive's share of a raidz pool is not a real number.

Net: every interface that is up is listed and tappable, defaulting to the one that has
actually carried traffic rather than whichever the kernel listed first.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-24 12:09:39 -07:00
co-authored by Claude Opus 5
parent bf0bdd54af
commit e0625be684
6 changed files with 101 additions and 17 deletions
+41 -14
View File
@@ -250,6 +250,7 @@ export default function Top() {
// the process table, and the CPU model, come from the same place the Processes page reads
const [pt, setPt] = useState<ProcTable | null>(null)
const [pickedIf, setPickedIf] = useState<string | null>(null) // which NIC the net box draws
const [err, setErr] = useState('')
useEffect(() => {
let stop = false
@@ -319,7 +320,10 @@ export default function Top() {
const free = (mem as { free?: number }).free ?? Math.max(0, mem.total - mem.used - cached)
const swap = s?.swap ?? pt?.swap
const net = (s?.net ?? pt?.net ?? []).filter((n) => n.up && n.iface !== 'lo')
const iface = net[0]
// the one that has actually carried traffic, not whichever the kernel listed first — wg0
// and a dormant wlan should not push the real NIC out of the way. Tapping a name switches.
const busiest = net.length ? net.reduce((a, b) => (b.rx_total + b.tx_total > a.rx_total + a.tx_total ? b : a)) : undefined
const iface = net.find((n) => n.iface === pickedIf) ?? busiest
// the disks box is this machine's own storage: a CIFS or NFS share is somebody else's disk
const fs = (s?.fs ?? []).filter((f) => !f.remote)
const pct = (a: number, b: number) => (b > 0 ? (a / b) * 100 : 0)
@@ -379,18 +383,26 @@ export default function Top() {
<Box n={3} name="disks" className="top-disks" right="io">
<div className="t-disks">
{swap && swap.total > 0 && <Disk name="swap" size={swap.total} used={swap.used} />}
{fs.map((f) => {
// which drive is behind a mount isn't in the sample, so only the root filesystem
// gets an IO line, against the machine's largest drive — which is the one it is on
const d = f.mount === '/' ? [...(s?.disks ?? [])].sort((a, b) => b.size - a.size)[0] : undefined
return <Disk key={f.mount} name={f.mount === '/' ? 'root' : f.mount.split('/').filter(Boolean).pop() ?? f.mount} size={f.size} used={f.used} io={d ? d.util_pct : undefined} />
{(s?.disks ?? []).map((d) => {
// what is on this drive: the filesystems the sampler could attribute to it. A ZFS
// pool or an md array spans several drives, so nothing is attributed and the row
// shows what is true of the hardware — how busy it is, and how hot.
const on = fs.filter((f) => f.disk === d.name)
const used = on.reduce((a, f) => a + f.used, 0)
return <Disk key={d.name} name={d.name} note={d.model} size={d.size} used={on.length ? used : undefined} io={d.util_pct} temp={d.temp} mounts={on.map((f) => f.mount)} />
})}
{fs.length === 0 && <div className="t-dim">no filesystems reported</div>}
{(s?.disks ?? []).length === 0 && <div className="t-dim">no drives reported</div>}
</div>
</Box>
<Box n={4} name="net" className="top-net" right={iface ? <span className="t-if">{iface.iface}</span> : undefined}>
<Box n={4} name="net" className="top-net" right={net.length ? (
<span className="t-ifs">
{net.map((n) => (
<button key={n.iface} className={'t-if' + (iface && n.iface === iface.iface ? ' on' : '')}
title={`${n.iface} — ${bytes(n.rx_total)} in, ${bytes(n.tx_total)} out`} onClick={() => setPickedIf(n.iface)}>{n.iface}</button>
))}
</span>
) : undefined}>
{iface ? (
<div className="t-net">
<div className="t-net-graphs">
@@ -471,14 +483,29 @@ const Rows = memo(function Rows({ procs, sel, onSel, curves, empty }: { procs: P
)
})
function Disk({ name, size, used, io }: { name: string; size: number; used: number; io?: number }) {
const upct = size > 0 ? (used / size) * 100 : 0
// One physical drive. `used` is the bytes of the filesystems on it, left out when none can
// be attributed to it — a pool member's share of a pool is not a number worth inventing.
function Disk({ name, note, size, used, io, temp, mounts }: { name: string; note?: string; size: number; used?: number; io?: number; temp?: number; mounts?: string[] }) {
const upct = used !== undefined && size > 0 ? (used / size) * 100 : 0
return (
<div className="t-disk">
<div className="t-row"><b>{name}</b><span className="spacer" />{bytes(size)}</div>
<div className="t-row">
<b>{name}</b>
{note && <span className="t-dim t-model" title={note}>{note}</span>}
<span className="spacer" />
{temp !== undefined && temp > 0 && <span className="t-temp">{Math.round(temp)}°C</span>}
{bytes(size)}
</div>
{io !== undefined && <div className="t-row t-sub"><span>IO%</span><Meter pct={io} grad="io" /></div>}
<div className="t-row t-sub"><span>Used:</span><span className="t-pct">{Math.round(upct)}%</span><Meter pct={upct} grad="used" /><span className="t-amt">{bytes(used, 1)}</span></div>
<div className="t-row t-sub"><span>Free:</span><span className="t-pct">{Math.round(100 - upct)}%</span><Meter pct={100 - upct} grad="free" /><span className="t-amt">{bytes(Math.max(0, size - used), 1)}</span></div>
{used !== undefined ? (
<>
<div className="t-row t-sub"><span>Used:</span><span className="t-pct">{Math.round(upct)}%</span><Meter pct={upct} grad="used" /><span className="t-amt">{bytes(used, 1)}</span></div>
<div className="t-row t-sub"><span>Free:</span><span className="t-pct">{Math.round(100 - upct)}%</span><Meter pct={100 - upct} grad="free" /><span className="t-amt">{bytes(Math.max(0, size - used), 1)}</span></div>
{mounts && mounts.length > 0 && <div className="t-row t-sub t-dim t-mounts" title={mounts.join(' ')}>{mounts.join(' ')}</div>}
</>
) : (
<div className="t-row t-sub t-dim">no filesystem of this machine is on it</div>
)}
</div>
)
}