Forced Poll: ask for one Series to be checked now #146

Closed
opened 2026-08-21 16:39:24 +07:00 by sulthan · 1 comment
Owner

Parent

Spec #134.

What to build

If one Series' Latest Chapter looks stuck, the owner currently waits for its turn in the Lane's hour. This ticket gives them Check now -- a control that writes a row the poller notices on its next pass, and never commands the poller. It survives a restart because it is a fact about the Series rather than about the running process, and the whole surface stays testable with no poller running at all.

One column already exists from the read-model ticket: a request stamp in unix ms, zero meaning never asked. Writing it again re-stamps the request time; the write is idempotent. Pending is derived, never stored: the request stamp is newer than the check stamp. It clears itself with no second write and no sweeper, because the check stamp is written before the fetch -- so the first attempt ends the pending state whatever the attempt returns. That stamp-before-fetch order is load-bearing; changing it silently makes forced requests sticky. No expiry: a request the Lane never reaches keeps ageing in the UI rather than vanishing, so an old pending marker is itself the evidence that a Lane is stuck. The control tells the owner how long ago they asked and gives no estimate, because the page does not know when a sleeping browser will wake.

The due query gains the flag in three places and forces the row to the front of the queue. A Forced Poll overrides the rest cutoff, the Sighting-deferral clause and the finished-only bucket. It never overrides an empty series URL (nothing to fetch), the Bookmarks join (a Series no Reader holds has no consumer for the result), the Lane's refusal backoff (hand-forcing a request at a Site that is actively refusing is the one move that makes it worse), the sidecar-down skip, or the Lane's gap. One pass-level gate it does open: the browser wake thresholds. A forced Series wakes a sleeping Chrome -- those thresholds exist to stop the machine waking itself for one unattended check, and a human asking is not that. If the home machine is off, nothing happens and the request ages visibly.

Two notes for whoever implements the query. The finished-status clause in the current due query is today's Lifecycle test and is deleted by a later spec in this series; implement it as it stands, that spec replaces it rather than amending it. And rejected: zeroing the check stamp as the force signal -- it corrupts the never-checked and stale counts the landing page exists to show, and makes a pending marker impossible.

The control is unconfirmed -- it takes nothing away -- and answers with the freshly rendered row, so the figures describe the state after the press. The ageing pending marker renders once, on the title line flush right above the actions; under the action it added a second line to every row. The control is hidden on a Series with no page to fetch and on one no Reader holds, so the owner is never offered a button that can never do anything. It appears on both the Series list row and the Series detail page.

Write the ADR for the through-the-database command seam and its queue-jump rules with this implementation.

Acceptance criteria

  • Pressing Check now on a list row and on the detail page both answer with freshly rendered markup showing the pending marker
  • The pending marker ages and never expires; a second press re-stamps the request time
  • The control is absent on a Series with no series URL and on one no Reader holds
  • A forced Series is fetched ahead of the rest cutoff, ahead of the Sighting deferral and ahead of the finished bucket
  • A forced Series is not fetched through a refusal backoff, an empty series URL or the Bookmarks join
  • A forced Series wakes a sleeping browser Lane that the wake thresholds would otherwise leave asleep
  • The pending flag self-clears on the check stamp with no second write, including when the attempt fails
  • The check stamp is still written before the fetch, with a test that fails if the order is swapped
  • Form bodies on the action route are capped the way the API path caps them, and the route joins the route list rather than gating inside itself
  • An ADR for the through-the-database command seam lands with the change
  • go test ./... green

Blocked by

  • #141 — The poller records one pass row per exit, with a skip reason and outcome counts
  • #140 — Cross-Series admin read model, with the privacy boundary in the projection
  • #142 — Series list page: eight hygiene filters, Site and Library narrowing, paging
  • #144 — Per-Series detail page, keyed by the composite the rest of the system uses
