The full switch: Monitors and Disks read only the sensor API; /api/metrics retired
- stock Monitors modules fed by one sensor stream, the catalog and one batch of histories (GET /api/sensors/history?ids=…), through sampleAdapter.ts - the stream closes while the page is hidden and refills its curves on return - Disks tab live rates from the stream - new sensors: hamlab.sample_interval, ups.last_event - stream: asking for about the sampling rate means every sample - NUT runtime rounded at the source Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
f01175ceca
commit
4ddf10be51
@@ -0,0 +1,71 @@
|
||||
// The stock Monitors 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, Series } from './api'
|
||||
import { Sensor } from './sensors'
|
||||
|
||||
export type Values = Record<string, number | boolean | string>
|
||||
export type Catalog = Map<string, Sensor>
|
||||
|
||||
// Everything the modules draw, as one stream query.
|
||||
export const MONITOR_SHAPE = { v: '*' }
|
||||
// The curves they keep: CPU, memory, per-NIC and per-disk rates, every temperature.
|
||||
export const MONITOR_HISTORY = 'cpu.usage,mem.used,net.*.rx,net.*.tx,disk.*.read,disk.*.write,disk.*.util,temp.*'
|
||||
|
||||
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.<x>.rx by the real name in the catalog's labels, keeping catalog order.
|
||||
function groups(v: Values, cat: Catalog, prefix: string, label: string): Map<string, string> {
|
||||
const out = new Map<string, string>() // 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') })),
|
||||
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.
|
||||
export function seriesOf(hist: { id: string; t: number[]; v: number[] }[], cat: Catalog): Series {
|
||||
const by = new Map(hist.map((h) => [h.id, h]))
|
||||
const t = by.get('cpu.usage')?.t ?? []
|
||||
const n = t.length
|
||||
const fit = (id: string) => { const v = (by.get(id)?.v ?? []).slice(-n); return [...new Array(Math.max(0, n - v.length)).fill(0), ...v] as number[] }
|
||||
const out: Series = { t: [...t], cpu: fit('cpu.usage'), mem: fit('mem.used'), net: {}, disk: {}, temp: {} }
|
||||
for (const h of hist) {
|
||||
const l = cat.get(h.id)?.labels
|
||||
const stem = h.id.slice(0, h.id.lastIndexOf('.'))
|
||||
if (h.id.startsWith('net.') && l?.iface) out.net[l.iface] ??= { rx: fit(stem + '.rx'), tx: fit(stem + '.tx') }
|
||||
else if (h.id.startsWith('disk.') && l?.device) out.disk[l.device] ??= { r: fit(stem + '.read'), w: fit(stem + '.write'), u: fit(stem + '.util') }
|
||||
else if (h.id.startsWith('temp.') && l?.chip !== undefined) out.temp[`${l.chip}/${l.label}`] = fit(h.id)
|
||||
}
|
||||
return out
|
||||
}
|
||||
Reference in New Issue
Block a user