refactor(web): share poll-state derivation; keep zebra parity across a row swap (#146)

Review round: pollState() unifies the CanPoll/Pending/Requested derivation
used by the list row and the detail page, and the row anchor carries its
band parity with the press (hx-vals) so the swapped answer keeps the
alternation.
This commit is contained in:
2026-08-22 01:02:55 +07:00
parent fe2cd12049
commit 5b7adf5f2c
4 changed files with 51 additions and 19 deletions
+32 -13
View File
@@ -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)