diff --git a/backend/internal/web/admin_series.go b/backend/internal/web/admin_series.go
index 4730863..e9099ad 100644
--- a/backend/internal/web/admin_series.go
+++ b/backend/internal/web/admin_series.go
@@ -149,7 +149,8 @@ func (h *Handler) adminSeriesPoll(w http.ResponseWriter, r *http.Request) {
// Re-read after the stamp: the answer must describe the state after the
// press. The detail page's control swaps its meta in place and the list
// row's swaps the row; htmx names an id target in HX-Target, so the
- // response matches the surface it came from.
+ // response matches the surface it came from. The row's band parity travels
+ // with the press (hx-vals), so the swap keeps the zebra alternation.
a, found, err := h.adminSeriesByKey(site, seriesID)
if err != nil {
log.Printf("series poll %s: %v", site+":"+seriesID, err)
@@ -164,7 +165,11 @@ func (h *Handler) adminSeriesPoll(w http.ResponseWriter, r *http.Request) {
h.render(w, http.StatusOK, "series-detail-meta", h.seriesDetailView(a))
return
}
- h.render(w, http.StatusOK, "series-row", seriesRow(a, 0, time.Now()))
+ band := 0
+ if r.PostFormValue("band") == "1" {
+ band = 1
+ }
+ h.render(w, http.StatusOK, "series-row", seriesRow(a, band, time.Now()))
}
// seriesListView assembles one Series list view from the request's query
@@ -267,18 +272,32 @@ func (h *Handler) seriesFilterOptions(selected string, cutoff int64) ([]seriesFi
// 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(),
- Title: a.Title,
- Site: a.Site,
- Readers: a.ReaderCount,
- Band: i%2 == 1,
- CanPoll: a.SeriesURL != "" && a.ReaderCount > 0,
- }
+// pollState derives the Check now control and the pending marker (issue
+// #146), shared by the list row and the detail page: CanPoll is false on a
+// Series with no page to fetch and on an orphan, so the owner is never
+// offered a button that can never do anything. Pending is derived — the
+// request stamp is newer than the check stamp — and requested is its ageing
+// label, which never expires.
+func pollState(a store.AdminSeries, now time.Time) (canPoll, pending bool, requested string) {
+ canPoll = a.SeriesURL != "" && a.ReaderCount > 0
if a.ForcePollAt > a.LatestCheckedAt {
- row.Pending = true
- row.Requested = requestedAge(now, a.ForcePollAt)
+ pending = true
+ requested = requestedAge(now, a.ForcePollAt)
+ }
+ return canPoll, pending, requested
+}
+
+func seriesRow(a store.AdminSeries, i int, now time.Time) seriesRowView {
+ canPoll, pending, requested := pollState(a, now)
+ row := seriesRowView{
+ Key: a.Key(),
+ Site: a.Site,
+ Title: a.Title,
+ Readers: a.ReaderCount,
+ Band: i%2 == 1,
+ CanPoll: canPoll,
+ Pending: pending,
+ Requested: requested,
}
if a.LatestChapterNum != nil {
row.Ch = strconv.FormatFloat(*a.LatestChapterNum, 'f', -1, 64)
diff --git a/backend/internal/web/admin_series_detail.go b/backend/internal/web/admin_series_detail.go
index c856926..74810aa 100644
--- a/backend/internal/web/admin_series_detail.go
+++ b/backend/internal/web/admin_series_detail.go
@@ -96,6 +96,7 @@ func (h *Handler) adminSeriesByKey(site, seriesID string) (store.AdminSeries, bo
// seriesDetailView shapes one AdminSeries row for display: every judgement in
// Go, the template left to print strings and flags.
func (h *Handler) seriesDetailView(a store.AdminSeries) seriesDetailView {
+ canPoll, pending, requested := pollState(a, time.Now())
v := seriesDetailView{
Key: a.Key(),
Site: a.Site,
@@ -107,11 +108,9 @@ func (h *Handler) seriesDetailView(a store.AdminSeries) seriesDetailView {
NoCover: a.CoverAddress == "",
Orphan: a.ReaderCount == 0,
SightingRaised: a.RaisedByReader,
- CanPoll: a.SeriesURL != "" && a.ReaderCount > 0,
- }
- if a.ForcePollAt > a.LatestCheckedAt {
- v.Pending = true
- v.Requested = requestedAge(time.Now(), a.ForcePollAt)
+ CanPoll: canPoll,
+ Pending: pending,
+ Requested: requested,
}
if a.LatestChapterNum == nil {
v.Chapter = "—"
diff --git a/backend/internal/web/templates/series-list.html b/backend/internal/web/templates/series-list.html
index 48eeb94..c2cce35 100644
--- a/backend/internal/web/templates/series-list.html
+++ b/backend/internal/web/templates/series-list.html
@@ -48,6 +48,6 @@
{{.Age}}
{{.Readers}}
{{range .Notes}}{{.}}{{end}}{{if .More}}+{{.More}}{{end}}
- {{if .CanPoll}}Check now{{end}}
+ {{if .CanPoll}}Check now{{end}}
{{end}}
diff --git a/backend/web_test.go b/backend/web_test.go
index a515d19..5e43da4 100644
--- a/backend/web_test.go
+++ b/backend/web_test.go
@@ -2645,6 +2645,20 @@ func TestSeriesPollRoundTrip(t *testing.T) {
t.Errorf("poll response lacks the pending marker:\n%s", body)
}
+ // The press answers with the row's own band parity (hx-vals), so the swap
+ // keeps the zebra alternation.
+ req = httptest.NewRequest(http.MethodPost, "/admin/series/asura:x/poll", strings.NewReader("band=1"))
+ req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
+ req.AddCookie(sessionCookie(t, st))
+ rr = httptest.NewRecorder()
+ router.ServeHTTP(rr, req)
+ if rr.Code != http.StatusOK {
+ t.Fatalf("POST poll (banded) status = %d, want 200", rr.Code)
+ }
+ if !strings.Contains(rr.Body.String(), `class="trow`) || !strings.Contains(rr.Body.String(), ` band"`) {
+ t.Errorf("banded press answer lost the zebra parity:\n%s", rr.Body.String())
+ }
+
// Detail surface: the meta fragment carries the same marker.
req = httptest.NewRequest(http.MethodPost, "/admin/series/asura:x/poll", nil)
req.AddCookie(sessionCookie(t, st))