Admin dashboard information architecture and route map #115
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Part of #114
Question
What are the admin dashboard's pages, what lives on each, and what are their routes?
Settled going in: sub-routes with a nav row rather than one long stacked page, because Series
needs a filterable list plus a bookmarkable address (
/admin/series?filter=no-cover) and aper-Series detail page. Also settled: the surface leads with one summary line aggregating Lane
state and Series staleness.
To decide:
adminRoutes()(backend/internal/web/admin.go) they extend,including whether the existing
GET /ui/admin/lanesfragment pattern generalises to each newblock or whether only Lanes keeps a self-refreshing fragment.
together; whether Readers stays on the landing page or moves to its own route is open.
(
latest.Statusplusseries.latest_checked_at) — the poll-history ticket may extend itlater, so it must be useful without history.
seriesis keyed(site, series_id); the wirealready derives
site:series_id, so whether the URL carries that composite or something elseneeds settling.
/adminentry.Answer
Four pages behind one nav row, plus the one existing fragment. Every new route joins
adminRoutes()(backend/internal/web/admin.go), sorequireOwnerand the gate test cover itby construction rather than by remembering a check.
GET /adminGET /admin/laneslanesblock, still self-refreshingGET /admin/readersreadersroster and its two actionsGET /admin/seriesGET /admin/series/{key}key = site:series_idGET /ui/admin/lanesPOST /readers/{id}/revoke,POST /readers/{id}/clear-marksPOST /admin/series/{key}/<verb>Address conventions
/admin/...; self-refreshing fragments under/ui/admin/...;intervention posts to
/admin/series/{key}/<verb>and answers with the swapped row or block,the shape
renderRosteralready uses.symmetry edits working templates for no gain.
site:series_idin one path segment, matching/ui/bookmarks/{key}andstore.Get. A surrogate id would need a column and a migration tosave nothing;
:is legal unescaped in a path segment and no observedseries_idcarries/.The page split rule
Landing shows library shape, read from the database. The Lanes page shows poller liveness,
read from memory. One rule decides where any new figure goes, and nothing is rendered on two
pages to drift apart. So due / checked / ran / gap / refusing / asleep / browser stay on
/admin/lanesand never appear in the landing page's per-Site table.Refresh
Only Lanes refreshes on a timer. Three reasons, and the first is measured:
defaultRest = time.Hour(internal/latest/sites.go), so a Lane rests an hour between passes andseries.latest_checked_atmoves at that granularity — a 30s timer would re-run a cross-Seriesjoin ~120 times an hour to redraw the same rows.
LaneStatus()by contrast copies a map out ofmemory and touches no database, which is why its timer is free. And an auto-swap on an action
surface eats a half-open Cinder
.confirm-row, or re-sorts the list between the press and theconfirm. Freshness on the new pages is per-action: each button answers with freshly rendered
markup, and force-poll (#119) is asynchronous anyway — the Lane notices the flag on its next
pass, so a timer would show "nothing yet" for up to an hour. A manual refresh button stays
available later as one
hx-getattribute; it is not bought now.The summary line
<verdict> · <N> series waiting · <M> unchecked over 24hlaneRow.Attentionis set, else " lanes needattention"; when
lanesView.Rowsis empty it says no Lane has reported since restart ratherthan "healthy" — the rule
lanes.htmlalready keeps, never drawing absent data as confidentzeroes.
N= sum ofLaneState.Dueacross reported Lanes (memory).M=COUNT(*)overserieswhereseries_url <> ''andlatest_checked_atolder than 24h..attentionclass. Never--ember(new chapter only), never--danger(destruction).
Landing stats block
One compact block of
label — numberrows under the summary line, hygiene numbers linking into/admin/series?filter=…so a count and its entry point are the same control:cover_address = ''), neverchecked (
latest_checked_at = 0with aseries_url), stale over 24h, unpollable(
series_url = ''), Latest Chapter Sighting-raised (latest_sighted_at > latest_checked_at).series.sitewith those database facts.All of it is one grouped pass over
series—COUNT(…) FILTER (WHERE …)grouped by site, notone query per figure. Note
EligibleSeriesCountalready exists but takes one Site per call andreturns one number; the landing page wants every Site at once, so this lands as a second method
on #116's read model rather than five calls.
Excluded deliberately: anything per-Reader (privacy boundary), anything over time (the map rules
out timeseries).
What this pushes onto other tickets
landing page's per-Site table, and are impossible today — nothing about a poll is persisted.
Until #117 lands, that column is absent, not zero. Also relevant to #117's shape: only two
of three failure kinds are visible to the Lane at all (a Site refusing, the sidecar
unreachable); an adapter reading a wrong number (#79) is not, so a rendered "0 errors" would
lie in exactly the case that motivated the dashboard.
Series list projection.
stacked page.
.Owner-gatedAdminlink inapp.htmlis theonly way in, and the nav row handles everything inside. No per-Series admin link from library
cards — a second gated branch in the library templates for a hop the admin Series list already
provides.
sulthan referenced this issue2026-08-18 06:59:54 +07:00