diff --git a/CHANGELOG.md b/CHANGELOG.md index 877b64e..08440ee 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 `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 - Output status mismatch detection — flags receipts where the declared output hash does not match the computed value (#52) diff --git a/internal/server/server.go b/internal/server/server.go index 149f9e2..0907b03 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,19 @@ 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 + } + const maxLimit = 10000 + if n > maxLimit { + writeError(w, http.StatusBadRequest, fmt.Sprintf("limit must not exceed %d", maxLimit)) + return + } + f.Limit = &n + } rows, err := s.reader.ListReceipts(f) if err != nil { 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) diff --git a/internal/server/static/index.html b/internal/server/static/index.html index 7f99bbe..df2da32 100644 --- a/internal/server/static/index.html +++ b/internal/server/static/index.html @@ -806,7 +806,7 @@