Skip to content

feat: live polling for new receipts#48

Merged
ojongerius merged 4 commits into
mainfrom
claude/realtime-dashboard-polling-c37OL
May 18, 2026
Merged

feat: live polling for new receipts#48
ojongerius merged 4 commits into
mainfrom
claude/realtime-dashboard-polling-c37OL

Conversation

@ojongerius
Copy link
Copy Markdown
Contributor

@ojongerius ojongerius commented May 18, 2026

Summary

  • Tails /api/receipts on a configurable interval and prepends fresh rows into the active view — new receipts appear without a manual refresh.
  • Pauses polling when the tab is hidden (document.visibilityState); resumes with an immediate catch-up poll on focus.
  • Exponential backoff after 3 consecutive empty polls, capped at 30s; resets on activity or refocus.
  • Subtle row flash + "N new receipts" toast on arrival.

Backend

  • Filter.Since (inclusive timestamp >= ?) watermark, surfaced via the since query param on /api/receipts. Inclusive on purpose: receipt timestamps may lack sub-second precision, so a strict > watermark would silently lose any row sharing the second with the watermark. The client deduplicates by id, so re-emitting the watermark row is harmless.
  • New GET /api/config returning poll_interval_ms so the frontend matches the server-side cadence.
  • -poll-interval flag and AR_DASHBOARD_POLL_INTERVAL env var (Go time.Duration, default 5s), validated as positive. Precedence is flag > env > default — a malformed env var no longer aborts startup when the flag is set explicitly.

I picked a since timestamp watermark over the spec's ?after=<id> because receipt IDs are UUIDs with no inherent ordering; inclusive timestamp filter + client-side ID dedup gives the same no-duplicate guarantee without relying on sub-second precision. Existing after/before inclusive-timestamp filters are unchanged.

Frontend

  • Single poller object tracks active view, container, filter snapshot, watermark, and known IDs.
  • pollOnce calls /api/receipts?since=<watermark>&[filters], dedupes by ID, and insertAdjacentHTML('afterbegin', ...) into a stable tbody-{containerId}.
  • Overview keeps the recent list bounded at 10 rows (both incremental and empty-state rebuild paths); switching views or changing filters resets polling state cleanly.
  • Overview dedup state is seeded from the full /api/receipts result, not the 10-row slice, so rows sharing the watermark second but falling below RECENT_LIMIT don't re-emerge as "new" on the next poll.

