Hygiene filters for the Series view #118
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #114
Blocked by: #116, #117
Question
Which "broken Series" classes can the Series view filter to?
Candidates raised while charting: no cover stored (
cover_address = ''); never successfullypolled (
latest_checked_at = 0, orlatest_chapter_num IS NULLmeaning never captured); lastsuccess older than N days;
series_urlthat failsfetchableSeriesURL; Latest Chapter that wentbackwards; a Series no Reader is still reading.
Deliberately not pre-decided: each filter is a real query, and at least one — Latest Chapter
regressing — is only detectable if poll history exists, which is why this waits on that decision.
To decide: the final filter list, each one's exact predicate against the schema, which are
navigable as URLs, and which of them feed the summary line's problem count.
Resolution
The filter vocabulary closes at eight names, one candidate is rejected outright, and the
presentation rules are fixed: every count is its own entry point, a zero is a figure and not a
door, and nothing links to a list that cannot answer for it.
The eighth filter: no-chapter
Added here, on top of the seven #116 fixed:
?filter=no-chapters.latest_chapter_num IS NULL AND s.latest_checked_at > 0 AND s.series_url <> ''A Series the poller has attempted and has never once read a chapter number from. Free: no
column, no migration, no write on the poll path. It is queryable today because
MarkLatestCheckedstampslatest_checked_atbefore the fetch (poller.go:428) while a failedread never reaches
SetLatestChapter—poller_test.go:345-348asserts exactly that pair(timestamp stamped,
latest_chapter_numnil) across all four failure modes.Named to match #117's
no_chapterpass count deliberately: same observation, one made durable onthe row, the other counted over a 12h window. It does not make #127 redundant — this catches a
Series that never worked, #127 catches one that worked for a year and broke on Tuesday. The two
coexist, and neither is a superset.
Disjoint from
uncheckedby construction (latest_checked_at = 0vs> 0), so a Series is in atmost one of them and the two counts never double-report the same row.
Rejected: a "Latest Chapter went backwards" filter
Not implementable and not wanted, in that order.
Not implementable: the row holds only the current number.
SetLatestChapter(store.go:1063)writes downward unconditionally — poller.go:474 skips only the equal case — and
Upsert(store.go:858-869) lets a Reader's PUT lower it too. Detection would need the fact captured at
write time in a new column; #117 already rejected per-Series check history on cost.
Not wanted: a downward write is the correction, not the fault.
poller_test.go:481-497seeds 400 and asserts 296 after a poll — lowering is tested, intended behaviour. Given #79
(lightnovelworld's chapter list maxing at 1317 while the site's own newest indicator says 1298),
an inflated number falsely lights the ember for every Reader of that Series; the write that brings
it back to 1298 is the system healing, and a regression filter would flag precisely the Series
that just got fixed.
And it misses the case that actually costs something: an adapter reading a stable wrong 1317
every hour never moves, so nothing ever fires. That is #79's real shape — a confidently wrong
number — and no filter can see it.
sighting-raisedremains the only honest suspicion lens (thestored value came from a Reader's report, not a Poll), and hand repair is #121.
Labels
Query-string names are the API and stay exactly as #116 fixed them; these are display strings
only. Ordered permanent-and-fixable first, informational last — the order of the stats block rows
and of the filter row:
?filter=unpollableno-chapterorphanuncheckedstaleno-coversighting-raisedunpollableandorphanread as the repair they need rather than as the SQL they are — the ownerarrives at this block to act, not to admire a predicate.
Every figure is an entry point, including the non-hygiene ones
#115 made the hygiene counts links. Extended here to the rest of the block, so it doubles as the
navigation for the whole surface and no number is a fact you then have to go hunting for:
/admin/series/admin/series?kind=manga,?kind=novel/admin/series?site=<site>/admin/readers/admin/series?filter=<name>/admin/series?site=<site>&filter=<name>A zero renders the digit and is not a link. The figure stays — a measured zero is a real fact,
and hiding it would repeat the mistake
lanes.htmlavoids — but no anchor, because following itlands on an empty list, which is navigation that costs a click and returns nothing.
The filtered list
SeriesFilter.Filterholds one value;?site=and?kind=stack ontop and are separate controls, so narrowing to a Site never silently drops the filter.
.attentionclass, plus anAll series reset. Never
--ember(New Chapter only), never--danger(destruction).<N> series · <label>, N being #116'sCOUNT(*) OVER ()total, so the figure thelanding page promised and the figure the list shows come from one query.
"nothing here". An empty hygiene list is good news and should read as an answer.
No aggregate problem count
The summary line stays exactly as #117 left it:
<verdict> · N series waiting · M unchecked over 12h. No third "N problems" figure, because the classes overlap — one orphaned Series with noseries_urland no cover is three counts and one row — so a sum over-reports while aDISTINCTcount is a number nothing can be done about. The stats block is the whole hygiene surface, and
every figure on it is individually actionable.
The per-Site outcome counts link nowhere yet
#117 pushed this here. The five sums (
refused/unreachable/no_chapter/unfetchable/errors) render as named text with the none observed zero wording, unlinked, because thepass row stores counts and never identities — there is no list of the four refused Series to point
at, and "every Series on this Site" is a haystack posing as an answer.
After #127 lands, all five link to
/admin/series?site=<site>&filter=failing.Considered and dropped: wiring
no_chapterup immediately, since this ticket gives it a filter.The sets differ — the count means attempts that read no chapter in the last 12h, the filter means
never once succeeded, and a Series that broke this morning is in one and not the other.
no-chapterstays reachable from the stats block, where it is exact.Boundary with #122
This ticket fixes the list's frame: filter row, heading, empty state, and what links where. Which
SeriesRowfields render on a row, in what order, at what density — plus #119's Check nowcontrol — is the prototype's call, not settled by prose here.
Pushed onto other tickets
no-chapterjoins the filter enum as an eighth predicate constant, andSeriesStatsgains the matching
COUNT(*) FILTER (WHERE latest_chapter_num IS NULL AND latest_checked_at > 0 AND series_url <> '')column so the landing block can show it. No new SQL shape, one moreFILTERin the pass that already exists.failingis the ninth name, not a replacement forno-chapter; its threshold musttherefore only cover Series that have succeeded at least once. It also inherits the link target
for all five outcome counts.
<N> series · <label>heading, anda filter-specific empty state; the stats block is all links except the zeroes.
filter, so hand repair carries that whole class alone.
Amended by #122 (closed): the filter control is a
<select>whose option labels carry their counts (No cover (3)), not a row of eight plain links — eight hygiene labels do not read as chips. Site is a second select, kind a three-way segmented row. One filter at a time, URL-addressable; heading, empty state and the linked-figures-except-zero rule unchanged.Amendment from #124 (closed) — the vocabulary you closed at eight takes a tenth name:
finished→s.finished_at > 0, labelled Finished, ordered last in the stats block as informational rather than fixable, with its ownSeriesStatscount. Your zero-renders-the-digit-unlinked rule and the one-filter-at-a-time<select>from #122 apply unchanged.failingstays #127's ninth.AND s.finished_at = 0:stale,unchecked,no-cover,no-chapter. This is what keeps your "an empty hygiene list is good news" rule true — a finished Series is never checked again, so it would otherwise sit instalepermanently and the count would never return to zero.unpollable,orphanandsighting-raisedare deliberately not guarded: a finished Series with noseries_url, no Readers, or a Latest Chapter that came from a Sighting is still a row worth repairing.Amendments from #127 (closed):
failingis the ninth name, as you reserved it, and it is disjoint from yourno-chapterby construction: its predicate carriesAND s.latest_chapter_num IS NOT NULL, so a Series that never once succeeded is inno-chapterand never infailing. Full test: a row exists in the newpoll_failurestable (migration 0017)AND s.latest_chapter_num IS NOT NULL AND f.failing_sinceolder thanownerWindow(12h).unverifiedis the eleventh name (finishedtook tenth in #124), ordered besidesighting-raisedas a suspicion lens:s.latest_raised_by IS NOT NULLplus thefailingtest — a Latest Chapter that came from a Reader's Sighting and that no Poll has confirmed for over 12 hours. #121's correction control is the action it leads to.<select>carries both new options with counts, per its amendment to this ticket; one filter at a time and?site=/?kind=stacking are unchanged.?filter=failing" line:refusedandunreachablenow write no per-Series state, and the other four count attempts in a 12h window rather than Series failing now. You had already applied this test once by refusing to link theno_chaptercount to theno-chapterfilter — #127 applies it to the remaining five. The Lanes page gets one separate per-Site navigation link instead.