Live values went from twenty-five seconds stale to two, and the app asks the server for less than it did before — because the fix was to stop conflating how often something is measured with how often it is stored. Chasing a stream that ticked twice per interval then turned up something older and worse: every container ever shipped has been running two complete copies of the application.
Added
The dashboard moves in real time now — and asks the server for less than it used to. Screen cadence and storage cadence are separate things at last. SAMPLE_INTERVAL still governs what gets written, because every energy and cost figure is integrated as sum(watts) × INTERVAL and a denser sample would silently reprice your whole history. Alongside it, a new FAST_INTERVAL (2s, 0 disables) re-reads only the cheap counters, writes no rows, and wakes a server-sent-event stream; the heavy work stays on the sampler. /api/data — ~15 bucketed aggregates plus the entire charted series — is now fetched no faster than its own buckets can change.
Live values: ~25s worst case → ~2s. Requests over 65 seconds on a live box: /api/data 4 → 1, /api/fleet 4 → 0, and a backgrounded tab costs nothing at all.
Charts and KPIs refresh in place instead of the page being rebuilt. Every refresh used to destroy and re-instantiate every chart on the page — which is also why the code had to hand-save and restore which datasets you'd toggled off. They're updated in place now, and count-up animations don't re-run on a live tick, because a number that re-animates every two seconds never settles.
Disk-content scanning works on every host, not just the hub. Remote machines used to get a notice explaining that the Disks tab couldn't help them. Now a remote is scanned with the same du over the SSH connection the fleet is already polled on, with df along for free space — still nothing to install on the far end. Paths reaching a remote shell are validated and shell-quoted (there's a test that pushes each hostile path through a shell parser to prove it stays one argument), and Windows or unreachable hosts now say which of the two is the problem instead of offering a button that would fail.
Fixed
Every container was running two copies of the application. The entrypoint starts the monitor as a script, so app.py is the module "__main__" — and every lazy import app as _app under backend/ found nothing registered under "app" and executed the whole file a second time as a separate module object. That gave one process two SQLite connections and two LOCKs each guarding a different object. It also gave it two LATEST dicts, so the API served one while half the samplers wrote the other — and, because the worker threads start at module level, two collectors, two host pollers, two notifiers.
Nothing ever crashed, because the second copy never reached the line that binds the port. What it did instead was SSH-probe every registered remote twice per interval and write every append-only table twice: on the v0.29.1 build, 268 duplicate (ts, service) groups in proc and 10,663 in net_samples within a single hour, hidden in samples behind INSERT OR REPLACE. Registering the module under its import name before the first backend import fixes it; duplicate rows go to zero, and the fleet is polled half as often as it was yesterday.
The star-history chart in the README rendered as a broken image. api.star-history.com 301-redirects a mixed-case repo owner to its lower-case form, and GitHub's camo image proxy doesn't follow the redirect — it caches the empty 301.
The README's "what's new" banner still announced v0.26 three releases later, and the configuration table was missing nine variables that ship. The banner no longer names a version, so it can't go stale again.
Upgrading
Nothing to do — no configuration changes are required and existing history is untouched. FAST_INTERVAL defaults to 2; set it to 0 if you'd rather the dashboard poll as it did before.
docker compose pull && docker compose up -dFull changelog: https://github.com/SikamikanikoBG/homelab-monitor/blob/main/CHANGELOG.md