Spec #137: per-Series poll failure state, completion hint, and outbound owner notification #174

Merged
sulthan merged 30 commits from spec-137 into main 2026-08-23 11:31:11 +07:00
Owner

Implements spec #137 (spec 4 of 4 from wayfinder map #114).

Closes #137.

Tickets: #164, #165, #166, #167, #168, #169, #170, #171, #172, #173 — all closed, landed on this branch.

Summary

  • #164/#168: sixth outcome word not_found; per-Site completed marker predicate.
  • #165/#169: poll_failures row is the failure state; the pass remembers a Site-reported completion.
  • #166/#171: two new admin filters (failing, unverified); outbound owner notification + stall condition.
  • #167/#170: a failure names itself on the Series page; the completion hint reaches the owner and decides nothing.
  • #172: the other three fault conditions (no-browser-route, sidecar-down, adapter-broken) feeding the notifier.
  • #173: the landing verdict line shares the same latest.FaultsFrom judgement the notifier uses, so the page and the push cannot disagree.

cd backend && go test ./... green on the merged branch (8 packages).

Implements spec #137 (spec 4 of 4 from wayfinder map #114). Closes #137. Tickets: #164, #165, #166, #167, #168, #169, #170, #171, #172, #173 — all closed, landed on this branch. ## Summary - #164/#168: sixth outcome word `not_found`; per-Site completed marker predicate. - #165/#169: `poll_failures` row is the failure state; the pass remembers a Site-reported completion. - #166/#171: two new admin filters (`failing`, `unverified`); outbound owner notification + stall condition. - #167/#170: a failure names itself on the Series page; the completion hint reaches the owner and decides nothing. - #172: the other three fault conditions (no-browser-route, sidecar-down, adapter-broken) feeding the notifier. - #173: the landing verdict line shares the same `latest.FaultsFrom` judgement the notifier uses, so the page and the push cannot disagree. `cd backend && go test ./...` green on the merged branch (8 packages).
sulthan added 30 commits 2026-08-23 11:29:46 +07:00
site gains a Completed predicate answering whether a fetched body
carries the Site's own completed value: asura escaped props status,
demonic info-block Status pair, comix scoped detail entry (shared
comixDetailQuery helper with the cover read), kagane
publication_status only, novelfull status link, lightnovelworld
JSON-LD creativeWorkStatus. siteCompletedFrom dispatches it; an
absent hint stays false. Fixtures trimmed from live pages fetched
2026-08-22; TestSiteCompletedFrom covers completed, ongoing,
selector-removed, and challenge bodies per site, the kagane
upload/publication divergence, and an unknown site.
Spec review flagged the live-verification acceptance criterion as
unproven in the diff: the brief's adapter rule wants the probe
recorded, not just the date. Each fixture comment now names how the
live body was fetched on 2026-08-22 — curl probe for asura,
demonic and lightnovelworld; cleared Chrome tab (CDP sidecar) for
comix, kagane and novelfull, including the challenge/403 fallback
story for novelfull. Also renamed lnwCompletedRe to lnwStatusRe for
symmetry with the other site-marker vars.
Add series.site_completed_at (epoch-ms, default 0): the durable answer
#168's predicates compute. checkOne writes it after the successful read
on the zero/non-zero transition only, so a Series still completed keeps
its original stamp (the age #170 prints is 'since the Site first said
so'), one that stopped is zeroed, one that became completed is stamped.
The due query projects the column, so the transition is a field
comparison on the snapshot checkOne already holds.

Refused, unreachable and errored reads reach nothing (AC4 is placement,
not a guard); the write sits before the no-chapter and unchanged-number
returns, so a completed page whose chapter did not change still writes.
Store failure logs and carries on — the outcome word never changes.

Series-level like Latest Chapter: a bookmark's updated_at is never
touched. #170 is the surface; nothing reads the column yet.
# Conflicts:
#	backend/internal/latest/poller_test.go
Rollout note: the owner_notices table starts empty, so the first pass after deploy sends for conditions already true — correct per one-row-per-episode; say so rather than have it reported as a bug. Prod step: create the webhook, set DISCORD_WEBHOOK_URL on the deployment, redeploy — unset is silent by design, and without that step the feature ships dark. Security invariants preserved: the webhook address is a secret in the class of TOKEN_KEY (never logged, never on a config-printing line), and the owner gate is unchanged.
The Site's own completed marker (issue #168) joins the owner's surface as
an informational hint, never a decision:

- store: SeriesFilterSiteCompleted ('site_completed') sits beside Finished
  in the filter vocabulary; its predicate carries the same finished_at
  guard as the clock-driven filters, because the Site's stamp keeps
  standing after the owner retires the row. The projection and SeriesPage
  GROUP BY carry site_completed_at so the detail page can age it.
- web: the filter joins the Series list select as 'Site says completed'
  and rides the tail of seriesFilterOrder, so the landing stats figure
  comes for free and sits after the finished figure.
- detail: one hint line beside the Finish control - 'the site says this
  work is completed (since 3d ago)' - rendered only while the stamp
  stands and no Finish stands; the Finish control itself is untouched.
- tests: filter's finished exclusion and un-finish return to the Poll
  query (store); figure at zero and nonzero, hint present/absent, and the
  Finish control's markup byte-identical with a hint present (web).
The Series list row's fact line gains the failure word and its age in one
mark before the Notes chips, spelled through outcomeWord — the one
vocabulary shared with the Lanes chips. The detail page adds one sentence
beside the correction control while a Reader's number stands behind a
failure past the owner window, without naming the Reader. Each Lanes Site
name is a plain link to that Site's failing Series; the outcome chips stay
unlinked, and outcomeChips records why the promise is withdrawn.
# Conflicts:
#	backend/internal/web/admin_series_detail.go
FaultsFrom judges three more faults at the shared twelve-hour OwnerWindow:
a no-browser-route Site's refusing run (browser-backed Sites excluded), a
sidecar no Lane has reached (one site-wide fault), and a Site where more
than half of Series hold an old no-chapter failure row. recordPass fills
the inputs from three new store reads (RefusingSince, SidecarOK,
NoChapterShare), each failing independently and never failing a pass, and
the clear loop forgets the empty-Site row too so a lifted sidecar-down
fires again on return.
A fresh database whose browser Lanes skipped from day one has no
sidecar-reaching pass, so sidecarDownSince returned Since 0 and the notice
rendered the age since the epoch. Report the window itself: 'at least
twelve hours' is the honest floor the condition guarantees.
Only sidecar-down ever writes a Site=='' row, so the loop's empty-Site
clear for the other three conditions was a no-op delete on every pass.
Move it out of the per-condition loop, scoped to ConditionSidecarDown.
sulthan merged commit e93e79c1bb into main 2026-08-23 11:31:11 +07:00
Sign in to join this conversation.