0.30.0 split the page into two cadences — live values on a two-second stream, history charts on their own timer — and the split quietly went wrong: every number moved every two seconds while every chart sat still for up to two minutes. This release closes that gap, and takes per-host data the rest of the way.
Fixed
-
Charts move with the numbers again. The history timer was pinned to the server's bucket size — 60s on the default 6h range — and the live path only ever assigned the current values, never touching a chart series. So a tile and the chart directly beneath it could disagree by two minutes. Each live frame is now folded into the newest bucket on the client, at no extra request, because the stream already carried every field it needed. Within a bucket the point keeps a running mean, seeded from how far into the bucket it already is, so it converges on the average the server will send instead of jittering — and the next authoritative fetch barely moves it. Measured chart lag before this, by range: 1h ~25s, 6h ~120s, 24h ~5min, 7d ~29min,
all~5.5h. -
A GPU spike is a spike again. VRAM is the only step metric on the page, so averaging it across a 60s bucket didn't merely delay a model load — it flattened the peak away entirely. A load of 481 MB → 7900 MB now lands at 7900 immediately; the mean would have drawn ~4191. Rate metrics stay averaged, because for those the average is the honest number.
-
The per-card GPU panels refresh like everything else. The small-multiples grid only repainted on the 60s history fetch, because the full render replaces the whole grid and re-binds every card's click and keyboard handler — it couldn't run on the live cadence without fighting the pointer. Per-card temp, watts and utilisation are the fastest-moving numbers on the page, so the grid sat still while the detail chart behind it tracked live. Each card's body is now written in place and the wrapper — handlers, tabindex, focus ring — is left untouched. Anything structural still goes through the full render.
-
Four ways the refresh loop could stop dead. A throw out of a synchronous renderer skipped the reschedule and killed the chain for the life of the page — invisible, because the header shows a clock time rather than an age. A stream flapping on its 3s retry hint reset the 15s timer before it could ever fire: 300 seconds of flapping produced zero refreshes.
LIVE_ONtracked connection events but never data arrival, so a dead-but-open stream left the cadence slow and stopped polling the fleet altogether. And a terminally failedEventSourcewas never cleared, so the page could not open a stream again for the rest of its life. -
Chart overlays stopped lying. The in-place chart update never reassigned inline plugins, and the GPU plugins close over the payload they were built from — so throttle bands and the VRAM capacity line stayed frozen at page-load state while the lines underneath them moved. The overview chart's cache key was labels-only, holding it still for a whole bucket at a time. A range switch now re-arms the poll instead of waiting out the old range's tick.
-
Switching to a remote host left the hub's data on screen. Only the System card changed; Top processes, Power & cost and the CPU/RAM/load chart kept the hub's numbers under the remote machine's name. The bug had a peculiar shape: two renderers each open with a correct guard that hides them on a remote — and their only caller wrapped them in the same condition, so on a remote they were never called, the guard never ran, and the last local paint stayed. The chart had no guard at all. It only surfaced on the local → remote switch, never on a fresh load, which is how it survived.
-
The System tab's chart now draws the host you selected. Per-host CPU, RAM and load were already being written to
host_sampleson every poll, but nothing ever read them back — fixing the chart meant adding the reader it never had. A host with no history yet says so, rather than drawing an empty chart that is indistinguishable from a chart of zeroes.
Added
- Top processes works on every host, not just the hub. The mini-htop card was hub-only because
/procwas the hub's and the probe never shipped a process table. It does now:probe.pywalks/proctwice around the same sub-second dwell its CPU reading already takes, so a remote gains a process table without its poll taking any longer than before. The aggregation is the hub's, line for line, because a number meaning "percent of one core" in one place and something else in another is worse than no number. The two sampling windows do differ, and the card says which one you're reading. Per-process disk I/O needs/proc/<pid>/io, root-only on most distributions, so a remote reports it unavailable rather than shipping an empty table.
Internal
The dashboard has its first tests. tests/js/test_refresh_loop.js runs the real functions — lifted out of the shipped HTML, unmodified — against a fake clock: 65 checks, wired into CI on the runner's preinstalled Node, with no npm project and no new dependency. It was mutation-tested by re-introducing each original bug one at a time; every one is caught by the check written for it. tests/test_dashboard_refresh_invariants.py guards the structure and now actually runs in CI, which had been invoking pytest on the snapshot suite alone.