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
+28
-7
@@ -32,24 +32,45 @@ export async function fetchHistory(id: string, src?: Source): Promise<{ t: numbe
|
||||
return r.ok ? r.json() : null
|
||||
}
|
||||
|
||||
// useSensorStream keeps a shaped query answered: the object comes back filled with whole
|
||||
// sensors and is replaced on every sample. One stream serves any number of widgets.
|
||||
export function useSensorStream<T = unknown>(shape: Shape | null, src?: Source, everyMs?: number): { data: T | null; error: string } {
|
||||
export async function fetchHistories(ids: string, src?: Source): Promise<{ id: string; unit?: string; t: number[]; v: number[] }[]> {
|
||||
const r = await fetch(url(src, '/api/sensors/history', { ids }), { credentials: 'same-origin' })
|
||||
if (!r.ok) throw new Error(r.statusText)
|
||||
return (await r.json()).series
|
||||
}
|
||||
|
||||
export interface StreamOpts { src?: Source; everyMs?: number; full?: boolean }
|
||||
|
||||
// useSensorStream keeps a shaped query answered: the object comes back filled (with whole
|
||||
// sensors unless full is false) and is replaced on every sample. One stream serves any
|
||||
// number of widgets. It closes while the page is hidden — a phone in a pocket shouldn't
|
||||
// hold a stream open — and `resumed` counts the times it came back, so a caller that
|
||||
// keeps history knows there is a gap to refill.
|
||||
export function useSensorStream<T = unknown>(shape: Shape | null, opts: StreamOpts = {}): { data: T | null; error: string; resumed: number } {
|
||||
const [data, setData] = useState<T | null>(null)
|
||||
const [error, setError] = useState('')
|
||||
const [resumed, setResumed] = useState(0)
|
||||
const [visible, setVisible] = useState(() => document.visibilityState === 'visible')
|
||||
const key = shape === null ? '' : JSON.stringify(shape)
|
||||
const srcKey = `${src?.machine ?? '·'}|${src?.token ?? ''}|${everyMs ?? ''}`
|
||||
const { src, everyMs, full = true } = opts
|
||||
const srcKey = `${src?.machine ?? '·'}|${src?.token ?? ''}|${everyMs ?? ''}|${full}`
|
||||
const srcRef = useRef(src); srcRef.current = src
|
||||
useEffect(() => {
|
||||
const onVis = () => { const v = document.visibilityState === 'visible'; setVisible(v); if (v) setResumed((n) => n + 1) }
|
||||
document.addEventListener('visibilitychange', onVis)
|
||||
return () => document.removeEventListener('visibilitychange', onVis)
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (!key) { setData(null); return }
|
||||
const params: Record<string, string> = { q: key, full: '1' }
|
||||
if (!visible) return
|
||||
const params: Record<string, string> = { q: key }
|
||||
if (full) params.full = '1'
|
||||
if (everyMs) params.every = String(everyMs)
|
||||
const es = new EventSource(url(srcRef.current, '/api/sensors/stream', params))
|
||||
es.onmessage = (e) => { try { setData(JSON.parse(e.data)); setError('') } catch { /* skip a bad frame */ } }
|
||||
es.onerror = () => setError('reconnecting…') // EventSource retries by itself
|
||||
return () => es.close()
|
||||
}, [key, srcKey]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return { data, error }
|
||||
}, [key, srcKey, visible]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
return { data, error, resumed }
|
||||
}
|
||||
|
||||
// ---- formatting, by unit ----
|
||||
|
||||
Reference in New Issue
Block a user