Two owner-gated POST routes write the durable poll_lanes pause stamp the
poller's top-of-pass gate already reads: /admin/lanes/{site}/pause validates
the duration against the fixed 1h/6h/24h allow-list and the Site against the
registry, and /admin/lanes/{site}/resume zeroes the stamp. Both cap the form
body like the API path, answer with the freshly rendered Lanes block, and
never command the poller — the pause is a fact about the Site, so it
survives a restart. The Lanes page's c-ctrl slot now carries the pausebar:
Resume while paused, the duration select plus Pause while running, with the
paused phrase read from the live poll_lanes stamp so a press renders as
paused with no pass having run. Tests cover the round trips, rejections,
body caps, the pause-before-refusal ordering, fresh-poller survival, resume
restoring the full queue, and acquisition being unaffected.
Review round: pollState() unifies the CanPoll/Pending/Requested derivation
used by the list row and the detail page, and the row anchor carries its
band parity with the press (hx-vals) so the swapped answer keeps the
alternation.
The control writes series.force_poll_at (column landed in migration 0014)
and never commands the poller: pending is derived as force_poll_at >
latest_checked_at and self-clears because the check stamp is written before
the fetch. The due query's forced flag overrides the rest cutoff, the
Sighting-deferral and finished-only clauses, jumps the queue, and wakes a
sleeping browser Lane; it never overrides an empty series_url, the Bookmarks
join, the refusal backoff, the sidecar-down skip or the Lane gap.
ADRs: 0013-commands-through-the-database.
Second review round (spec + standards):
- Polling-off and unreachable render as statusline tokens; the unreachable
span drops mark-strong so the patina dot never sits next to red text.
- Reachability tests assert on the rendered span, which 'reachable' and
'unreachable' substrings never could.
- A pass-log query failure now reads as reachable (no evidence rule) instead
of condemning the browser, and admin.go loses its dead time import.
- startLatestPoller's doc no longer claims the admin page reads the poller;
AGENTS.md carries the RefuseBackoff rename.
Review fixes on top of cb2b104:
- Delete the write-only in-memory refuseUntil map and setRefusalBackoff now
that status.go is gone: the pass gate reads the durable stamp, so the map
was half-deleted dead state (spec review C-1).
- An outcome-query failure no longer blanks the Lane table into the false
'no data yet': rows render with 'none observed' chips instead (I-1).
- due-query and eligible-count skips get their own sentences instead of the
merged 'check failed' (I-2).
- ADR-0012 constrain wording corrected and trailing newline added (M-1)
The Lanes page now projects store.LatestLanePasses and the owner-window
outcomes (store.LanePassOutcomes) into per-Site rows instead of reading an
in-memory Poller snapshot, so a deploy answers the instant the store is up.
Browser configuration is a config fact and reachability is derived from
recent browser-Site passes inside latest.RefuseBackoff.
This atomically deletes the in-memory path in the same commit that makes the
page read the DB: latest/status.go (LaneState, Status, LaneStatus,
recordLaneState) and the web.LaneReporter seam plus fakeLanes are gone, and
latest.refuseBackoff is renamed latest.RefuseBackoff at every callsite.
ownerWindow (#142) is referenced, never declared (contract C2)
Every way a Lane pass can end now writes exactly one durable row: a skip
value naming the exit (paused, refusing, sidecar-down, no-fetcher,
due-query, asleep, eligible-count, nothing-eligible, or empty for the
loop), five outcome counts from the classification the Series read already
makes, and carry-forward of the previous pass's figures exactly when the
pass's own gap is zero. Refusal is durable through the poll_lanes row, so
a restart does not re-probe a Site inside its backoff. Retention is 14
days. The mid-loop browser-unreachable return writes an empty skip by
design: a tenth value is not invented here. (#141)
SeriesPage returns one 50-row page of Series matching one of eight named hygiene filters (all, no_series_url, never_read_a_chapter, no_readers, never_checked, stale, no_cover, reader_report), with a window-count total; SeriesShapes returns the per-Site aggregate, one grouped pass. The compound filter value object takes Site, Kind, Name, a caller-supplied staleness cutoff, and a 1-based page.
The privacy boundary lives in the projection: adminSeriesColumns never selects latest_raised_by, and AdminSeries has no field for it — a SQL-computed boolean is all that crosses. LEFT JOIN surfaces orphans (reader_count 0); (site, series_id) tie-breaks the zero-stamp boundary so pages stay stable.
Also lands 0014: the series(latest_checked_at) index and series.force_poll_at, both expand-step schema for later tickets in the series.