Repository navigation
Live system meters (issue #40) - #44
Merged
Merged
Conversation
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
marked this pull request as ready for review
July 24, 2026 19:00
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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/SensorManagerwith 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.cpu_percent/mem_percentsources — identical on Linux, macOS Intel, and Apple Silicon, no per-platform install.widget_updateframes on change (100ms tick, ~1 frame/s/meter, deduped on unchanged value).MeterCell+useMeterStore: readout, bar,localStoragepersistence, stale handling.default.yaml: CPU/MEM meters; thectrl+tkey 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
useMeterStorereturned a fresh object each render, soApp'sonWidgetUpdate— and the socket effect depending on it — churned every render, tearing the WebSocket down before it could authenticate. Now depends on the stableonUpdate; store is memoised.unauthorizedframe, 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./500: fallback route called the nonexistent_req.response(...); now usesweb.Response.PortInUseErrorwith an actionable message + exit 1 instead of a rawOSErrortraceback.Tooling / docs
just kill— free the daemon (:8765) and Vite (:5173) ports.wsswon't connect / no gate".Testing
4401close code).MeterCell+useMeterStoreidentity-stability regression tests);tsc+eslintclean.🤖 Generated with Claude Code