// The stock Dashboard modules were written against one Sample object. They now read the // sensor API like everything else; this file is the seam — it rebuilds that object from // sensor values (the live stream) and sensor metadata (the catalog: labels, thresholds). import { Sample } from './api' import { Sensor } from './sensors' export type Values = Record export type Catalog = Map // Everything the modules draw, as one stream query. export const MONITOR_SHAPE = ['cpu.*', 'load.*', 'uptime', 'hamlab.*', 'mem.*', 'swap.*', 'temp.*', 'fan.*', 'net.*', 'disk.*', 'fs.*', 'ups.*'] // The stream answers a list with a list; the modules want one flat map. export function mergeValues(parts: unknown[]): Values { const out: Values = {} parts.forEach((part, i) => { if (part && typeof part === 'object') Object.assign(out, part) else if (part !== null && part !== undefined) out[MONITOR_SHAPE[i]] = part as Values[string] }) return out } // The curves they keep: CPU, memory, per-NIC and per-disk rates, every temperature. const num = (v: Values, id: string) => (typeof v[id] === 'number' ? (v[id] as number) : 0) const str = (v: Values, id: string) => (typeof v[id] === 'string' ? (v[id] as string) : '') // Groups ids like net..rx by the real name in the catalog's labels, keeping catalog order. function groups(v: Values, cat: Catalog, prefix: string, label: string): Map { const out = new Map() // real name → id stem ("net.enp3s0") for (const id of Object.keys(v)) { if (!id.startsWith(prefix)) continue const real = cat.get(id)?.labels?.[label] if (real && !out.has(real)) out.set(real, id.slice(0, id.lastIndexOf('.'))) } return new Map([...out].sort((a, b) => a[0].localeCompare(b[0]))) // by real name: "/" leads the filesystems } // Every id in the values must be known to the catalog, or the catalog is stale (a disk // or interface appeared) and wants refetching. export const stale = (v: Values, cat: Catalog) => Object.keys(v).some((id) => !cat.has(id)) export function sampleOf(v: Values, cat: Catalog): Sample { const cores = Object.keys(v).map((id) => /^cpu\.core\.(\d+)\.usage$/.exec(id)).filter(Boolean).map((m) => [Number(m![1]), v[m![0]] as number] as const).sort((a, b) => a[0] - b[0]).map((x) => x[1]) const mem = (p: string) => ({ total: num(v, p + '.total'), used: num(v, p + '.used'), avail: num(v, p + '.avail'), free: num(v, p + '.free'), cached: num(v, p + '.cached'), buffers: num(v, p + '.buffers') }) const hw = (prefix: string) => Object.keys(v).filter((id) => id.startsWith(prefix) && cat.get(id)?.labels?.chip !== undefined).map((id) => ({ id, s: cat.get(id)! })) .sort((a, b) => a.s.labels!.chip.localeCompare(b.s.labels!.chip) || a.s.labels!.label.localeCompare(b.s.labels!.label)) return { t: Date.now(), interval_ms: num(v, 'hamlab.sample_interval'), cpu: num(v, 'cpu.usage'), cores, load: [num(v, 'load.1'), num(v, 'load.5'), num(v, 'load.15')], mem: mem('mem'), swap: mem('swap'), uptime: num(v, 'uptime'), temps: hw('temp.').map(({ id, s }) => ({ chip: s.labels!.chip, label: s.labels!.label, c: num(v, id), max: s.warn, crit: s.crit })), 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 })), 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'), load_w: num(v, 'ups.load'), load_pct: num(v, 'ups.load_pct'), last_event: str(v, 'ups.last_event') || undefined, }, } } // The modules' history: one shared time axis (the CPU's), every other curve right-aligned // to it and zero-padded on the left where the sensor is younger than the ring.