feat: live polling for new receipts#48
Merged
Merged
Conversation
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.
9a112c5 to
fc19da6
Compare
There was a problem hiding this comment.
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+sincequery param support on/api/receipts, plus new/api/configfor poll interval + server time. - CLI/server: add configurable poll interval via
-poll-intervalandAR_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.
- 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.
23761f7 to
0db29dc
Compare
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.
Summary
/api/receiptson a configurable interval and prepends fresh rows into the active view — new receipts appear without a manual refresh.document.visibilityState); resumes with an immediate catch-up poll on focus.Backend
Filter.Since(inclusivetimestamp >= ?) watermark, surfaced via thesincequery 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 byid, so re-emitting the watermark row is harmless.GET /api/configreturningpoll_interval_msso the frontend matches the server-side cadence.-poll-intervalflag andAR_DASHBOARD_POLL_INTERVALenv var (Gotime.Duration, default5s), 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
sincetimestamp watermark over the spec's?after=<id>because receipt IDs are UUIDs with no inherent ordering; inclusivetimestampfilter + client-side ID dedup gives the same no-duplicate guarantee without relying on sub-second precision. Existingafter/beforeinclusive-timestamp filters are unchanged.Frontend
pollerobject tracks active view, container, filter snapshot, watermark, and known IDs.pollOncecalls/api/receipts?since=<watermark>&[filters], dedupes by ID, andinsertAdjacentHTML('afterbegin', ...)into a stabletbody-{containerId}./api/receiptsresult, not the 10-row slice, so rows sharing the watermark second but falling belowRECENT_LIMITdon't re-emerge as "new" on the next poll.Test plan
go vet ./...andgo test ./... -count=1greenFilter.Sinceis 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/configfalls back toDefaultPollIntervalon zero-config; explicit value is echoedAR_DASHBOARD_POLL_INTERVALparsing: unset, valid (s/ms), unparseable, zero, and negativechoosePollIntervalprecedence: flag wins over env (incl. when env is broken), non-positive flag rejected, env used and surfaced when flag unsetOut of scope