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

7 Commits

Author SHA1 Message Date
sulthan 2be9256e5e chore: refresh the graphify map after the #102 review fixes 2026-08-16 14:31:11 +07:00
sulthan 58014eb8dd review: verdigris patina, honest Lane figures, tests that can fail (#102)
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.
2026-08-16 14:31:00 +07:00
sulthan d878580df8 chore: refresh the graphify map for the admin page (#102) 2026-08-16 14:18:08 +07:00
sulthan 0b946ee2c4 test, docs: admin page gate tests and the page's written rules (#102)
- 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.
2026-08-16 14:17:36 +07:00
sulthan e69da2a99b web: owner-only /admin with the Reader roster and Poll Lane status (#102)
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.
2026-08-16 14:17:30 +07:00
sulthan d555f54529 latest: report each Poll Lane's last pass to the owner's page (#102)
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.
2026-08-16 14:17:22 +07:00
sulthan cdd4eb7c25 store: Sighting counters and the owner's clear control (#102)
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.
2026-08-16 14:17:10 +07:00