Settings: one window for how far back every graph reaches, 1 minute to 24 hours

Each machine keeps 300 points per curve whatever the window, so memory doesn't follow it;
a wider window means wider points, each the average of its bucket rather than one instant
out of it. The hub passes the setting to every machine it manages.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Mitchell-Johnson
2026-09-20 19:07:55 -07:00
co-authored by Claude Opus 5
parent 885b050d02
commit 04dd98e2cd
13 changed files with 533 additions and 33 deletions
+19 -5
View File
@@ -41,17 +41,31 @@ function Gauge({ s }: { s: Sensor }) {
function Spark({ s, src }: { s: Sensor; src?: Source }) {
const pts = useRef<number[]>([])
const lastT = useRef(0)
const stride = useRef(0) // how far apart the machine's own points are, as they arrive
const [, bump] = useState(0)
useEffect(() => {
let stop = false
pts.current = []; lastT.current = 0
fetchHistory(s.id, src).then((h) => { if (!stop && h) { pts.current = h.v.slice(-MAX_POINTS); lastT.current = h.t[h.t.length - 1] ?? 0; bump((n) => n + 1) } })
pts.current = []; lastT.current = 0; stride.current = 0
fetchHistory(s.id, src).then((h) => {
if (stop || !h) return
pts.current = h.v.slice(-MAX_POINTS); lastT.current = h.t[h.t.length - 1] ?? 0
const t = h.t.slice(-MAX_POINTS)
const gaps = t.slice(1).map((x, i) => x - t[i]).sort((a, b) => a - b)
stride.current = gaps.length ? gaps[gaps.length >> 1] : 0 // the median: one long gap is a restart, not the spacing
bump((n) => n + 1)
})
return () => { stop = true }
}, [s.id, src?.machine, src?.token]) // eslint-disable-line react-hooks/exhaustive-deps
if (typeof s.value === 'number' && s.t > lastT.current) {
lastT.current = s.t
pts.current.push(s.value)
if (pts.current.length > MAX_POINTS) pts.current.shift()
// a new point once a stride has passed; until then the newest point keeps being refined,
// so a wide window's curve isn't squeezed by a fast stream of live values
if (pts.current.length && s.t - lastT.current < stride.current) {
pts.current[pts.current.length - 1] = s.value
} else {
lastT.current = s.t
pts.current.push(s.value)
if (pts.current.length > MAX_POINTS) pts.current.shift()
}
}
return <Sparkline series={[{ name: s.name, values: pts.current, color: 'var(--s1)' }]} max={s.unit === '%' ? 100 : s.max} format={(v) => fmtValue(v, s.unit)} height={40} />
}