feat(dashboard): windowed metric cards — backend + frontend#153
Conversation
Add optional time-window support to GET /metrics and the dashboard
metric grid.
Backend:
- GET /metrics accepts ?since and ?until (ISO-8601); returns counts
for that window instead of lifetime totals
- Both SQLite and Postgres backends: windowed path uses dynamic WHERE
clauses on events.time / sessions.started_at; fast path (no window)
unchanged, still uses pre-compiled prepared statements
- Server-wide process counters (received, rejected, duplicates) are not
time-indexed so they remain lifetime totals regardless of window
- max_tree_depth is omitted (returns 0) on the windowed path
- 400 on malformed since/until values
- Response includes windowed: { since, until } when a window is active
Frontend:
- Window-picker pill row (All time | 1h | 24h | 7d) above the metric
grid; active pill highlighted in accent colour
- Windowed DB-backed cards (Sessions, Accepted, Workflows) gain a
duration suffix in their labels; Received/Rejected show "(all time)"
to be honest that they can't be filtered
- setMetricWindow() + loadMetrics() extracted so the window can be
refreshed independently of the full loadAll() session fetch
Tests: 4 new integration tests (windowed response shape, both bounds,
invalid since → 400, future since → 0 ≤ lifetime)
Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
- Normalize since/until to ISO-8601 via new Date().toISOString() after the NaN check so non-standard but Date.parse-valid strings (e.g. "Jan 1 2026") produce consistent results across SQLite text-compare and Postgres timestamptz backends - Replace vacuous 4th windowed test (future-window <= lifetime, always true) with a positive-side assertion: verifies a past-10-min window captures the just-emitted event (accepted >= 1, session_count >= 1), then also checks the future-window exclusion - Add parentheses to windowed boolean expression in both backends for readability - Gate SSE nudge() calls on !metricWindow so the displayed count is not optimistically inflated when a time-window is active - Update OpenAPI /metrics entry with since/until query params, full property list, windowed response field, and 400 response Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
…on !metricWindow refreshSessions() and buildWorkflowList() pull from GET /sessions (lifetime totals) and overwrite the Sessions and Workflows metric cards on every SSE tick, clobbering the windowed counts placed by loadMetrics(). Guard both writes with !metricWindow so loadMetrics() remains the sole owner of those cards when a time window is active. Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
surpradhan
left a comment
There was a problem hiding this comment.
Round 1 findings (all fixed in 757f006): (1) Date.parse accepted non-ISO strings → normalized via new Date().toISOString(); (2) vacuous 4th test → replaced with positive-side assertion (accepted >= 1 in past window, = 0 in future window); (3) windowed boolean missing parens → added in both backends; (4) SSE nudge() not gated → wrapped in if (!metricWindow); (5) OpenAPI not updated → since/until params + windowed field + 400 response added.
Round 2 finding (fixed in ca0b16d): refreshSessions()/buildWorkflowList() were overwriting windowed metric cards with lifetime totals on every SSE tick → both writes gated on !metricWindow.
Round 3: both reviewers approve, zero actionable items. 243/243 tests, lint clean. Approve.
What does this PR do?
Adds optional time-window filtering to the metric cards at the top of the dashboard. A pill-row picker (All time / 1h / 24h / 7d) lets operators quickly scope the key counters to a recent window without leaving the page.
Changes
Backend (
GET /metrics)?sinceand?until(ISO-8601) query paramsevents.time/sessions.started_at; the existing fast-path (pre-compiled prepared statements) is unchanged when no window is setreceived,rejected,duplicates) have no timestamps and remain lifetime totals regardless of windowwindowed: { since, until }in the response when a window is activeFrontend
How to test
```bash
npm run ingest
open dashboard, click 1h / 24h / 7d pills — metric cards update instantly
curl -H "Authorization: Bearer " \
"http://localhost:8787/metrics?since=$(date -u -v-1H +%Y-%m-%dT%H:%M:%SZ)"
→ { accepted: N, session_count: N, ..., windowed: { since: "...", until: null } }
curl -H "Authorization: Bearer " "http://localhost:8787/metrics?since=not-a-date"
→ 400 { error: "invalid since/until..." }
```
Checklist
Closes: part of UX polish sprint (PR-C of 4)