Skip to content

Live system meters (issue #40) - #44

Merged
jonocodes merged 4 commits into
mainfrom
ticket-40-meters
Jul 24, 2026
Merged

jonocodes merged 4 commits into
mainfrom
ticket-40-meters

Conversation

@jonocodes

Copy link
Copy Markdown
Owner

Summary

Adds live system meters to deckd — psutil-backed CPU% and MEM% cells that render a value + proportional bar and update over the WebSocket. Also folds in several robustness fixes uncovered while getting the feature running end-to-end.

Note

Draft — more work to come on this branch.

Meters / sensors

  • SensorSource / SensorManager with refcounted, layout-driven polling: a source is only polled while the active layout has a meter bound to it. Switch to a meter-less layout (e.g. firefox) and all psutil reads stop; switch back and they resume.
  • psutil-backed cpu_percent / mem_percent sources — identical on Linux, macOS Intel, and Apple Silicon, no per-platform install.
  • Server sensor pump broadcasts widget_update frames on change (100ms tick, ~1 frame/s/meter, deduped on unchanged value).
  • Client MeterCell + useMeterStore: readout, bar, localStorage persistence, stale handling.
  • default.yaml: CPU/MEM meters; the ctrl+t key button is commented out (kept as a worked example) since the meters now own its grid slot. Meter icons removed per design.

Fixes made along the way

  • Reconnect storm / no password gate: useMeterStore returned a fresh object each render, so App's onWidgetUpdate — and the socket effect depending on it — churned every render, tearing the WebSocket down before it could authenticate. Now depends on the stable onUpdate; store is memoised.
  • WS auth rejection dropped by browsers: the daemon closed immediately after sending the unauthorized frame, and browsers dropped the coalesced frame. Added a short grace before close + a dedicated close code (4401) the client also treats as unauthorized; guarded the send against an already-closing transport.
  • SPA / 500: fallback route called the nonexistent _req.response(...); now uses web.Response.
  • Fail-fast port bind: PortInUseError with an actionable message + exit 1 instead of a raw OSError traceback.

Tooling / docs

  • just kill — free the daemon (:8765) and Vite (:5173) ports.
  • README: Tailscale-dev troubleshooting for "page loads but wss won't connect / no gate".

Testing

  • Daemon: 218 passed (adds meters, sensors, port-in-use, 4401 close code).
  • Client: 31 passed (adds MeterCell + useMeterStore identity-stability regression tests); tsc + eslint clean.
  • Verified end-to-end in a real browser through the Vite proxy: gate shows → password → live; meters render.

🤖 Generated with Claude Code

jonocodes and others added 4 commits July 24, 2026 11:12
Meters/sensors feature:
- SensorSource / SensorManager with refcounted, layout-driven polling:
  sources are only polled while the active layout has a meter bound to
  them, so switching to a meter-less layout (e.g. firefox) stops all
  psutil reads. psutil-backed CPU% and MEM% sources work identically on
  Linux/macOS.
- Server sensor pump broadcasts widget_update frames on change; client
  MeterCell + useMeterStore render the readout and bar, with localStorage
  persistence and stale handling.
- default.yaml: CPU/MEM meters; the ctrl+t key button is commented out
  (kept as a worked example) since the meters now own its grid slot.

Fixes found while getting it running end-to-end:
- Reconnect storm / no password gate: useMeterStore returned a fresh
  object each render, so App's onWidgetUpdate (and the socket effect that
  depends on it) churned every render, tearing down the WebSocket before
  it could authenticate. Depend on the stable onUpdate; memoise the store.
- WebSocket auth rejection: browsers dropped the unauthorized frame when
  the daemon closed immediately after sending it. Add a short grace before
  close and a dedicated close code (4401) the client also treats as
  unauthorized; guard the send against an already-closing transport.
- SPA fallback route called the nonexistent _req.response(...), 500ing on
  GET / when serving the built client. Use web.Response.
- Daemon fails fast with PortInUseError (clear message, exit 1) instead of
  a raw OSError traceback when the port is already bound.

Tooling/docs:
- just kill: free the daemon (:8765) and Vite (:5173) ports.
- README: Tailscale-dev troubleshooting for the "page loads but wss won't
  connect / no gate" case.
- Tests: meters, sensors, port-in-use, 4401 close code, meter-store
  identity stability.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
New `kind: stats` widget: a compact, bar-less cell that shows several
sensor values at once (one "LABEL value unit" row per metric). Simpler
than the single-value `meter` and easy to grow — add another entry to the
widget's `metrics` list. Each metric names a daemon-side source; `label`
is optional and the client derives one from the source name
(cpu_percent -> CPU) when omitted.

To let one cell show many sources (and two widgets share a source), the
client meter store is now keyed by **source** instead of widget id — a
reading belongs to a sensor, not a widget. Store/lookup/persistence and
the active-set reap all follow suit; localStorage migrates transparently
(old id-keyed entries fall back once, then re-key by source).

- default.yaml: the standalone MEM meter at [3,1] is replaced by a
  `System` stats cell showing CPU + MEM; the CPU bar meter stays at [2,1].
- Daemon: MetricSpec model + `stats` validation (non-empty metrics);
  subscription/pump fold both widget shapes via `_widget_sources`.
- Client: StatsCell + CSS; ButtonGrid dispatch; source-keyed store.
- Demo: the `?demo=meter` fixture gains a stats cell (reuses the existing
  cpu/mem seeds), so the gallery/screenshot tool showcases both styles.
- Tests: stats schema + pump (daemon, 221 pass); StatsCell render + store
  source-keying (client, 35 pass).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The System stats cell already shows CPU (and MEM), so the separate CPU bar
meter at [2,1] was redundant. Restore the previously-commented send-key-tab
(ctrl+t) button there instead, and keep the stats cell at [3,1].

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jonocodes
jonocodes marked this pull request as ready for review July 24, 2026 19:00
@jonocodes
jonocodes merged commit 06fb7e0 into main Jul 24, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant