Skip to content

Add metric detail modal with history range picker#54

Merged
LarsLaskowski merged 4 commits into
mainfrom
claude/issue-9-g9mae9
Jul 16, 2026
Merged

Add metric detail modal with history range picker#54
LarsLaskowski merged 4 commits into
mainfrom
claude/issue-9-g9mae9

Conversation

@LarsLaskowski

@LarsLaskowski LarsLaskowski commented Jul 15, 2026

Copy link
Copy Markdown
Owner

Summary

Implements the historical range picker / metric detail view (C1). Clicking a
metric card now opens a modal showing that metric's history in a larger chart,
with 5m / 15m / 1h range buttons that re-scale the chart to the chosen span.

  • Clickable cards: CPU Usage, Load Average, Temperature, Memory.
    Each is keyboard-accessible (role="button", tabindex="0", Enter/Space to
    open) with a hover/focus accent outline.
  • The detail modal reuses the existing updates-modal pattern
    (index.html / app.js / style.css) and the existing chart.js
    sparkline renderer — no new charting code.
  • Range buttons filter the retained history to the selected span (measured back
    from the most recent sample's timestamp, i.e. the Pi clock) and redraw the
    chart. A stats line reports now / min / avg / max and the sample count. The
    span is naturally bounded by the server's retained history
    (history_window_minutes), since points older than that are never returned by
    GET /api/v1/metrics/history.
  • The open modal stays in sync with freshly polled history and repaints on theme
    changes. It closes via the close button, backdrop click, or Escape.

No changes to the REST API or configuration — consumes the existing
GET /api/v1/metrics/history endpoint.

Verification

Built the binary and drove the live dashboard in a headless browser
(Chromium/Playwright):

  • Clicking a card opens the modal with the correct title and the default 15m
    range active; the enlarged chart renders the metric's history.
  • Clicking the 1h range button updates the active state and re-renders/re-scales
    the chart and stats.
  • Opening via keyboard (focus + Enter), and closing via Escape and backdrop
    click, all work with no console errors.

go build ./..., go vet ./..., go test ./..., and golangci-lint run
are all clean.

Related Issue

Closes #9

Checklist

  • Tests added/updated for the change (go test ./... passes locally) —
    change is frontend-only (HTML/CSS/JS); the existing internal/web embed
    tests still pass and there is no JS test harness in the repo. Verified
    end-to-end in a headless browser instead.
  • go vet ./... and golangci-lint run are clean
  • Documentation updated if this changes the REST API, configuration, or
    installation/packaging — N/A, no such changes
  • No breaking change to /api/v1/... response shapes, or a new API
    version was introduced instead — no API changes

Clicking a metric card (CPU, Load, Temperature, Memory) now opens a
modal showing that metric's history in a larger chart, with 5m/15m/1h
range buttons that re-scale the chart to the chosen span. The window is
naturally bounded by the server's retained history (history_window_minutes),
since points beyond that are never returned by /api/v1/metrics/history.

Reuses the existing updates-modal pattern and the chart.js sparkline
renderer; no API or config changes. The open modal stays in sync with
freshly polled history and repaints on theme changes.

Cards are keyboard-accessible (role=button, Enter/Space to open); the
modal closes via the close button, backdrop click, or Escape.

@LarsLaskowski LarsLaskowski left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed against the project's review checklist (.claude/skills/review-pr).

Verification

  • go build ./..., go vet ./..., go test ./... all pass on the PR branch
  • golangci-lint run — 0 issues

Checklist results

  • REST API stability: ✅ No changes to any /api/v1/... response shape — the modal consumes the existing GET /api/v1/metrics/history endpoint. I verified the JS reads the exact JSON keys the server emits (cpu_percent, load1, temperature, memory_used_percent, and per-point t/v from HistoryPoint).
  • Range semantics: ✅ Filtering back from the most recent sample's timestamp (Pi clock) rather than the browser clock is the right call and consistent with how renderMetrics treats snap.timestamp. The 5m/15m/1h buttons also align with the default history_window_minutes: 60 retention.
  • Dependencies: ✅ None added; reuses the existing hand-rolled drawSparkline renderer, per the project's dependency philosophy.
  • Error handling/leaks/parsing/exec safety: N/A — no Go changes.
  • Test coverage: Frontend-only; there is no JS test harness in the repo, and the existing internal/web embed tests still pass. Headless-browser verification described in the PR is a reasonable substitute here.
  • Language: ✅ All new code and comments are in English.

Minor, non-blocking observations

  1. Focus management in the modal: opening doesn't move focus into the dialog and closing doesn't return focus to the invoking card, so keyboard users who open via Enter are left focused on the (now background) card. The existing updates modal has the same behavior, so this is consistent with the codebase pattern — fine to leave as a follow-up that fixes both modals together.
  2. role="button" on the whole <section>: children of an element with role="button" are treated as presentational by assistive tech, which flattens the card's <h2> and metric values into one long accessible name. A visually hidden "Show details" button inside the card (or role="button" on the heading only) would preserve the card semantics. Minor a11y nit, non-blocking.
  3. Single-sample edge: with exactly one point in the selected range, the stats line reports "1 sample" but the chart area is blank (drawSparkline needs ≥ 2 points). Trivial and self-resolving after the next poll.
  4. There are now two document-level Escape handlers (updates modal + detail modal), each closing its own modal. Harmless since only one is open at a time — just noting it in case a shared helper ever makes sense.

Verdict

Everything on the checklist checks out — this is a clean, well-scoped frontend change that reuses the existing modal and sparkline patterns as intended. Good to merge as-is; the observations above are optional follow-ups.

- Move role="button"/tabindex/aria-label from the card <section> onto its
  heading. With the role on the whole section, assistive tech treated the
  card's children as presentational and flattened the heading and metric
  values into one accessible name; exposing the heading as the single
  control keeps the card's text readable. The full card stays clickable via
  mouse, and the card highlights on :focus-within so keyboard focus on the
  heading is still visible.
- In the detail modal, a single sample in the selected range left the chart
  blank (drawSparkline needs >= 2 points) while the stats line still said
  "1 sample". Show a "collecting more samples to plot" hint instead so the
  empty chart is explained.

LarsLaskowski commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Thanks for the review! Addressed two of the observations in 6985b41:

  • A1: Persist metric history to disk (survive restarts) #2 (a11y): Moved role="button"/tabindex/aria-label from each card <section> onto its heading, so assistive tech now exposes a single named control instead of flattening the heading + metric values into one accessible name. The full card stays clickable via mouse, and the card highlights on :focus-within so keyboard focus on the heading remains visible. Verified in a headless browser: the accessibility tree reports one button node named e.g. "Open temperature detail".
  • A2: Threshold-based alert engine (state + events) #3 (single-sample edge): When only one point falls in the selected range, the stats line now shows "Now X · collecting more samples to plot…" instead of "1 sample" next to the (necessarily blank) chart.

Leaving #1 (focus management) and #4 (shared Escape handler) as a follow-up, since both touch the updates modal too and are better fixed for both modals together.

go build/vet/test and golangci-lint remain clean.

LarsLaskowski and others added 2 commits July 16, 2026 19:40
Follow-up to the review on both modals:

- Move focus into a dialog when it opens (its close button) and restore
  focus to the invoking element when it closes, so keyboard users who open
  the detail view from a card land in the dialog and return to that card
  afterwards. Tab is now trapped within the open dialog, as an aria-modal
  dialog should behave.
- Replace the two separate document-level Escape listeners (one per modal)
  with a single handler that dismisses whichever modal is open and also
  drives the Tab focus trap.

Applies to both the updates modal and the metric detail modal via shared
openModal/closeModal helpers.

LarsLaskowski commented Jul 16, 2026

Copy link
Copy Markdown
Owner Author

Also did the two deferred follow-ups (7cf5cb8), for both the updates modal and the detail modal via shared openModal/closeModal helpers:

Verified in a headless browser: focus lands on the close button on open, Tab/Shift+Tab wrap within the dialog, and Escape returns focus to the opening card (detail) / button (updates), with no console errors. go build/vet/test and golangci-lint clean. Rebased onto the latest branch head after the main merge.

@LarsLaskowski
LarsLaskowski merged commit 1d672db into main Jul 16, 2026
3 checks passed
@LarsLaskowski
LarsLaskowski deleted the claude/issue-9-g9mae9 branch July 16, 2026 17:47
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.

C1: Historical range picker / metric detail view

2 participants