dashboard: quick-wins batch (#69, #64, #74)#87
Merged
Conversation
sound_event already carries `balance`; the dashboard rendered only a bare event tag. Capture it into a 60 s rolling buffer and render an inline SVG sparkline on the State tile — directly visualises the stuck-left localizer bias tracked in #27. - bridge.py: _sound_balance_history deque + _sound_balance_series getter, fed from the sound_event perception handler. - dashboard.py: _sound_balance_sparkline builds the SVG polyline; /ui/state passes it to the template. - state.html: sparkline row beneath the state buttons. Closes #69. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
zeroclaw-discord runs on the same RPi as the bridge, so its health is a local `systemctl` query — no SSH needed. New /ui/discord card shows the unit's systemd state (active / failed / inactive) with a status dot and the active-since timestamp. - dashboard.py: DISCORD_UNIT env (ZEROCLAW_DISCORD_UNIT), _discord_unit_status via `systemctl show`, GET /ui/discord. - discord.html: status card body. - dashboard.html: new card after the perception card. Model / channel / last-message would need the daemon to expose a status endpoint — out of scope for this card, which surfaces daemon health. Closes #64. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vision endpoint already classifies failures by `err_kind` for the Prometheus metric; add a lightweight in-memory aggregate so the dashboard surfaces capture trouble without a metrics scrape. - bridge.py: _vision_failures deque + _vision_failure_record (called from the vision_explain finally block, where err_kind is already in scope) + _vision_failures_last_hour getter. - dashboard.py: _vision_failures_count helper, wired into the perception card context. - perception.html: "N capture failures in the last hour" line in the SIGHT scene-view box, shown only when non-zero. Closes #74. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
This PR batches three small dashboard enhancements to improve operational visibility on the bridge UI: a sound-localizer balance sparkline, a local systemd health card for the Discord daemon, and an aggregated “vision capture failures (last hour)” indicator on the Perception card.
Changes:
- Add a 60s rolling sound-localizer
balancesparkline on the State tile. - Add a new
/ui/discordpartial and dashboard card that queries localsystemctlstatus forzeroclaw-discord. - Track and display aggregated vision capture failures over the last hour.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated 3 comments.
Show a summary per file
| File | Description |
|---|---|
| bridge/templates/state.html | Renders the sound-balance sparkline when data is available. |
| bridge/templates/perception.html | Displays a “failures in last hour” warning line when non-zero. |
| bridge/templates/discord.html | New partial template for the Discord daemon status card. |
| bridge/templates/dashboard.html | Adds the Discord card container with HTMX polling. |
| bridge/dashboard.py | Wires new getters, builds sparkline context, adds /ui/discord endpoint. |
| bridge.py | Adds rolling buffers + getters for sound balance and vision failures; wires them into the dashboard. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+3637
to
+3638
| if err_kind: | ||
| _vision_failure_record(err_kind) |
Comment on lines
+1718
to
+1731
| # tuples; the getter windows to the last hour and counts by error kind. | ||
| _vision_failures: "collections.deque[tuple[float, str]]" = collections.deque( | ||
| maxlen=200 | ||
| ) | ||
|
|
||
|
|
||
| def _vision_failure_record(kind: str) -> None: | ||
| """Record a vision-capture failure for the dashboard aggregator (#74).""" | ||
| _vision_failures.append((time.time(), str(kind or "unknown"))) | ||
|
|
||
|
|
||
| def _vision_failures_last_hour() -> dict[str, int]: | ||
| """Vision-capture failures in the last hour, counted by error kind.""" | ||
| cutoff = time.time() - 3600.0 |
Comment on lines
+1710
to
+1714
| """Sound-localizer `balance` samples from the last | ||
| `_SOUND_BALANCE_WINDOW_SEC`, oldest→newest — the dashboard sparkline | ||
| source (#69). Empty when no sound_event has landed recently.""" | ||
| cutoff = time.time() - _SOUND_BALANCE_WINDOW_SEC | ||
| return [bal for ts, bal in _sound_balance_history if ts >= cutoff] |
`_call_vision_api` swallows its own request/timeout exceptions and returns an `ERROR:` sentinel string rather than raising. A dead or unconfigured VLM endpoint therefore reached `vision_explain` as an ordinary return, leaving `err_kind` unset — so the failure aggregator added in this branch never counted the single most common failure mode. Promote the two sentinel strings to named constants and add `_classify_vision_result()`, which `vision_explain` now applies to the final description. Failures are recorded as `camera_offline` (API key unset), `vision_api` (request failed / non-2xx / bad body), or `exception` (anything raised) — so the per-kind aggregation and the `dotty_request_errors_total` metric carry real signal. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
BrettKinny
added a commit
that referenced
this pull request
May 22, 2026
Brings in 5 commits that landed on main while the ZeroClaw-retirement doc sweep was in progress (#53 per-person memory, #74, #81 hardware-doc verification, #88 CI fix, the #87 dashboard-quick-wins merge). Conflicts resolved (2 files): - CLAUDE.md — kept the post-#36 architecture diagram and pi_voice Config-Files bullet over main's older ZeroClaw text. - docs/faq.md — combined main's verified servo spec (feedback servos, SCS0009 pitch) with the ZeroClaw-retirement fix to the brain-host line. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
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.
Dashboard quick-wins
Three small, self-contained dashboard improvements — one commit each.
sound_eventalreadycarries
balance; capture it into a 60 s rolling buffer and render aninline SVG sparkline on the State tile. Directly visualises the
stuck-left localizer bias tracked in Stereo mic head-turn — calibrate localizer + re-enable #27.
zeroclaw-discordshares the RPiwith the bridge, so its health is a local
systemctlquery — no SSH.New
/ui/discordcard with a status dot + active-since timestamp.classifies failures by
err_kind; add an in-memory, hour-windowedcount, surfaced in the perception card's SIGHT box when non-zero.
Not included — #72 (content-filter rule-detail ring)
Pulled from the batch — spec/code mismatch. The issue assumes a
dotty_content_filter_hits_totalincrement atbridge.py:1286with therule + tier in scope. In the current tree
content_filter()is importedfrom a
textUtilsmodule and returns only the replacement string — itexposes neither rule nor tier. The
(rule, redacted-prefix, tier)ringtherefore needs a
content_filter()API change in a shared module, not adashboard-only edit. Re-triage comment posted on #72.
Verification
py_compileand Jinja tag-balance are clean. The bridge test suite and alive dashboard render need the RPi (
fastapietc. not installed in thedev env).
Closes #69. Closes #64. Closes #74.
🤖 Generated with Claude Code