Owner-only admin page: Reader roster plus Poll Lane status (#102) #107

Merged
sulthan merged 7 commits from feat/102-admin-page into main 2026-08-16 15:02:15 +07:00
Owner

Closes #102.

The only operational surface was /healthz and a fold-out roster inside the owner's own reading page. This adds /admin: an owner-only page carrying the Reader roster and one row per Poll Lane.

  • Poller seam. latest.Poller records each Lane's last pass (Site, Due, Checked, LastRun, Gap, Clamped, Browser) and answers LaneStatus(); the page reads that snapshot, never a table. A pass that returns before computing its figures (refusal backoff, sidecar down) carries the previous pass's figures forward rather than recording zeroes, and a Lane that has never reached a pace renders no gap at all. Refusal and sidecar reachability are derived at snapshot time.
  • Owner gate at registration. Every route reaching past the acting Reader lives in adminRoutes() and is wrapped in requireOwner when it is registered, so a missing gate is visible in the route list rather than hidden in a handler. web.AdminPatterns() is what the gate test walks, so a new route cannot be added without being tested. A non-owner gets 404, never 403.
  • Nil poller is a first-class state. main.newRouter takes the reporter as an interface and converts a nil *Poller to a nil interface; no poller and no completed pass both render "No data yet" with the reason spelled out, rather than confident zeroes.
  • Roster moved off the reading page onto /admin, with the Sighting counters and a confirm-gated Clear marks control. #103 fills those counters, so on delivery they read zero for everyone - deliberate ordering.
  • One accent, --patina (verdigris, both colour branches): the far side of the wheel from ember's crimson and clear of the archive blue. Ember still means new chapter only; revocation still wears --danger.

Verification: go vet ./... and go test ./... green (Docker-backed); admin page screenshotted at 1100px and 390px in both colour schemes. Reviewed on both axes (spec, standards); findings on the accent hue, zero-figure honesty and three tests that could not fail are fixed in 58014eb.

Closes #102. The only operational surface was /healthz and a fold-out roster inside the owner's own reading page. This adds /admin: an owner-only page carrying the Reader roster and one row per Poll Lane. - **Poller seam.** `latest.Poller` records each Lane's last pass (`Site`, `Due`, `Checked`, `LastRun`, `Gap`, `Clamped`, `Browser`) and answers `LaneStatus()`; the page reads that snapshot, never a table. A pass that returns before computing its figures (refusal backoff, sidecar down) carries the previous pass's figures forward rather than recording zeroes, and a Lane that has never reached a pace renders no gap at all. Refusal and sidecar reachability are derived at snapshot time. - **Owner gate at registration.** Every route reaching past the acting Reader lives in `adminRoutes()` and is wrapped in `requireOwner` when it is registered, so a missing gate is visible in the route list rather than hidden in a handler. `web.AdminPatterns()` is what the gate test walks, so a new route cannot be added without being tested. A non-owner gets 404, never 403. - **Nil poller is a first-class state.** `main.newRouter` takes the reporter as an interface and converts a nil `*Poller` to a nil interface; no poller and no completed pass both render "No data yet" with the reason spelled out, rather than confident zeroes. - **Roster moved** off the reading page onto /admin, with the Sighting counters and a confirm-gated `Clear marks` control. #103 fills those counters, so on delivery they read zero for everyone - deliberate ordering. - **One accent, `--patina`** (verdigris, both colour branches): the far side of the wheel from ember's crimson and clear of the archive blue. Ember still means new chapter only; revocation still wears --danger. Verification: `go vet ./...` and `go test ./...` green (Docker-backed); admin page screenshotted at 1100px and 390px in both colour schemes. Reviewed on both axes (spec, standards); findings on the accent hue, zero-figure honesty and three tests that could not fail are fixed in 58014eb.
sulthan added 7 commits 2026-08-16 14:31:30 +07:00
The owner's page (issue #102) shows each Reader's Sighting record and offers
one action to wipe it. The counters ship before the Sighting feature that
moves them (issue #103) on purpose: the remedy for a false mark must exist
before marks can be made, or the first broken adapter is fixed with SQL
against production.

- 0010 adds sighting_agreements / sighting_disagreements, both zero-defaulted,
  so every existing Reader reads as trusted.
- ReaderSummary carries both, plus Blocked() against SightingDisagreementLimit,
  so the page states the verdict rather than making the owner derive it.
- ClearReaderMarks zeroes one Reader's pair.
A Lane's pace, its refusal backoff and whether its Site needs the browser were
only ever visible in the log. The admin page (issue #102) has to state them, so
the Poller keeps one snapshot per Lane.

- LaneState/Status (status.go) is the page's view of a Lane: due, gap, clamped,
  refusing, browser.
- runOnce records a snapshot on every return path, including the pass that
  refuses, so a cooling Lane does not read as one that never ran.
- Refusing is derived at snapshot read time from the backoff, not stored: a
  Lane that cooled down between passes must report false without a new pass.
- LaneStatus reports only Sites that have completed a pass, so a fresh restart
  renders "no data yet" instead of confident zeroes.
The only operational surface was /healthz and a fold-out roster inside the
owner's own reading page. This gives the owner a page: two sections of facts on
the same measured sheet, no cards.

- admin.go holds every route that reaches past the acting Reader, listed once
  in adminRoutes() and wrapped in requireOwner at registration - a missing gate
  is visible in the route list rather than hidden inside a handler. A non-owner
  gets 404, the same answer revoke already gave.
- Lane figures arrive through the LaneReporter seam, so the page reads the
  running poller rather than a table. newRouter converts a nil *Poller to a nil
  interface: a typed nil would make the page claim a poller exists.
- The roster moves out of the reading page and gains the Sighting counters, the
  blocked verdict and Clear marks. Clearing restores a privilege, so it is a
  plain ghost button; --danger stays with revocation.
- --patina is the page's one accent, held at the weight of the other action
  accents. Neither --ember (new chapter) nor --danger (destruction) is borrowed
  for system health.
- web_test.go walks web.AdminPatterns() rather than naming routes by hand, so a
  new administrative route that forgets requireOwner fails the gate test
  instead of shipping open.
- The harnesses take a LaneReporter; a fake one keeps the page's tests free of
  a poller and a Site.
- backend/AGENTS.md records the adminRoutes/requireOwner rule and the nil-poller
  trap; design-system.md records --patina and the admin page's shape.
Two-axis review found the shipped colour and three weak tests.

- --patina was a warm gold at the same hue family as --brass; the spec
  asked for a cool blue-green so an unhealthy Lane is unmistakably not
  ember. Now verdigris in both branches, with docs/design-system.md
  stating why the far side of the wheel is the point.
- A Lane pass that returns before computing its figures carries the
  previous pass's due count and gap forward instead of recording zeroes,
  and Checked rides beside Due so a stopped Lane is distinguishable from
  a quiet one.
- TestAdminPageWithoutAPollerSaysSo now separates the two causes it
  conflated, TestOwnerClearsReaderMarks seeds real counters so the
  clearing assertion can fail, and TestLaneStatus asserts Checked and
  the carry-forward.
- backend/AGENTS.md records the one deliberate owner comparison outside
  requireOwner and the carry-forward rule.
sulthan merged commit 1e6f1e985d into main 2026-08-16 15:02:15 +07:00
Sign in to join this conversation.