Admin dashboard: pages, Lane observability, poll pass log, per-Series intervention (#134) #148

Merged
sulthan merged 22 commits from spec-134 into main 2026-08-22 08:27:20 +07:00

22 Commits

Author SHA1 Message Date
sulthan 264839e798 Merge ticket/147-lane-pause 2026-08-22 01:19:51 +07:00
sulthan cbe0a28921 feat(web,latest): pause and resume one Site's Lane with a mandatory expiry (#147)
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.
2026-08-22 01:15:57 +07:00
sulthan e45445cb20 Merge ticket/146-forced-poll 2026-08-22 01:05:09 +07:00
sulthan 4a95657425 Merge ticket/143-overview 2026-08-22 01:03:33 +07:00
sulthan 5b7adf5f2c refactor(web): share poll-state derivation; keep zebra parity across a row swap (#146)
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.
2026-08-22 01:02:55 +07:00
sulthan 00fa237151 fix(web): verdict space per design mock, single door() fig builder (#143) 2026-08-22 00:56:49 +07:00
sulthan fe2cd12049 feat(web,store,latest): forced poll — Check now stamps a Series for the next Lane pass (#146)
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.
2026-08-22 00:53:51 +07:00
sulthan c432a3be30 feat(web): overview page with verdict line and stats doors (#143) 2026-08-22 00:49:39 +07:00
sulthan 6f9109c885 Merge ticket/145-lanes-from-db 2026-08-22 00:35:34 +07:00
sulthan f5b52e48c8 Merge ticket/144-series-detail 2026-08-22 00:34:14 +07:00
sulthan bf08d6e65c Merge ticket/142-series-list 2026-08-21 20:42:42 +07:00
sulthan d72c48295d fix(web): statusline tokens, unreachable dot, reachability test spans (#145)
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.
2026-08-21 20:41:45 +07:00
sulthan fc8a40cfc6 fix(latest,web): finish the atomic deletion and honest empty states (#145)
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)
2026-08-21 20:31:50 +07:00
sulthan cb2b104e63 feat(web): read admin lanes from the durable pass log, not a poller snapshot (#145)
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)
2026-08-21 20:25:38 +07:00
sulthan 134c9307b1 #142: fix stale filter count cutoff, tighten comments (review fixes) 2026-08-21 19:48:24 +07:00
sulthan 09a094ca67 fix(web): detail row lookup walks all pages of the Site read (#144) 2026-08-21 19:42:45 +07:00
sulthan e8a3c5f826 #142: series list page with eight hygiene filters, site/library narrowing, paging 2026-08-21 19:40:45 +07:00
sulthan 14990bde21 feat(web): per-Series detail page keyed by site:series_id (#144) 2026-08-21 19:38:44 +07:00
sulthan 503fb49d0a feat(latest): record one poll pass per exit with skip reason and outcome counts
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)
2026-08-21 18:48:30 +07:00
sulthan e0b9063d9e feat(store): cross-series admin read model with privacy in the projection (#140)
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.
2026-08-21 18:20:59 +07:00
sulthan fd1131d11d feat(store): persist poll lane state 2026-08-21 17:22:23 +07:00
sulthan 030ffdc26e feat: split admin surface into bookmarkable pages (#138) 2026-08-21 17:01:46 +07:00