diff --git a/backend/internal/web/admin_lanes.go b/backend/internal/web/admin_lanes.go index c70b972..d6fcb85 100644 --- a/backend/internal/web/admin_lanes.go +++ b/backend/internal/web/admin_lanes.go @@ -56,6 +56,10 @@ type laneRow struct { // offer Resume from the moment the owner presses Pause, with no pass // having run to record it (issue #147). Paused bool + // FailingHref is the one navigation the row offers: the Site's name + // links to that Site's failing Series. The chips beside it stay + // unlinked; the withdrawn promise lives on outcomeChips (issue #167). + FailingHref string } // chip is one named outcome count over the owner's window. @@ -223,6 +227,7 @@ func buildLaneRow(p store.LanePass, o store.SiteOutcomes, now time.Time) laneRow if p.GapMS > 0 { row.Gap = (time.Duration(p.GapMS) * time.Millisecond).Truncate(time.Second).String() } + row.FailingHref = seriesListHref(store.SeriesFilterFailing, p.Site, "", 0) row.Chips = outcomeChips(o) row.HasChips = len(row.Chips) > 0 row.StatePhrase, row.StateGood, row.Attention = laneState(p, now) @@ -232,18 +237,24 @@ func buildLaneRow(p store.LanePass, o store.SiteOutcomes, now time.Time) laneRow // outcomeChips lists a Site's nonzero window sums in the taxonomy's fixed // order, so the chips never reorder as the window changes. None observed is -// written by the template, not drawn as a confident zero count. +// written by the template, not drawn as a confident zero count. The names are +// spelled through outcomeWord, the one vocabulary the failure surface shares +// with the Series list's fact line (issue #167). The counts stay unlinked +// permanently — a withdrawn promise: two of the six words write no per-Series +// state, and the other four count attempts inside the owner window while the +// failing filter lists Series failing now, so neither set contains the other +// and no chip can be a door to its list. func outcomeChips(o store.SiteOutcomes) []chip { fixed := []struct { name string count int }{ - {"refused", o.Refused}, - {"unreachable", o.Unreachable}, - {"no chapter", o.NoChapter}, - {"unfetchable", o.Unfetchable}, - {"not found", o.NotFound}, - {"errors", o.Errors}, + {outcomeWord("refused"), o.Refused}, + {outcomeWord("unreachable"), o.Unreachable}, + {outcomeWord("no_chapter"), o.NoChapter}, + {outcomeWord("unfetchable"), o.Unfetchable}, + {outcomeWord("not_found"), o.NotFound}, + {outcomeWord("errors"), o.Errors}, } var out []chip for _, f := range fixed { diff --git a/backend/internal/web/admin_series.go b/backend/internal/web/admin_series.go index 32ab04d..2f2ae2e 100644 --- a/backend/internal/web/admin_series.go +++ b/backend/internal/web/admin_series.go @@ -123,6 +123,11 @@ type seriesRowView struct { // page, where a press that retires a Series from the Lane is on purpose // and confirm-gated (issue #158). Finished bool + // Failure names the standing failure and how long it has stood — "not + // found · 3d ago", "" while no failure row stands. Its own field, never a + // Notes chip: the chips cap at two plus a tail, so the one fact that + // names the failure would be the most likely to be truncated away. + Failure string } // adminSeries renders the filterable, bookmarkable Series list: filter, Site, @@ -683,6 +688,9 @@ func seriesRow(a store.AdminSeries, i int, now time.Time) seriesRowView { row.Ch = "—" } row.Age = checkedAge(now, a.LatestCheckedAt) + if a.FailureOutcome != "" { + row.Failure = outcomeWord(a.FailureOutcome) + " · " + checkedAge(now, a.FailingSince) + } notes := seriesNotes(a, now) if n := len(notes); n > 2 { row.Notes, row.More = notes[:2], n-2 @@ -693,6 +701,20 @@ func seriesRow(a store.AdminSeries, i int, now time.Time) seriesRowView { return row } +// outcomeWord spells the wire failure word as the Lanes chips spell it — a +// space, not the underscore: not_found reads "not found", no_chapter "no +// chapter". The remaining words are their own spelling, so an unknown word +// degrades to itself rather than vanishing from the page. +func outcomeWord(wire string) string { + switch wire { + case "not_found": + return "not found" + case "no_chapter": + return "no chapter" + } + return wire +} + // seriesNotes are a row's hygiene chips in the design's order: no URL, no // cover, orphan, stale, reader sighting. func seriesNotes(a store.AdminSeries, now time.Time) []string { diff --git a/backend/internal/web/admin_series_detail.go b/backend/internal/web/admin_series_detail.go index 9d0f3d1..08f6bde 100644 --- a/backend/internal/web/admin_series_detail.go +++ b/backend/internal/web/admin_series_detail.go @@ -64,6 +64,11 @@ type seriesDetailView struct { // stands. It rides the meta fragment both presses swap, so the answer // itself shows how long the Series has been finished. FinishedSince string + // Unverified is the sentence beside the Latest Chapter correction + // control while a Reader's number stands behind a failure past the + // owner window: the value is unconfirmed and the owner should not trust + // it. It never names the Reader. "" otherwise. + Unverified string } // adminSeriesDetail renders one Series' page, keyed by the composite @@ -151,6 +156,14 @@ func (h *Handler) seriesDetailView(a store.AdminSeries) seriesDetailView { v.Corrected = correctedAge(time.Now(), a.LatestCorrectedAt) v.Finished = a.FinishedAt != 0 v.FinishedSince = finishedAge(time.Now(), a.FinishedAt) + // Unverified: a Reader's number standing behind a failure that has outlived + // the owner window is a number nobody has re-checked since — say so next to + // the correction control, without naming the Reader. A machine read or a + // failure still inside the window carries no sentence; the failure itself + // has not yet outlived the twelve hours' worth of trust. + if a.RaisedByReader && a.FailureOutcome != "" && a.FailingSince < time.Now().Add(-ownerWindow).UnixMilli() { + v.Unverified = "Unverified Reader number: the page has been failing for over 12h, so this Reader-reported chapter is unconfirmed." + } // Provenance: the actor class behind the current number, evaluated in the // order the classes outrank one another — the owner's stamp, which a diff --git a/backend/internal/web/templates/lanes.html b/backend/internal/web/templates/lanes.html index c4a359e..0bf79ac 100644 --- a/backend/internal/web/templates/lanes.html +++ b/backend/internal/web/templates/lanes.html @@ -26,7 +26,7 @@ {{range .Rows}}
{{.Unverified}}
{{end}}