Closes#100.
Each Site runs its own Poll Lane: an independent goroutine with its own rest
and pace from the registry (`backend/internal/latest/sites.go`), replacing the
shared cooldown/interval/stagger/batch configuration. Rest (1h, all six Sites
including the browser trio) is enforced by the due query's WHERE clause; the
Lane sleeps its effective gap between fetches — the registry 10s, or
rest/eligible when a Site holds enough Series, floored at 1s with a
Site-naming warning when the floor engages.
Lane-local failure handling:
- Two challenge-held results stop that Site's Lane for 15m; the probes keep
their stamp, untried Series stay due.
- A lost browser sets a shared Poller flag: the other browser Lanes skip
their passes for the same 15m (no stamp-per-pass-per-Lane on a dead tab),
then decay and probe again.
- Browser wake gate preserved (5 due, or one waiting 15m, ADR-0005); one tab
shared by the three browser Sites; "browser lane behind by X" logged every
pass.
- Cover work (healing a stored source URL and filling a blank from the series
page) runs in the background so a slow CDN cannot consume a Lane's gap.
Removed: `LATEST_CHAPTER_POLL_{COOLDOWN,BROWSER_COOLDOWN,INTERVAL,BATCH,STAGGER}`
and the 6h browser rest. Only `LATEST_CHAPTER_POLL_ENABLED` remains; DEPLOY.md
documents the exact `.env` edit. ADR-0010 records the decisions.
Reviewed-on: #106
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>
Closes#94.
## What
Two phases per the spec, in three feature commits plus two review-fix commits:
**Phase one — one registry entry per Site** (`2d134fb`)
The six per-site comparison points that used to live across three files collapse
into one `sites` map in `backend/internal/latest/sites.go`: Latest Chapter parse,
Cover parse, browser-backed list, fetcher route, host pins, and the browser
payload read all become lookups into it. `browserBackedSites()` is derived from
the registry (sorted, deterministic); `fetcherFor` and `fetchableSeriesURL` keep
their signatures and become lookups; `BrowserFetcher.Get` dispatches through the
entries' `Read`/`Done` while the tab lifecycle stays in `BrowserFetcher.run`.
**Phase two — one shared Series-page read** (`f215130`)
`readSeriesPage` (new `read.go`) performs the read the Poll and the Acquisition
have in common: gate, route, fetch, parse Latest Chapter, parse Cover address.
It returns facts only — polling and persistence policies (stamp order, cooldowns,
cover policy) stay with the callers; `acquire.go` gained the comment naming the
deliberate post-fetch stamp order. The poll's legacy cover heal and the
no-chapter byte-count diagnostic were restored after review (`d998f87`) so the
claims "the Poll keeps its own Cover policy" and "pinning is the only
behavioural change" both hold.
## Behaviour
- All six Sites now pin their host exactly; asura/demonic/comix previously
accepted any https host. For asura this is a strict improvement: its dead old
domain redirects deep links to the site root and would parse the wrong
document.
- Everything else is unchanged: existing parse tables, the challenge-body table
and the gate table pass unmodified except the one deliberate exception — the
gate table gains the three new pin cases.
## Security invariants preserved
- The address gate is recognisably the same rule, now a single registry lookup:
`https` + exact hostname match, all callers route through it. No fetch path
was widened; asura/demonic/comix were narrowed.
- The second host pin inside each browser entry's Read is retained deliberately
(browser = strong SSRF primitive, `series_url` is client-supplied) and is not
deduplicated against the shared gate.
- Review hardening: `fetcherFor` now fails closed for unknown site strings
(previously fell through to the TLS fetcher on an unreachable path), and the
browser dispatch iterates a sorted list so outcomes cannot depend on map order.
- The security review's log-injection finding was checked against Go's
`url.Parse` and does not hold: control characters are rejected anywhere in a
URL, so a client-supplied value in a log line cannot carry a newline.
## Review
Reviewed on three axes (spec, standards, security) by read-only subagents over
`672c16f..f1b26f4`. No blocking findings; all minor/nit findings addressed in
`d998f87` and `700de20`. Verified end to end with `go test ./...` (Docker
Postgres per test package) on every commit.
## Out of scope (tracked separately)
- Dropping asuracomic.net (CORS allowlist, userscript match, API fixtures,
live env) — separate issue, per spec.
Reviewed-on: #95
Co-authored-by: Sulthan Zaki <sultankiki05@gmail.com>
Co-committed-by: Sulthan Zaki <sultankiki05@gmail.com>