Skip to content

feat(dashboard): windowed metric cards — backend + frontend#153

Merged
surpradhan merged 3 commits into
mainfrom
feat/dashboard-windowed-metrics
Jun 26, 2026
Merged

feat(dashboard): windowed metric cards — backend + frontend#153
surpradhan merged 3 commits into
mainfrom
feat/dashboard-windowed-metrics

Conversation

@surpradhan

Copy link
Copy Markdown
Owner

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)

  • Accepts optional ?since and ?until (ISO-8601) query params
  • Both SQLite and Postgres backends: windowed path uses dynamic WHERE clauses on events.time / sessions.started_at; the existing fast-path (pre-compiled prepared statements) is unchanged when no window is set
  • Server-wide process counters (received, rejected, duplicates) have no timestamps and remain lifetime totals regardless of window
  • Returns windowed: { since, until } in the response when a window is active
  • 400 on malformed date strings

Frontend

  • Window-picker pill row above the metric grid (All time / 1h / 24h / 7d)
  • DB-backed cards (Sessions, Accepted, Workflows) gain a duration suffix when a window is active (e.g. "Sessions (24h)")
  • Received / Rejected cards show `(all time)` to be transparent that they can't be filtered
  • `setMetricWindow()` + standalone `loadMetrics()` so the window refreshes independently of the full `loadAll()` session/timeline fetch

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

  • Tests pass (243/243, +4 new integration tests)
  • Lint clean
  • No new CI jobs
  • No schema migrations

Closes: part of UX polish sprint (PR-C of 4)

surpradhan and others added 3 commits June 26, 2026 11:34
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 surpradhan 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.

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.

@surpradhan surpradhan merged commit 2ff349c into main Jun 26, 2026
14 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