From a6d7ff436fd0c6f7f308f5efa5e7efd00a834974 Mon Sep 17 00:00:00 2001 From: Mitchell-Johnson Date: Wed, 11 Nov 2026 00:06:33 -0800 Subject: [PATCH] Drive tile counts each source device once (a bind mount doubled nexus's NVMe) fs.* sensors carry a source label; Top.tsx dedupes by it. Co-Authored-By: Claude Opus 5.5 --- internal/metrics/metrics.go | 5 ++++- internal/metrics/sensors.go | 3 +++ web/src/Top.tsx | 4 +++- web/src/api.ts | 2 +- web/src/sampleAdapter.ts | 2 +- 5 files changed, 12 insertions(+), 4 deletions(-) diff --git a/internal/metrics/metrics.go b/internal/metrics/metrics.go index 285541b..df3a020 100644 --- a/internal/metrics/metrics.go +++ b/internal/metrics/metrics.go @@ -102,6 +102,9 @@ type FS struct { // said honestly: a ZFS pool or an md array spreads a filesystem over several drives, and // a remote mount is on none of them. Disk string `json:"disk,omitempty"` + // Source is the device the mount is of ("/dev/nvme0n1p1"). A bind mount has the same + // source as the filesystem it shows, so a sum over a drive counts each source once. + Source string `json:"source,omitempty"` } type UPS struct { @@ -863,7 +866,7 @@ func filesystems() []FS { return } bs := uint64(st.Bsize) - done <- FS{Mount: mount, Type: fs[2], Size: st.Blocks * bs, Used: (st.Blocks - st.Bfree) * bs, Remote: remoteFS[fs[2]], Disk: diskOf(fs[0])} + done <- FS{Mount: mount, Type: fs[2], Size: st.Blocks * bs, Used: (st.Blocks - st.Bfree) * bs, Remote: remoteFS[fs[2]], Disk: diskOf(fs[0]), Source: fs[0]} }() select { case r := <-done: diff --git a/internal/metrics/sensors.go b/internal/metrics/sensors.go index eadd533..a9956dc 100644 --- a/internal/metrics/sensors.go +++ b/internal/metrics/sensors.go @@ -137,6 +137,9 @@ func flatten(s Sample, want func(id string) bool) []sensors.Sensor { if x.Disk != "" { lbl = append(lbl, "disk", x.Disk) } + if x.Source != "" { + lbl = append(lbl, "source", x.Source) + } f.add(p+".size", x.Mount+" size", "bytes", "B", float64(x.Size), s.fsT, none, lbl...) f.add(p+".used", x.Mount+" used", "bytes", "B", float64(x.Used), s.fsT, upTo(float64(x.Size)), lbl...) f.add(p+".used_pct", x.Mount+" used", "usage", "%", ratio(x.Used, x.Size), s.fsT, pct, lbl...) diff --git a/web/src/Top.tsx b/web/src/Top.tsx index e58e43e..dbdcda0 100644 --- a/web/src/Top.tsx +++ b/web/src/Top.tsx @@ -490,7 +490,9 @@ export default function Top() { // 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) + // a bind mount (nexus's /var/log is /data/var-log) is the same filesystem twice: + // count each source device once, or the drive reads double + const used = [...new Map(on.map((f) => [f.source || f.mount, f.used])).values()].reduce((a, b) => a + b, 0) return f.mount)} pool={pools[d.name]} /> })} {(s?.disks ?? []).length === 0 &&
no drives reported
} diff --git a/web/src/api.ts b/web/src/api.ts index e0a49e4..721ccf7 100644 --- a/web/src/api.ts +++ b/web/src/api.ts @@ -75,7 +75,7 @@ export interface Temp { chip: string; label: string; c: number; max?: number; cr export interface Fan { chip: string; label: string; rpm: number } export interface NetIf { iface: string; up: boolean; rx_bps: number; tx_bps: number; rx_total: number; tx_total: number } export interface Disk { name: string; model?: string; size: number; read_bps: number; write_bps: number; util_pct: number; temp?: number } -export interface FS { mount: string; type: string; size: number; used: number; remote?: boolean; disk?: string } +export interface FS { mount: string; type: string; size: number; used: number; remote?: boolean; disk?: string; source?: string } export interface UPS { driver: string; model: string; state: string; source: string; on_battery: boolean; in_v: number; out_v: number; battery_pct: number; runtime_min: number; load_w: number; load_pct: number; last_event?: string } export interface Proc { pid: number; ppid: number; name: string; cmd?: string; user: string; cpu: number; gpu?: number; cpu_time: number; rss: number; threads: number; state: string; nice: number; started: number } export interface MemInfo { total: number; used: number; avail: number; free?: number; cached?: number; buffers?: number } diff --git a/web/src/sampleAdapter.ts b/web/src/sampleAdapter.ts index 62d8e53..3ba4d30 100644 --- a/web/src/sampleAdapter.ts +++ b/web/src/sampleAdapter.ts @@ -51,7 +51,7 @@ export function sampleOf(v: Values, cat: Catalog): Sample { fans: hw('fan.').map(({ id, s }) => ({ chip: s.labels!.chip, label: s.labels!.label, rpm: num(v, id) })), net: [...groups(v, cat, 'net.', 'iface')].map(([iface, p]) => ({ iface, up: v[p + '.up'] === true, rx_bps: num(v, p + '.rx'), tx_bps: num(v, p + '.tx'), rx_total: num(v, p + '.rx_total'), tx_total: num(v, p + '.tx_total') })), disks: [...groups(v, cat, 'disk.', 'device')].map(([name, p]) => ({ name, model: cat.get(p + '.size')?.labels?.model || undefined, size: num(v, p + '.size'), read_bps: num(v, p + '.read'), write_bps: num(v, p + '.write'), util_pct: num(v, p + '.util'), temp: num(v, p + '.temp') || undefined })), - fs: [...groups(v, cat, 'fs.', 'mount')].map(([mount, p]) => ({ mount, type: cat.get(p + '.size')?.labels?.fstype ?? '', size: num(v, p + '.size'), used: num(v, p + '.used'), remote: cat.get(p + '.size')?.labels?.remote === '1', disk: cat.get(p + '.size')?.labels?.disk })), + fs: [...groups(v, cat, 'fs.', 'mount')].map(([mount, p]) => ({ mount, type: cat.get(p + '.size')?.labels?.fstype ?? '', size: num(v, p + '.size'), used: num(v, p + '.used'), remote: cat.get(p + '.size')?.labels?.remote === '1', disk: cat.get(p + '.size')?.labels?.disk, source: cat.get(p + '.size')?.labels?.source })), ups: v['ups.battery'] === undefined ? null : { driver: cat.get('ups.last_event')?.labels?.driver ?? '', model: cat.get('ups.battery')?.labels?.model ?? '', state: str(v, 'ups.state'), source: str(v, 'ups.source'), on_battery: v['ups.on_battery'] === true, in_v: num(v, 'ups.in_v'), out_v: num(v, 'ups.out_v'), battery_pct: num(v, 'ups.battery'), runtime_min: num(v, 'ups.runtime'),