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
+17
-10
@@ -1,9 +1,10 @@
|
||||
import { useCallback, useEffect, useState } from 'react'
|
||||
import { api, Sample, SmartInfo, StorageDevice, ZFSInfo, ZFSSnapshot } from './api'
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react'
|
||||
import { api, SmartInfo, StorageDevice, ZFSInfo, ZFSSnapshot } from './api'
|
||||
import { fmtBytes, fmtRate } from './Dashboard'
|
||||
import TaskOutput from './TaskOutput'
|
||||
import { Btn, Loading, Spinner, toast } from './ui'
|
||||
import { useRefreshMs } from './prefs'
|
||||
import { Sensor, useSensorStream } from './sensors'
|
||||
|
||||
const pct = (v: number) => `${Math.round(v)}%`
|
||||
const KIND: Record<string, string> = { nvme: 'NVMe', ssd: 'SSD', hdd: 'HDD', usb: 'USB', emmc: 'eMMC', sd: 'SD', other: 'disk' }
|
||||
@@ -183,7 +184,6 @@ export default function Disks() {
|
||||
const [devs, setDevs] = useState<StorageDevice[] | null>(null)
|
||||
const [zfs, setZfs] = useState<ZFSInfo | null>(null)
|
||||
const [hasSmart, setHasSmart] = useState(true)
|
||||
const [live, setLive] = useState<Sample | null>(null)
|
||||
const [err, setErr] = useState('')
|
||||
const [task, setTask] = useState<string | null>(null)
|
||||
const [allSmart, setAllSmart] = useState<boolean>(() => localStorage.getItem('hamlab.disks.smart') === '1')
|
||||
@@ -196,13 +196,20 @@ export default function Disks() {
|
||||
} catch (e) { setErr(e instanceof Error ? e.message : 'failed') }
|
||||
}, [])
|
||||
useEffect(() => { load() }, [load])
|
||||
useEffect(() => {
|
||||
let stop = false
|
||||
const tick = async () => { if (!stop && document.visibilityState === 'visible') { try { setLive(await api.metrics()) } catch { /* ignore */ } } }
|
||||
tick()
|
||||
const t = setInterval(tick, rate)
|
||||
return () => { stop = true; clearInterval(t) }
|
||||
}, [rate])
|
||||
// Live per-device rates come from the sensor stream, grouped by the kernel name in each sensor's labels.
|
||||
const stream = useSensorStream<{ d: Record<string, Sensor> }>({ d: 'disk.*' }, { everyMs: rate })
|
||||
const live = useMemo(() => {
|
||||
const by: Record<string, { name: string; read_bps: number; write_bps: number; util_pct: number }> = {}
|
||||
for (const x of Object.values(stream.data?.d ?? {})) {
|
||||
const name = x.labels?.device
|
||||
if (!name || typeof x.value !== 'number') continue
|
||||
const row = (by[name] ??= { name, read_bps: 0, write_bps: 0, util_pct: 0 })
|
||||
if (x.id.endsWith('.read')) row.read_bps = x.value
|
||||
else if (x.id.endsWith('.write')) row.write_bps = x.value
|
||||
else if (x.id.endsWith('.util')) row.util_pct = x.value
|
||||
}
|
||||
return { disks: Object.values(by) }
|
||||
}, [stream.data])
|
||||
|
||||
if (!devs) return <div className="dash">{err ? <div className="error">{err}</div> : <Loading label="Scanning disks…" />}</div>
|
||||
const liveFor = (dev: StorageDevice) => {
|
||||
|
||||
Reference in New Issue
Block a user