From fd45ba474dde7406dbe5885b3cb37afbb47381db Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 20 May 2026 16:28:28 +1200 Subject: [PATCH 1/4] fix: cap overview recent-receipts fetch to 20 rows (#58) The overview loadOverview() call was fetching /api/receipts with no limit, pulling up to 10k rows to display only a handful. Add ?limit=20 to that specific fetch; the full receipts view is unchanged. Closes #58 Co-Authored-By: Claude Sonnet 4.6 (1M context) --- CHANGELOG.md | 4 ++++ internal/server/static/index.html | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 877b64e..5b9cca6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Fixed + +- Overview "Recent receipts" now fetches only 20 rows from the server instead of the full store (up to 10,000 after #55), reducing unnecessary bandwidth and memory usage (#58) + ### Added - Output status mismatch detection — flags receipts where the declared output hash does not match the computed value (#52) diff --git a/internal/server/static/index.html b/internal/server/static/index.html index 7f99bbe..db85dea 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -806,7 +806,7 @@ renderStats(stats); renderHeaderContext(null, stats); - const receipts = await fetchJSON('/api/receipts'); + const receipts = await fetchJSON('/api/receipts?limit=20'); if (gen !== poller.generation) return; renderReceiptsTable(receipts.slice(0, RECENT_LIMIT), 'recent-receipts'); From 7aa0a554e6d24497f15ce14f7857f0fc8562a68b Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 20 May 2026 16:46:49 +1200 Subject: [PATCH 2/4] fix: wire limit query param in handleReceipts; derive fetch limit from RECENT_LIMIT --- internal/server/server.go | 9 +++++++++ internal/server/static/index.html | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/internal/server/server.go b/internal/server/server.go index 149f9e2..0e0c808 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -11,6 +11,7 @@ import ( "log" "net/http" "path/filepath" + "strconv" "strings" "time" @@ -130,6 +131,14 @@ func (s *Server) handleReceipts(w http.ResponseWriter, r *http.Request) { if v := q.Get("since"); v != "" { f.Since = &v } + if v := q.Get("limit"); v != "" { + n, err := strconv.Atoi(v) + if err != nil || n < 1 { + writeError(w, http.StatusBadRequest, "limit must be a positive integer") + return + } + f.Limit = &n + } rows, err := s.reader.ListReceipts(f) if err != nil { diff --git a/internal/server/static/index.html b/internal/server/static/index.html index db85dea..df2da32 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -806,7 +806,7 @@ renderStats(stats); renderHeaderContext(null, stats); - const receipts = await fetchJSON('/api/receipts?limit=20'); + const receipts = await fetchJSON(`/api/receipts?limit=${RECENT_LIMIT}`); if (gen !== poller.generation) return; renderReceiptsTable(receipts.slice(0, RECENT_LIMIT), 'recent-receipts'); From c5be0daf34b2e481fe0656e68d31f56920b4c261 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 20 May 2026 16:56:28 +1200 Subject: [PATCH 3/4] fix: cap limit param at 10000; fix CHANGELOG row count --- CHANGELOG.md | 2 +- internal/server/server.go | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5b9cca6..08440ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,7 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- Overview "Recent receipts" now fetches only 20 rows from the server instead of the full store (up to 10,000 after #55), reducing unnecessary bandwidth and memory usage (#58) +- Overview "Recent receipts" now fetches only `RECENT_LIMIT` (10) rows from the server instead of the full store (up to 10,000 after #55), reducing unnecessary bandwidth and memory usage. The `/api/receipts` endpoint now honours a `?limit=N` query parameter (capped at 10,000) (#58) ### Added diff --git a/internal/server/server.go b/internal/server/server.go index 0e0c808..dcd970e 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -137,6 +137,11 @@ func (s *Server) handleReceipts(w http.ResponseWriter, r *http.Request) { writeError(w, http.StatusBadRequest, "limit must be a positive integer") return } + const maxLimit = 10000 + if n > maxLimit { + writeError(w, http.StatusBadRequest, "limit must not exceed 10000") + return + } f.Limit = &n } From f6b4cff9a7ef9657c390f57e152865cf47715897 Mon Sep 17 00:00:00 2001 From: Otto Jongerius Date: Wed, 20 May 2026 17:10:11 +1200 Subject: [PATCH 4/4] fix: use maxLimit constant in error message; add limit param tests --- internal/server/server.go | 2 +- internal/server/server_test.go | 28 ++++++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/internal/server/server.go b/internal/server/server.go index dcd970e..0907b03 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -139,7 +139,7 @@ func (s *Server) handleReceipts(w http.ResponseWriter, r *http.Request) { } const maxLimit = 10000 if n > maxLimit { - writeError(w, http.StatusBadRequest, "limit must not exceed 10000") + writeError(w, http.StatusBadRequest, fmt.Sprintf("limit must not exceed %d", maxLimit)) return } f.Limit = &n diff --git a/internal/server/server_test.go b/internal/server/server_test.go index 7b40890..f113818 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -152,6 +152,34 @@ func TestReceiptsEndpoint_FilterByRisk(t *testing.T) { } } +func TestReceiptsEndpoint_Limit(t *testing.T) { + srv := setupServer(t) + + // Valid limit caps the result set. + req := httptest.NewRequest("GET", "/api/receipts?limit=1", nil) + w := httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + if w.Code != http.StatusOK { + t.Errorf("limit=1: got status %d, want 200", w.Code) + } + var rows []store.ReceiptRow + if err := json.Unmarshal(w.Body.Bytes(), &rows); err != nil { + t.Fatalf("decode: %v", err) + } + if len(rows) != 1 { + t.Errorf("limit=1: got %d rows, want 1", len(rows)) + } + + for _, bad := range []string{"0", "-1", "abc", "10001"} { + req = httptest.NewRequest("GET", "/api/receipts?limit="+bad, nil) + w = httptest.NewRecorder() + srv.Handler().ServeHTTP(w, req) + if w.Code != http.StatusBadRequest { + t.Errorf("limit=%s: got status %d, want 400", bad, w.Code) + } + } +} + func TestReceiptsEndpoint_FilterBySince(t *testing.T) { srv := setupServer(t) req := httptest.NewRequest("GET", "/api/receipts?since=2026-04-01T10:01:00Z", nil)