Merge #170: The completion hint reaches the owner, and decides nothing

# Conflicts:
#	backend/internal/web/admin_series_detail.go
This commit is contained in:
2026-08-23 00:53:29 +07:00
7 changed files with 363 additions and 81 deletions
+9 -9
View File
@@ -23,10 +23,10 @@ type overviewView struct {
// as stale + never_checked: a never-checked Series is already counted on
// its own filter, and the verdict wants the inclusive number.
Unchecked int
// Hygiene is the seven problem filters in the Series list's own render
// order plus the finished figure riding last (informational); Library is
// the library split plus the roster. Every figure is a door into the list
// that counts it, except a zero.
// Hygiene is the problem filters in the Series list's own render order
// plus the informational tail — finished, then site completed — riding
// last; Library is the library split plus the roster. Every figure is a
// door into the list that counts it, except a zero.
Hygiene []fig
Library []fig
// Sites is the per-Site library shape table, one row per Site with any
@@ -60,7 +60,7 @@ type siteRow struct {
}
// overviewView assembles the landing page from the store's read model: one
// SeriesShapes pass per filter summed in Go (the shipped surface offers nine
// SeriesShapes pass per filter summed in Go (the shipped surface offers ten
// grouped passes, not a stats query — #140), the pass log's latest pass per
// Site, and the roster. A failure in any read is a 500 with a logged reason,
// never a page of silent zeroes.
@@ -94,10 +94,10 @@ func (h *Handler) overviewView() (overviewView, error) {
view.Unchecked = totals[store.SeriesFilterStale] + totals[store.SeriesFilterNeverChecked]
view.Verdict, view.HasCounts = overviewVerdict(passes, now)
// The hygiene figures, in seriesFilterOrder's tail: the seven problem
// filters in permanent-then-fixable order, then the finished figure last —
// informational, not a problem, and last because seriesFilterOrder appends
// it there. The All filter's count belongs to the Library block, not to a
// The hygiene figures, in seriesFilterOrder's tail: the problem filters
// in permanent-then-fixable order, then the informational tail — finished
// and site completed — riding last because seriesFilterOrder appends them
// there. The All filter's count belongs to the Library block, not to a
// "hygiene" figure.
hygiene := make([]fig, 0, len(seriesFilterOrder)-1)
for _, name := range seriesFilterOrder[1:] {
+17 -14
View File
@@ -25,23 +25,25 @@ const seriesPageSize = 50
// the labels are read by later admin tickets too, so the map and the
// constants cannot drift apart.
var seriesFilterLabels = map[string]string{
store.SeriesFilterAll: "All series",
store.SeriesFilterNoURL: "No series URL",
store.SeriesFilterNoChapter: "Never read a chapter",
store.SeriesFilterNoReaders: "No Readers",
store.SeriesFilterNeverChecked: "Never checked",
store.SeriesFilterStale: "Not checked in 12h",
store.SeriesFilterNoCover: "No cover",
store.SeriesFilterReaderReport: "Latest from a Reader",
store.SeriesFilterFailing: "Failing over 12h",
store.SeriesFilterUnverified: "Unverified Reader number",
store.SeriesFilterFinished: "Finished",
store.SeriesFilterAll: "All series",
store.SeriesFilterNoURL: "No series URL",
store.SeriesFilterNoChapter: "Never read a chapter",
store.SeriesFilterNoReaders: "No Readers",
store.SeriesFilterNeverChecked: "Never checked",
store.SeriesFilterStale: "Not checked in 12h",
store.SeriesFilterNoCover: "No cover",
store.SeriesFilterReaderReport: "Latest from a Reader",
store.SeriesFilterFailing: "Failing over 12h",
store.SeriesFilterUnverified: "Unverified Reader number",
store.SeriesFilterFinished: "Finished",
store.SeriesFilterSiteCompleted: "Site says completed",
}
// seriesFilterOrder is the select's render order: All first, then the
// permanent repairs, then the fixable ones (issue #140). Finished rides the
// tail, last — deliberate, not a repair — and the Overview's stats block
// renders the same tail, which is what sits the finished figure last there.
// permanent repairs, then the fixable ones (issue #140). Finished and the
// site-completed hint ride the tail — deliberate, not repairs — and the
// Overview's stats block renders the same tail, which is what sits the
// finished and site-completed figures last there.
var seriesFilterOrder = []string{
store.SeriesFilterAll,
store.SeriesFilterNoURL,
@@ -54,6 +56,7 @@ var seriesFilterOrder = []string{
store.SeriesFilterFailing,
store.SeriesFilterUnverified,
store.SeriesFilterFinished,
store.SeriesFilterSiteCompleted,
}
// seriesListView is the Series list page's data. The template renders strings
@@ -69,6 +69,10 @@ type seriesDetailView struct {
// owner window: the value is unconfirmed and the owner should not trust
// it. It never names the Reader. "" otherwise.
Unverified string
// SiteCompleted is the hint's line, "" while the Site has said nothing or
// the owner has finished the Series: "the site says this work is
// completed (since 3d ago)". A hint, never a control (issue #170).
SiteCompleted string
}
// adminSeriesDetail renders one Series' page, keyed by the composite
@@ -164,6 +168,9 @@ func (h *Handler) seriesDetailView(a store.AdminSeries) seriesDetailView {
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."
}
if a.SiteCompletedAt != 0 && a.FinishedAt == 0 {
v.SiteCompleted = "the site says this work is completed (since " + checkedAge(time.Now(), a.SiteCompletedAt) + ")"
}
// Provenance: the actor class behind the current number, evaluated in the
// order the classes outrank one another — the owner's stamp, which a
@@ -47,6 +47,7 @@
{{else}}
<div class="dform">
<div class="field"><button type="button" class="ghost" onclick="document.getElementById('confirm-finish').hidden = false">Finish</button></div>
{{if .SiteCompleted}}<p class="hint">{{.SiteCompleted}}</p>{{end}}
<div class="confirm-row calm" id="confirm-finish" role="group" aria-live="polite" hidden>
<span>Mark this Series finished?</span>
<div>