From 134c9307b1d4911f4e820198b099b10ecff07291 Mon Sep 17 00:00:00 2001 From: Sulthan Zaki Date: Fri, 21 Aug 2026 19:48:24 +0700 Subject: [PATCH] #142: fix stale filter count cutoff, tighten comments (review fixes) --- backend/internal/web/admin.go | 3 +-- backend/internal/web/admin_series.go | 23 ++++++++++------------- backend/web_test.go | 12 +++++++++--- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/backend/internal/web/admin.go b/backend/internal/web/admin.go index b903801..08aa2eb 100644 --- a/backend/internal/web/admin.go +++ b/backend/internal/web/admin.go @@ -19,8 +19,7 @@ type LaneReporter interface { } // ownerWindow is the staleness boundary the Series list's "not checked in -// 12h" filter compares against, and the same twelve hours the landing page's -// figures share. Declared once; later admin tickets read it. +// 12h" filter compares against. Declared once; later admin tickets read it. const ownerWindow = 12 * time.Hour // adminView is the shared shell data for an administrative page and the roster diff --git a/backend/internal/web/admin_series.go b/backend/internal/web/admin_series.go index 96e88e2..6b98d35 100644 --- a/backend/internal/web/admin_series.go +++ b/backend/internal/web/admin_series.go @@ -160,7 +160,7 @@ func (h *Handler) seriesListView(r *http.Request) (seriesListView, error) { for i, a := range data.Rows { view.Rows = append(view.Rows, seriesRow(a, i, now)) } - view.Filters, err = h.seriesFilterOptions(filter) + view.Filters, err = h.seriesFilterOptions(filter, sf.Cutoff) if err != nil { return seriesListView{}, err } @@ -181,11 +181,12 @@ func (h *Handler) seriesListView(r *http.Request) (seriesListView, error) { // count, one SeriesShapes pass per filter summed in Go — the shipped surface // offers eight grouped passes, not a single stats query (#140). The counts // are library-wide because the select sits next to the Site narrowing and -// must not shift as the owner narrows the list itself. -func (h *Handler) seriesFilterOptions(selected string) ([]seriesFilterOption, error) { +// must not shift as the owner narrows the list itself. Cutoff travels with +// the stale filter, or its count would always be zero. +func (h *Handler) seriesFilterOptions(selected string, cutoff int64) ([]seriesFilterOption, error) { out := make([]seriesFilterOption, 0, len(seriesFilterOrder)) for _, name := range seriesFilterOrder { - shapes, err := h.store.SeriesShapes(store.SeriesFilter{Name: name}) + shapes, err := h.store.SeriesShapes(store.SeriesFilter{Name: name, Cutoff: cutoff}) if err != nil { return nil, err } @@ -203,9 +204,8 @@ func (h *Handler) seriesFilterOptions(selected string) ([]seriesFilterOption, er return out, nil } -// seriesRow formats one store row for the template. The note chips are the -// row's own hygiene facts, capped at two with a +N tail; attention marks a -// row that carries any. +// seriesRow shapes one store row for the template, capping its chips at two +// plus a +N tail; attention marks a row that carries any. func seriesRow(a store.AdminSeries, i int, now time.Time) seriesRowView { row := seriesRowView{ Key: a.Key(), @@ -273,13 +273,10 @@ func checkedAge(now time.Time, ts int64) string { } } -// pagerRange is the pager's "1–50 of 120" line. Zero rows reads as "0 of 0": -// the empty state carries the page then, and a window number for a list that -// rendered nothing would be a number nothing points at. +// pagerRange is the pager's "1–50 of 120" line. The template renders the +// pager only over rows (the empty state replaces it), so it is never asked +// to describe an empty list. func pagerRange(total, rows, page int) string { - if rows == 0 { - return "0 of 0" - } from := (page-1)*seriesPageSize + 1 return fmt.Sprintf("%d–%d of %d", from, from+rows-1, total) } diff --git a/backend/web_test.go b/backend/web_test.go index 61f7aa2..b8b2970 100644 --- a/backend/web_test.go +++ b/backend/web_test.go @@ -1939,15 +1939,16 @@ func TestSeriesListFilterWiring(t *testing.T) { t.Fatalf("open %s: %v", dsn, err) } defer db.Close() - seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:healthy", url: "https://asurascans.com/comics/healthy", cover: "aaa", checkedAt: 9000, latestNum: floatPtr(10), bookmarks: 1}) - seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:nocover", url: "https://asurascans.com/comics/nocover", checkedAt: 9000, latestNum: floatPtr(3), bookmarks: 1}) + seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:healthy", url: "https://asurascans.com/comics/healthy", cover: "aaa", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(10), bookmarks: 1}) + seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:nocover", url: "https://asurascans.com/comics/nocover", checkedAt: time.Now().UnixMilli(), latestNum: floatPtr(3), bookmarks: 1}) + seedSeriesRow(t, st, db, seriesRowSeed{key: "asura:stale", url: "https://asurascans.com/comics/stale", cover: "bbb", checkedAt: time.Now().Add(-24 * time.Hour).UnixMilli(), latestNum: floatPtr(4), bookmarks: 1}) srv := newRouter(st, testConfig(), nil) body := adminSeriesPage(t, srv, st, "?filter=no_cover") if !strings.Contains(body, "Title of asura:nocover") { t.Errorf("no_cover list misses its row:\n%s", body) } - if strings.Contains(body, "Title of asura:healthy") { + if strings.Contains(body, "Title of asura:healthy") || strings.Contains(body, "Title of asura:stale") { t.Errorf("no_cover list renders a covered row:\n%s", body) } if !strings.Contains(body, "1 series") || !strings.Contains(body, "No cover") { @@ -1959,6 +1960,11 @@ func TestSeriesListFilterWiring(t *testing.T) { if !strings.Contains(body, `