Test plan

  • go vet ./... and go test ./... -count=1 green
  • Filter.Since is inclusive — rows sharing the watermark second are re-emitted and the client dedups by id
  • /api/receipts?since=... returns rows at or after the timestamp
  • /api/config falls back to DefaultPollInterval on zero-config; explicit value is echoed
  • AR_DASHBOARD_POLL_INTERVAL parsing: unset, valid (s/ms), unparseable, zero, and negative
  • choosePollInterval precedence: flag wins over env (incl. when env is broken), non-positive flag rejected, env used and surfaced when flag unset
  • Manual browser check of row flash, toast, visibility pause, and backoff (couldn't render UI from this environment)

Out of scope

  • WebSocket/SSE push (noted in the original proposal as follow-on)
  • Persisting poll interval preference across sessions

The dashboard now tails /api/receipts on a configurable interval and
prepends fresh rows into the active view, so new receipts appear without
a manual refresh. Polling pauses when the tab is hidden and backs off
exponentially while the stream is quiet.

- Add Filter.Since (exclusive timestamp watermark) and surface it via
  the `since` query param so each poll returns only strictly newer rows
- Add /api/config exposing poll_interval_ms so the frontend matches the
  server-side cadence
- Wire -poll-interval flag + AR_DASHBOARD_POLL_INTERVAL env (default 5s)
Self-review surfaced four real issues with the polling change:

- Stale in-flight polls could prepend rows for the previous filter onto a
  freshly-rendered table when the user changed filters mid-request. Add a
  generation counter that loadOverview/loadReceipts/startPolling bump and
  pollOnce captures around its await; mismatched responses are dropped.
- `Filter.Since` was strict (`>`), so receipts sharing a second with the
  boundary could be silently lost when timestamps lack sub-second
  precision. Switch to inclusive (`>=`); the client already dedups by id.
- The empty-store watermark fell back to the client wall clock, so clock
  skew could mask new receipts indefinitely. /api/config now returns the
  server time and the frontend uses it as the baseline.
- The overview's stats cards and risk/status charts went stale on live
  updates. Refetch /api/stats whenever fresh rows are prepended on the
  overview view.
@ojongerius ojongerius force-pushed the claude/realtime-dashboard-polling-c37OL branch from 9a112c5 to fc19da6 Compare May 18, 2026 06:49
@ojongerius ojongerius requested a review from Copilot May 18, 2026 06:50
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds “live receipts” support by introducing a timestamp watermark (since) on /api/receipts, a new /api/config endpoint for frontend polling cadence, and a frontend poller that periodically fetches and prepends new rows (with visibility pause/backoff and a toast).

Changes:

  • Backend: add Filter.Since + since query param support on /api/receipts, plus new /api/config for poll interval + server time.
  • CLI/server: add configurable poll interval via -poll-interval and AR_DASHBOARD_POLL_INTERVAL, plumbed into server config.
  • Frontend: implement polling lifecycle (view-aware), row flash animation, and “N new receipts” toast.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
README.md Documents -poll-interval / env var and advertises live updates feature.
internal/store/reader.go Adds Filter.Since and applies it to receipt listing SQL.
internal/store/reader_test.go Adds coverage for Since filtering behavior.
internal/server/static/index.html Implements live poller, backoff, visibility pause, row flash, and toast UI.
internal/server/server.go Adds /api/config and wires since query param into store filter.
internal/server/server_test.go Adds tests for /api/config and /api/receipts?since=....
cmd/dashboard/main.go Adds poll interval env/flag handling and passes it into server config.
cmd/dashboard/main_test.go Adds tests for poll interval env parsing/validation.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread internal/server/static/index.html Outdated
Comment thread internal/server/static/index.html Outdated
Comment thread cmd/dashboard/main.go Outdated
Comment thread internal/store/reader.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread internal/server/static/index.html Outdated
Comment thread internal/server/static/index.html Outdated
Comment thread internal/store/reader.go
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 8 out of 8 changed files in this pull request and generated 3 comments.

Comment thread internal/store/reader.go
Comment thread internal/server/static/index.html
Comment thread internal/store/reader.go
- main: poll-interval precedence is now flag > env > default. A malformed
  AR_DASHBOARD_POLL_INTERVAL no longer aborts startup when -poll-interval
  was passed. Extracted choosePollInterval for direct test coverage.
- loadOverview: seed poller state from the full /api/receipts result
  rather than the RECENT_LIMIT slice, so receipts sharing the watermark
  second but falling below the display cap aren't surfaced as "new" on
  the next poll.
- prependFreshRows: the empty-state rebuild path now caps the overview
  at RECENT_LIMIT visible rows while still recording every fresh id in
  knownIds, mirroring the incremental path's bounding.
- reader: ORDER BY timestamp ASC, id ASC when Since is set. With DESC +
  LIMIT a burst larger than the cap silently lost the middle of the
  range — the client would bump the watermark past unread rows. ASC
  drains correctly across successive polls (client advances watermark
  to newest returned row; next poll picks up the rest). The id
  tie-break makes ordering deterministic across same-timestamp ties.
- index.html: escape `truncate(r.chain_id, 20)` before insertion.
  chain_id comes from the SQLite store; a malicious SDK/proxy could
  poison it with markup. Project policy requires validation at SQLite
  trust boundaries.
- index.html: stop deleting evicted ids from `knownIds` on overview
  trim. With an inclusive `since` watermark, removing a
  boundary-timestamp id would cause the next poll to re-flash and
  re-toast that row. knownIds growth is bounded by polled traffic
  (≤ store limit per request), which is acceptable for an interactive
  monitoring session.
@ojongerius ojongerius force-pushed the claude/realtime-dashboard-polling-c37OL branch from 23761f7 to 0db29dc Compare May 18, 2026 08:09
@ojongerius ojongerius self-assigned this May 18, 2026
@ojongerius ojongerius merged commit f3fb1a2 into main May 18, 2026
4 checks passed
@ojongerius ojongerius deleted the claude/realtime-dashboard-polling-c37OL branch May 18, 2026 08:11
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.

2 participants