## Parent Spec #134. ## What to build If one Series' Latest Chapter looks stuck, the owner currently waits for its turn in the Lane's hour. This ticket gives them *Check now* -- a control that writes a row the poller notices on its next pass, and **never commands the poller**. It survives a restart because it is a fact about the Series rather than about the running process, and the whole surface stays testable with no poller running at all. One column already exists from the read-model ticket: a request stamp in unix ms, zero meaning never asked. Writing it again re-stamps the request time; the write is idempotent. **Pending is derived, never stored**: the request stamp is newer than the check stamp. It clears itself with no second write and no sweeper, because **the check stamp is written before the fetch** -- so the first attempt ends the pending state whatever the attempt returns. That stamp-before-fetch order is **load-bearing**; changing it silently makes forced requests sticky. **No expiry**: a request the Lane never reaches keeps ageing in the UI rather than vanishing, so an old pending marker is itself the evidence that a Lane is stuck. The control tells the owner how long ago they asked and **gives no estimate**, because the page does not know when a sleeping browser will wake. The due query gains the flag in three places and forces the row to the front of the queue. **A Forced Poll overrides** the rest cutoff, the Sighting-deferral clause and the finished-only bucket. **It never overrides** an empty series URL (nothing to fetch), the Bookmarks join (a Series no Reader holds has no consumer for the result), the Lane's refusal backoff (hand-forcing a request at a Site that is actively refusing is the one move that makes it worse), the sidecar-down skip, or the Lane's gap. One pass-level gate it *does* open: the browser wake thresholds. A forced Series wakes a sleeping Chrome -- those thresholds exist to stop the machine waking itself for one unattended check, and a human asking is not that. If the home machine is off, nothing happens and the request ages visibly. Two notes for whoever implements the query. The finished-status clause in the current due query is today's Lifecycle test and is **deleted** by a later spec in this series; implement it as it stands, that spec replaces it rather than amending it. And rejected: zeroing the check stamp as the force signal -- it corrupts the never-checked and stale counts the landing page exists to show, and makes a pending marker impossible. The control is **unconfirmed** -- it takes nothing away -- and answers with the freshly rendered row, so the figures describe the state after the press. The ageing pending marker renders **once**, on the title line flush right above the actions; under the action it added a second line to every row. The control is **hidden** on a Series with no page to fetch and on one no Reader holds, so the owner is never offered a button that can never do anything. It appears on both the Series list row and the Series detail page. Write the ADR for the through-the-database command seam and its queue-jump rules with this implementation. ## Acceptance criteria - [ ] Pressing *Check now* on a list row and on the detail page both answer with freshly rendered markup showing the pending marker - [ ] The pending marker ages and never expires; a second press re-stamps the request time - [ ] The control is absent on a Series with no series URL and on one no Reader holds - [ ] A forced Series is fetched ahead of the rest cutoff, ahead of the Sighting deferral and ahead of the finished bucket - [ ] A forced Series is **not** fetched through a refusal backoff, an empty series URL or the Bookmarks join - [ ] A forced Series wakes a sleeping browser Lane that the wake thresholds would otherwise leave asleep - [ ] The pending flag self-clears on the check stamp with no second write, including when the attempt fails - [ ] The check stamp is still written before the fetch, with a test that fails if the order is swapped - [ ] Form bodies on the action route are capped the way the API path caps them, and the route joins the route list rather than gating inside itself - [ ] An ADR for the through-the-database command seam lands with the change - [ ] `go test ./...` green ## Blocked by - #141 — The poller records one pass row per exit, with a skip reason and outcome counts - #140 — Cross-Series admin read model, with the privacy boundary in the projection - #142 — Series list page: eight hygiene filters, Site and Library narrowing, paging - #144 — Per-Series detail page, keyed by the composite the rest of the system uses
sulthan added the ready-for-agent label 2026-08-21 16:39:24 +07:00
sulthan self-assigned this 2026-08-22 00:36:49 +07:00
Author
Owner

Merged into spec-134 (branch ticket/146-forced-poll, commits fe2cd12, 5b7adf5).

Check now lands on both the Series list row and the detail page, writing series.force_poll_at through POST /admin/series/{key}/poll and never commanding the poller. Pending is derived (force_poll_at > latest_checked_at), self-clears on the check stamp with no second write — including when the attempt fails — and never expires. The due query gained the flag in its three places plus the queue jump, and a forced Series opens the browser wake thresholds while still respecting the empty-URL filter, the Bookmarks join and the refusal backoff. ADR-0013 lands with it. No migration: 0014 already added the column.

The stamp-before-fetch ordering has its own test that fails if the order is swapped, as the acceptance criteria require.

Security invariants preserved and stated: parameterised SQL only, 64 KiB MaxBytesReader before ParseForm, owner gate in the route list rather than inside the handler, html/template output only, generic client errors with detail logged.

Review: both axes clean, one Minor fixed per axis.

Known shortcut, ponytail:-commented in-tree: two SeriesPage scans per press rather than a keyed read, with the upgrade path named.

go test ./... green on the merged base.

Merged into `spec-134` (branch `ticket/146-forced-poll`, commits fe2cd12, 5b7adf5). *Check now* lands on both the Series list row and the detail page, writing `series.force_poll_at` through `POST /admin/series/{key}/poll` and never commanding the poller. Pending is derived (`force_poll_at > latest_checked_at`), self-clears on the check stamp with no second write — including when the attempt fails — and never expires. The due query gained the flag in its three places plus the queue jump, and a forced Series opens the browser wake thresholds while still respecting the empty-URL filter, the Bookmarks join and the refusal backoff. ADR-0013 lands with it. No migration: 0014 already added the column. The stamp-before-fetch ordering has its own test that fails if the order is swapped, as the acceptance criteria require. Security invariants preserved and stated: parameterised SQL only, 64 KiB `MaxBytesReader` before `ParseForm`, owner gate in the route list rather than inside the handler, `html/template` output only, generic client errors with detail logged. Review: both axes clean, one Minor fixed per axis. Known shortcut, `ponytail:`-commented in-tree: two `SeriesPage` scans per press rather than a keyed read, with the upgrade path named. `go test ./...` green on the merged base.
Sign in to join this conversation.
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: sulthan/mangaBookmark#146