Data-correction actions: Latest Chapter and series_url repair #121
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: #115, #117
Question
Can the owner correct wrong Series data by hand, and under what constraints?
This is the intervention with real teeth: issue #79 has lightnovelworld reporting a chapter-list
maximum of 1317 against the Site's own newest-chapter indicators of 1298, and every Reader of that
Series sees the wrong number. Candidates from charting: correct a wrong Latest Chapter; repair or
re-discover a bad
series_url; delete an orphaned Series row.Hard constraints: any outbound fetch of a client-supplied URL must pass
fetchableSeriesURLfirst, so a repaired
series_urlis exactly the input that gate exists for. Series identity isdiscovered, not derived (ADR-0008).
To decide:
overwriting it — a sticky flag, a floor, or nothing at all.
the only thing that fixes #79, and it is also the one place a human types into a row every
Reader reads.
(
ClearSightingAttribution,RecordSightingOutcomeexist).bookmarksFK toserieshas no cascade.
decision.
From #119 (closed): a Forced Poll re-judges any outstanding Sighting for free —
checkOnealready callsjudgeSighting, and force overrides the deferral clause — so Check now is itself the manual remedy for a Latest Chapter a Reader raised wrongly, and it can mark that Reader. Whether the correction UI advertises it that way, or offers repair as a separate control, is this ticket's call. The action route shape (POST /admin/series/{key}/<verb>, answer with the swapped row fragment) is settled there too.sulthan referenced this issue2026-08-18 06:59:54 +07:00
Note from #118 (closed):
The class this ticket repairs is invisible to every filter, by proof rather than by omission.
#118 rejected a "Latest Chapter went backwards" filter: nothing stores the previous value, and a
downward write is the correction rather than the fault (
poller_test.go:481-497seeds 400 andasserts 296 after a poll). More to the point, the case that actually costs something — #79's
adapter reading a stable wrong 1317 every hour — never moves, so no movement-based filter could
ever fire on it.
So hand repair carries that whole class alone, and the only automated hint the owner gets is
sighting-raised(latest_sighted_at > latest_checked_at): the stored value came from a Reader'sreport rather than a Poll. Worth deciding here whether the correction UI is reachable from that
filter, since it is the closest thing to a worklist this class will ever have.
Resolved. Both verbs exist, neither is authoritative, and the premise this ticket was charted on
does not survive a read of #79.
The premise was wrong
#79 is not a defect. Its own body settles the opposite — "Latest Chapter is the highest
chapter number the Site lists for a Series", therefore "the current behaviour is correct …
No code changes" — and its Do not "fix" this later section pre-rejects capping the maximum at
the top list entry. 1317 is right; a Reader at 1298 genuinely has 19 unread chapters and the ember
is earned. So there is no known case of a wrong stored Latest Chapter on the tracker, and the map's
"fixes the class of #79" is struck.
What survives is one class no machine can reach: a Reader-raised value (
latest_raised_bynon-NULL) on a Series whose page the Poll cannot read. #119's Forced Poll is the remedy
whenever the page is readable —
checkOnecallsjudgeSightingand rewrites from the Site — soa correction on a readable Series is not a correction, it is a workaround for an adapter bug.
That framing is the whole shape of what follows.
Two overwriters, not one
The ticket assumed the Poll. There are two:
checkOne(poller.go:474-485) rewrites on any inequality with what the Site publishes.ON CONFLICT DO UPDATE SET latest_chapter=excluded.latest_chapter, latest_chapter_num=excluded.latest_chapter_num(store.go:864-867) — unconditionallast-write-wins from any Reader's PUT, and
reportLatestChapter(manga-bookmark.user.js:917-942, novel-bookmark.user.js:823) fires on every series-page visit,
sends the site-read value up or down, and sends it even unchanged (that is the Sighting).
So "sticky" was never one guard: it is a pin column plus a guard on the poller path and on the
client write path every Reader hits.
Decisions
1. A hand-set Latest Chapter is not authoritative. No pin, no floor. The correction reuses the
existing value columns and dies the moment any machine write lands. Rationale: the Poll is the
oracle (
judgeSighting's premise), a page read is stronger evidence than a typed number, and beingoverwritten by one is the correct outcome. On the class this control is for, nothing overwrites it
anyway — that is what "the Poll cannot read the page" means. A floor is rejected outright: it would
have made #79's downward direction — the one that was actually right — unrepairable.
2. One numeric input; the label is derived. The owner types a number only. Handler requires a
finite float
> 0, else400. The stored label is written as"Chapter " + strconv.FormatFloat(num, 'f', -1, 64)→Chapter 1298,Chapter 1298.5. Two inputs(label + number) are rejected: nothing depends on matching a Site's typography, and a free-text
label invents a failure mode where the number is right and the panel renders the typo
(
manga-bookmark.user.js:1377renderslatest_chapter,:1373-1375compareslatest_chapter_num). Leaving the old label is worse still — the panel would keep sayingChapter 1317after the number moved.3.
series_urlrepair is owner-typed and gated. New store methodSetSeriesURL(site, seriesID, url string) error; the handler validates with the existing gatebefore storing.
fetchableSeriesURL(poller.go:555) is unexported, so it is renamed tolatest.FetchableSeriesURLwith its in-package callers updated — one implementation, never asecond gate;
internal/web/admin.goalready importslatest. No synchronous fetch in the request:the owner presses Check now (#119) afterwards. This lifts the write-once rule that
migration 0002 states ("client-supplied values are ignored once the row exists") for the owner
only.
Rejected: letting a Reader's PUT heal a stale
series_url(the client already sends a fresh oneevery time and the store drops it).
seriesis a shared row — one Reader could repoint aSeries every other Reader reads at a different work on the same host, and
FetchableSeriesURLwould pass it, because the gate stops SSRF, not mis-pointing.
Honest limit, stated on the page rather than papered over: a Site-wide host change (asuracomic →
asurascans) invalidates every row of that Site at once, and a per-Series form is the wrong tool for
it. That is a migration, and it is now a fog line on the map, not a promise of this control.
4.
latest_corrected_atmeans "the current value is a human's", and is transient.New column, migration 0015 (after #116's 0012, #119's 0013, #117's 0014):
Written by the correction, zeroed by every machine write of the value:
CorrectLatestChapter(site, seriesID string, num float64, ts int64) error— setslatest_chapter,latest_chapter_num,latest_corrected_at = ts.SetLatestChaptergains, latest_corrected_at = 0in the same UPDATE. Already conditional ineffect:
checkOnereturns early on equality, so the Poll only writes when the number moved.DO UPDATE, conditional on the valueactually changing:
latest_corrected_at = CASE WHEN excluded.latest_chapter_num IS DISTINCT FROM series.latest_chapter_num THEN 0 ELSE latest_corrected_at END.Unconditional zeroing would be wrong for the common case: after a correction, a Reader's cached
row holds the corrected number, so an ordinary progress PUT resends it and would clear the stamp
while the value is still the owner's. One clause in an existing statement — no extra round trip,
no second UPDATE — but it does touch the security-reviewed Upsert, so say so in that PR.
This is deliberately not an audit trail: no history, no
corrected_by(there is one owner),and a stamp cannot outlive the value it describes. The map's provenance patch still owns the audit
question, which now spans three unrecorded writers — a correction, #120's replaced Cover, and
SetLatestChapterlowering a number unconditionally.The
series_urlrepair records nothing. It changes what the Poll fetches, not what a Reader reads.5. A contradicting correction clears the Sighting attribution without judging it. After a
successful correction, if
latest_raised_byis non-NULL callClearSightingAttribution(site, seriesID, *raisedBy)— neverRecordSightingOutcome(..., false).Marking the Reader is tempting (this is the one class where a mark would otherwise never land) but
recovery is 20 confirming Polls (
SightingAgreementsToClear), and this control exists preciselybecause no Poll can read the page — so a mark earned here is permanent in practice and an owner's
typo would be unappealable. Marks stay machine-earned. Leaving the attribution in place is wrong
outright: the next Poll would credit or blame that Reader for the owner's number.
6. No predicate gate; the page words the stopgap. Both controls sit on
GET /admin/series/{key}unconditionally, with copy saying the next successful Poll overwrites thevalue. Gating on "looks unverifiable" would make this ticket wait on #127 for cosmetics, and both
gate variants forbid the repair exactly when it gets easy — a briefly-healed Site does not make a
wrong stored number right.
Routes, in #115's shape (
POST /admin/series/{key}/<verb>, answering with the swapped fragment —here the detail fragment, since neither control is on the list row):
POST /admin/series/{key}/latestnumPOST /admin/series/{key}/series-urlurllatest.FetchableSeriesURL, else 400Both join
adminRoutes()behindrequireOwner; form bodies capped withhttp.MaxBytesReaderasthe API path does. No
.confirm-row— Cinder gates what pulls a Series out of the list, andneither verb does. Never
--ember, no--danger.7. Corrections are silent to Readers. No notification, no announcement: the corrected label
simply is the Latest Chapter, and the ember follows
latest_chapter_numagainst that Reader's ownlast_chapter_num(manga-bookmark.user.js:1373-1375), so a downward correction can onlyextinguish an ember and an upward one lights it — which is what "the Site published" means. With
#120's Cover half, the map's are admin corrections visible to Readers patch is fully decided and
comes off Not yet specified.
8. Orphan-Series deletion leaves this ticket. #125 owns it outright, and #120 already routed
Cover-byte reclamation there. Struck from the candidate list here; nothing about it decided.
Consequences for the map
typing a number is not evidence the page became readable; only a successful Poll is. (Answers one
of #127's open questions in the negative, from this side.)
SeriesRowfield, list row unchanged. The worklist into this control is?filter=sighting-raised, which #118 already calls the only suspicion lens this class will get.and a
corrected <age> agomarker whilelatest_corrected_at > 0.Invariants worth a test
CorrectLatestChapterstamps;SetLatestChapterzeroes.FetchableSeriesURLis rejected400and does not reach the store.latest_raised_byand leaves bothreaders.sighting_*counters untouched.