Spec: admin dashboard - pages, Lane observability, poll pass log, and per-Series intervention #134
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?
Spec derived from the wayfinder map #114, which is fully charted. This is spec 1 of 4; the map's
decisions were split for implementability, not re-decided. Nothing here is new — every decision
below was settled in #115, #116, #117, #118, #119, #122 and #123, and this document is the
handoff-ready synthesis.
Blocks: nothing. Blocked by: nothing.
Later specs in the series build on this one: per-Series intervention, the Finished Series cutover,
and per-Series Poll knowledge plus owner notification.
Problem Statement
I own this deployment and I cannot tell whether it is working.
The admin page shows a Reader roster and a Poll Lane block, and the Lane block says "No data yet"
for up to an hour after every deploy, because Lane state lives in the poller's memory and dies with
the process. So the one page whose job is proving the poller is alive cannot answer the question
after the most common event in the system's life.
Worse, the fault that actually costs me something is invisible from every other surface. When a
Poll Lane stops working, the web UI still loads, every Bookmark still opens, Progress still syncs,
and Latest Chapter quietly stops moving. No ember lights, and a Lane that is stuck looks exactly
like a week when the Sites published nothing. There is no reason to open the admin page, which is
precisely why the fault survives.
I also cannot see my library as a whole. Every read of a Series is scoped to one Reader, so there
is no way to ask "how many Series have no Cover", "which ones has the poller never read a chapter
from", or "which ones does nobody hold any more". Those rows accumulate: removing a Bookmark
leaves the Series behind, and nothing ever deletes one.
And I cannot ask for anything. If one Series' Latest Chapter looks stuck I wait for its turn in the
Lane's hour. If a Site is being hammered or is misbehaving I have no way to stop its Lane short of
a redeploy with the whole poller switched off.
Solution
A four-page owner surface, plus the Lane state made durable underneath it.
Observability. The landing page leads with one line — a verdict, how many Series are waiting,
how many have not been checked in twelve hours — and then a stats block where every figure is also
the link to the list of what it counts. The Lanes page shows one row per Site: what its last pass
found waiting, how many it read, its pace, and, when it did no work, the reason it declined. That
reason is the whole difference between a Lane resting and a Lane stuck, and it is recorded rather
than guessed.
Library-wide hygiene. A filterable, bookmarkable Series list across every Reader's library at
once, with eight named hygiene filters. Each figure on the landing page is a door into the list it
counts. A zero prints as a digit and is not a link, because following it lands nowhere.
Intervention, through the database. Two controls that write a row the poller notices on its
next pass, and never command the poller: Check now on a Series, and Pause on a Lane with a
mandatory expiry. Both survive a restart, because both are facts about a Series or a Site rather
than about the running process, and both mean the whole surface stays testable with no poller
running at all.
Privacy. Every figure the owner sees is a Series-level fact plus an anonymous Reader count. The
owner never learns which Reader reads what, and the boundary is enforced by the shape of the type
the store returns, not by a template that happens not to print a field.
User Stories
myself straight to the Series list rather than scrolling one long page.
know where the rest of the surface is.
in one glance whether to keep reading my library or start investigating.
"healthy", so that I know the size of the problem before I click.
confident zeroes, so that "nothing has happened" is never rendered as "everything is fine".
question thirty seconds after a deploy.
that read nothing because nothing was due from one that read nothing because it is broken.
Lane, a refusing Site, a sleeping browser and a failed query are not all rendered as the same
silence.
distinguishable from the eight other ways a pass can end early, so that I only investigate real
faults.
that a Site refusing me and my adapter reading no chapter are not the same fact.
claims a clean bill of health it cannot actually measure.
not immediately re-probe a Site that just told us to back off.
a Chrome I woke up is noticed rather than remembered as dead.
was doing at four in the morning and not only what it is doing now.
grow on a small VPS.
see which Site holds most of my collection.
broken rows without asking about one Reader at a time.
cover" and come back to it.
render a placeholder in everyone's library.
that were created and then forgotten.
that has been quietly skipping work.
PUT created without a series URL and that no client action can ever repair.
than from a Poll, so that I have a worklist of numbers nothing has verified.
that outlived every relationship to them.
from, so that I can find an adapter that has never worked for a Site rather than one that broke
yesterday.
that a count and its entry point are one control.
to an empty list.
number the landing page promised and the number the list shows come from one query.
hygiene list reads as good news rather than as a broken page.
that I can act on one Site at a time without losing the filter.
I move between pages.
so that its address is derivable from a row I am already looking at.
find out whether its page still reads.
that the page does not pretend to know when a sleeping browser will wake.
pending marker is itself the evidence that a Lane is stuck.
actually means something.
that my impatience cannot make a refusal worse.
exist to stop the machine waking itself, not to stop me.
no Reader holds, so that I am not offered a button that can never do anything.
Site that is having a bad day without stopping the other five.
on the one surface whose job is proving the poller is alive.
than about the process.
is not reported back to me as a fault.
page, so that pausing a Lane never breaks somebody adding a Series.
rather than discarding it.
see after a press describe the state after the press.
is never eaten by an auto-swap and the list does not re-sort between my press and my confirm.
operations surface is not something I have to understand.
that a dashboard for keeping the poller healthy is not a reading log.
Reader, so that I can act on a suspicious number without acquiring information I said I would
not hold.
the same device I read on.
not read as a bolted-on tool.
means "new chapter" keeps meaning only that.
Implementation Decisions
Access model and routes
requireOwnerstays the entire access model: single owner, no roles, no second admin, no per-usergrants. Every new route joins
adminRoutes, so the owner gate and the route list cannot drift andAdminPatternskeeps covering all of them by construction.GET /adminGET /admin/lanesGET /admin/readersGET /admin/seriesGET /admin/series/{key}keyissite:series_idGET /ui/admin/lanesPOST /admin/series/{key}/pollPOST /admin/lanes/{site}/pausePOST /admin/lanes/{site}/resumeedits working templates for no gain.
site:series_idin one path segment, matching the shapethe wire and
store.Getalready use. A surrogate id would need a column and a migration to savenothing;
:is legal unescaped in a path segment and no observed series id carries/./admin/..., self-refreshing fragments under/ui/admin/..., actionsanswering with the swapped fragment — the shape
renderRosteralready uses.per-Series admin link from the library cards: that is a second gated branch in the reading
templates for a hop the Series list already provides.
The page split rule
The landing page aggregates over Sites; the Lanes page shows per-Lane detail. No figure appears on
both. (This replaces the earlier "landing from the database, Lanes from memory" rule, whose
premise was the in-memory state this spec deletes.)
Lane state moves into the database
Sites:
(site, ran_at)is the whole index budget: one goroutine per Lane writes sequentially, so thepair is unique without a surrogate id, and it serves both reads — latest row per Site, and a
per-Site window sum. Retention scans, which at roughly 1.7k live rows is cheaper than a second
index.
poll_lanes(site primary key, paused_until, refuse_until), bothdefaulting to zero. This is one row read at the top of a pass serving two gates.
poll_lanes.refuse_until) and the poller's in-memory refusal map isdeleted. A refusal is the Site's mood and outlives our process. This changes today's behaviour,
where a restart forgets a refusing Site and immediately re-probes it — deliberately.
own reach, and a restart re-probing Chrome is correct behaviour rather than lost state.
laneStatesmap,recordLaneState,LaneState,Status,LaneStatus(), the whole status file, andweb.LaneReporter. With the store as the source, anadmin test inserts a pass row rather than constructing a fake reporter. Browser configuration
becomes "is
BROWSER_WS_URLset" read in the web layer's config — strictly more accurate, sinceit describes the deployment rather than whether one goroutine happened to construct a fetcher.
Browser reachability is derived: any browser Site whose latest pass carries
unreachable > 0inside the refusal backoff of now, which needs that backoff constant exported from
latest.The skip enum — one value per return path
The Lane pass has nine exits and today a page sees only "due, nothing checked", which reads as a
stall in eight cases that are not one. One text column, one value per return path:
skip''pausedrefusingsidecar-downno-fetcherdue-queryasleepeligible-countnothing-eligibleThe stall rule follows from it:
due > 0 AND checked = 0 AND skip = ''is the only true stall.Everything else is a Lane that declined to work and said why.
Outcome counts
The classification the Series read already makes is counted on the pass row:
refused(a challengeheld),
unreachable(the browser interrupted),no_chapter(200, real HTML, no chapter found),unfetchable(the host pin refused the stored URL, or no fetcher),errors(everything else —non-200, transport, a failed check stamp).
no_chapteris its own count and is never folded intorefused: it is what a broken adapter lookslike when it breaks loudly.
checked - (refused + no_chapter + unfetchable + errors).unreachableis excluded from that arithmetic because the sidecar-loss path returnsbefore the checked counter increments. A stored success column would be a fifth way to get that
wrong.
observed — the wording is the honesty, because the failure kind that would hurt most (an adapter
parsing a wrong number successfully) is not in this taxonomy and never can be.
list of the four refused Series to point at. A later spec revisits the navigation, not the counts.
Carry-forward
The existing rule moves verbatim: a pass that returned before computing its figures carries the
previous pass's due, gap, clamped and checked forward — exactly when the pass's own gap is zero
— so no row states a zero it did not measure and the read stays a single
DISTINCT ON. Withskiprecorded, a genuine zero beside
skip = 'due-query'now reads correctly rather than as ameasurement, which is why the rule is kept rather than widened.
Rejected: nullable columns plus a read-side "last non-null per Site", and writing no row at all for
a skipped pass (a Lane refusing for a day would look like a Lane that stopped existing).
Store surface for Lane state
RecordLanePassinserts and prunes in the same call — delete-on-insert, so the Lane goroutineis the pruner and no ticker enters a backend that has none (sessions already expire lazily at
lookup for the same reason).
retainBeforeis caller-supplied, keeping the store clockless as thedue query already is.
LatestLanePassesisDISTINCT ON (site) … ORDER BY site, ran_at DESCleft-joined to the Lanerow: one query for the Lanes page and the same rows the landing verdict sums.
LatestLanePass(site)is the carry-forward read — the recorder needs one Site, not six.retention is how far back a question can reach. The two must not be collapsed.
Forced Poll
series.force_poll_at bigint NOT NULL DEFAULT 0, unix ms, zero meaning never asked.Writing it again re-stamps the request time; the write is idempotent.
force_poll_at > latest_checked_at. It clears itself with nosecond 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.
force_poll_atjoins itsGROUP BYlist:Note: the
HAVING … status <> 'finished'clause is today's Lifecycle test and is deleted bythe Finished Series spec later in this series. Implement it as it stands here; that spec replaces
it with a
series.finished_attest rather than amending it.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.
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.
latest_checked_atas the force signal (it corrupts the never-checked and stalecounts the landing page exists to show, and makes a pending marker impossible).
Paused Lane
pass with
skip = 'paused'and sleeps until the expiry. Its Series stay due and unstamped — theidentical state a missing browser leaves them in, so nothing new has to handle it and the queue is
intact when the pause lifts.
for a runtime one is the case where you already have a shell.
the page. Pause governs the Lane only.
site = '*'pseudo-row for a global one (a second meaning forthe primary key of a six-row table).
The cross-Series read model
Two store methods over a dedicated row type. The privacy boundary is the projection, not a template.
it discards. The filter vocabulary is shared by being one set of named predicate constants, not by
being one method.
store.Seriesis deliberately not reused. Its column list carries the Reader id that raised aSighting, so reusing it would put Reader identity in the admin handler and leave the boundary
resting on a template that happens not to print a field.
SeriesRowhas no field for it, andlatest_raised_bynever leaves the store package.SightingRaisedanswers the attributionquestion — the owner learns a Reader's report set this number, and nothing about which Reader — and
is computed in SQL so no caller repeats the comparison.
orphans accumulate; the Lane's join hides them and the dashboard's third job is hygiene, so it must
not.
reader_count = 0is the orphan marker, and it is an anonymous Series-level fact.COUNT, every Bookmark on the Series. This knowingly disagrees with thetwo Lane queries for as long as the finished Lifecycle bucket exists; the Finished Series spec
removes that bucket, after which the two definitions are the same set. Until then, a Series every
Reader finished shows a non-zero count and is never Polled. Recorded rather than hidden.
that column alone gives no stable page boundary and rows would repeat or vanish across pages.
text. Site and Kind are bound parameters. Only compile-time constants may be concatenated into
query text — that rule is not relaxed here.
?page=1-based. The total comes fromCOUNT(*) OVER ()in the same query: windowfunctions run after grouping and before the limit, so the figure counts the filtered groups and one
where-clause cannot disagree with a second copy of itself. Because that count vanishes on an empty
page, the handler treats zero rows with
page > 1as an over-run and re-reads at page 1.SeriesStatsis one grouped pass —COUNT(…) FILTER (WHERE …)per class grouped by Site, plus theLibrary split — not one query per figure. Library-wide totals are summed in Go over six Sites;
ROLLUPwould add a null-Site row every scanner has to special-case. The orphan count needs theBookmark side, so the query left-joins a grouped subquery and counts where that side is null rather
than putting a subquery inside a
FILTER. Landing counts include orphans: they are exactly whatneeds attention. The existing eligible-count method is the wrong shape for this — one Site per call,
one number back.
series (latest_checked_at). The table has only its primary key today. This supports thedefault order and is a judgement, not a measurement — a grouped query over a join may ignore it.
Re-time on real data before adding a second.
The filter vocabulary — eight names
?filter=unpollables.series_url = ''no-chapters.latest_chapter_num IS NULL AND s.latest_checked_at > 0 AND s.series_url <> ''orphanHAVING COUNT(b.reader_id) = 0uncheckeds.latest_checked_at = 0 AND s.series_url <> ''stales.latest_checked_at > 0 AND s.latest_checked_at <= $staleBeforeno-covers.cover_address = ''sighting-raiseds.latest_sighted_at > s.latest_checked_atunpollableandorphanare labelled as the repair they needrather than as the SQL they are: the owner arrives to act, not to admire a predicate.
unpollablemeans an empty series URL only. The second unpollable case — a URL whose host failsthe fetch gate — is invisible to SQL, needs the Site registry in Go, and would break both the count
and the pager if filtered after the read. It is a repair, not a hygiene count, and belongs to the
intervention spec. Note the empty case is reachable and permanent: the column defaults to empty, the
Upsert writes the series URL only when the row is brand new, and no Poll ever writes it, so a PUT
that omitted it creates a Series no client action can fix.
no-chapteranduncheckedare disjoint by construction (zero versus non-zero check stamp), so thetwo counts never double-report a row.
no-chapteris named to match the pass-levelno_chaptercount deliberately: same observation, one durable on the row, the other counted over a window.
only the current number, the poller writes downward unconditionally on any inequality, and a
Reader's PUT can lower it too, so detection needs the fact captured at write time in a new column.
And not wanted — a downward write is the correction, not the fault (the poller tests seed 400 and
assert 296 after a poll), so the filter would flag precisely the Series that just healed. Worse, it
misses the case that actually costs something: an adapter reading a stable wrong number every hour
never moves, so nothing ever fires.
sighting-raisedstays the only honest suspicion lens, and handrepair is the intervention spec's job.
The landing page
<verdict> · N series waiting · M unchecked over 12h, fully database-computed.Nsumsdueover the latest pass per Site;Mcounts Series with a series URL whose check stampis older than the window. The verdict reads "All lanes healthy" when nothing needs attention, else
" lanes need attention"; with no pass rows at all it says no Lane has reported rather than
"healthy". That state does not disappear — it narrows from after every deploy to a virgin
database.
ownerWindow = 12 * time.Hour, in the web package, feeding both thestaleness cutoff and the outcome window. Twelve hours because the owner looks once by day and once
by night and each look should cover the interval since the last. It is a human threshold,
deliberately not a multiple of the Lane's rest, and the page reads the database rather than the
poller so it cannot follow the Lane's pace anyway. Two differently-named twelve-hour constants on
one page is how they drift apart.
library-wide and per Site.
Library split to
?kind=, a per-Site row label to?site=, the roster count to the Readers page,each hygiene count to
?filter=, each per-Site hygiene count to both. A zero renders the digitand is not a link — the figure stays, because a measured zero is a real fact, but no anchor,
because following it lands on an empty list.
live on the Lanes page; on the landing they were nine columns of zeros burying the four columns that
move.
is three counts and one row — so a sum over-reports while a distinct count is a number nothing can
be done about. The stats block is the whole hygiene surface and every figure on it is individually
actionable.
Refresh
Only the Lanes block refreshes, on the existing 30s timer, now a six-row primary-key read rather than
a map copy. Three reasons the other pages have none: the Lane rest is an hour, so the check stamp
moves at that granularity and a 30s timer would re-run a cross-Series join roughly 120 times an hour
to redraw the same rows; an auto-swap on an action surface eats a half-open confirm row or re-sorts
the list between the press and the confirm; and a Forced Poll is asynchronous by construction, so a
timer would show "nothing yet" for up to an hour. A manual refresh button stays available later as one
attribute; it is not bought now.
Visual surface
The artifact to port is the Claude Design project BookmarkManager Web UI
(
969ac210-fe02-4c01-ae1b-9a271dcc779a), filesadmin.cssandadmin-overview.html,admin-lanes.html,admin-readers.html,admin-series.html,admin-series-detail.html. Thethree-variant sketch on branch
prototype/admin-surface-122is superseded exploration — variant Bwon; port from the design project, not from that branch.
--measure-wide: 1080px, admin only; the 760px reading measure is unchanged andadmin is the only consumer of the wide one.
uppercase labels, hairline row rules, no vertical rules, no card backgrounds, no corners.
Gutters are cell padding, never a column gap — a gap slices the row rule into segments and reads as
a ragged edge. Figures are tabular mono in mixed case at roughly zero tracking; uppercase at wide
tracking is for labels and marks only. Titles stay in the display face.
and the facts on the line under it, so a long title never breaks column rhythm. Separation is
banding rather than hairlines, so a title and its facts read as one record; banding is a class,
not an
nth-of-type, because collapsed confirm rows are row siblings and would throw thealternation off. Lanes and the landing per-Site table are single-line rows; their numerics are
centred rather than right-flushed, because at column width a right-flushed figure loses its header.
+Ntail rather than stacking.unconfirmed, and its 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. Destructive actions use the
danger ghost and open an in-place confirm row: a full-width grid row on the danger wash with
Keep / Remove, shipping hidden and unmounted, or every eligible row prints an empty striped
band.
deliberate pause stays patina. Both colour branches are touched together.
right-aligned cluster in the topbar. Section heads are mono uppercase led by a 34px patina tick, not
a full-width hairline — stacked rules competed with the tables' own rules. The verdict line is set
in the mono data face at 15px, not the display face: it is three counts, not a page title.
minmax(232px, 1fr), one rule on the block, zeros in the mutedcolour and unlinked.
reachability is a status line pinned to the section heading, not a paragraph. A trouble state
renders danger; a pause renders patina as
paused · resumes in 3h. The pause select and Pauselive in one bar and Resume replaces them in the same slot.
site:series_id · site · kind, a 160pxhatched cover, an uppercase mono meta row carrying the marks, then a two-column grid reserved for
the intervention forms the next spec adds, with Check now below.
action cluster so a chip cannot make one row taller.
other table and the detail grid at ≤899px; stacked rows flex-wrap with the title full-width and
actions pushed right.
The filter control
A
<select>whose option labels carry their counts (No cover (3)), not a row of eight plainlinks — eight hygiene labels do not read as a chip row. Site is a second select; Library is a
three-way segmented row marked active in patina. Filters stay one-at-a-time and URL-addressable.
Frontend dependencies
Nothing new. The vendored htmx is 2.0.4 and every primitive this surface needs is in that build
and already used in tree:
hx-getplushx-push-urlfor URL-addressable filters (the reading tabsalready prove it, and the docs' requirement that a pushed URL render a full page is satisfied by these
handlers),
hx-trigger="keyup changed delay:500ms"for a debounced search,hx-targetplushx-swap="outerHTML"or an out-of-band swap for a single-row re-render,hx-trigger="every 30s"scoped to its own fragment for the Lanes block, and either
hx-confirmor the existing inline confirmrow for gating. htmx has no paging attribute: load-more is a
beforeendswap and numbered pages arethe tab pattern plus a pushed URL — same primitives, different swap and URL semantics, so paging is an
IA decision rather than a capability limit.
The existing client-side filter script is not reusable here. It is a pure title filter over an
already-rendered list, it issues no requests, it changes no URL, and it is not loaded on the admin page
at all. Client-side filter state cannot be bookmarkable without hand-written history calls, which is
exactly what these routes must avoid — so filtering is server-side. Research note:
docs/research/htmx-filterable-admin-series-list.mdon branchresearch/htmx-series-list; delete thebranch once this spec is implemented.
Testing Decisions
What makes a good test here: it asserts observable behaviour at a seam the owner or a Reader can
actually reach — a rendered page, a store method's returned rows, a poller pass's effect on the
database — and it fails on a plausible bug rather than on a refactor. No test asserts a template's
internal structure, a private helper's shape, or a log line's wording unless that wording is the
contract (the none observed zero is).
The primary seam is the router, and it is an existing one: the package-level web tests build a real
router over a throwaway Postgres and drive it with an owner session cookie. Every admin page, filter,
action, empty state and gate is asserted through a request and its rendered response. This spec
reduces the seam count: deleting
LaneReporterdeletes thefakeLanesfake, and a Lanes-page testinserts a pass row instead of constructing one.
Modules and what is tested at each:
every new route is owner-gated (the existing pattern-driven gate test covers it by construction once
the routes join the route list); the verdict line's three states, including no-passes-yet; each of
the eight filters returns the rows it names and no others; a zero figure renders unlinked; the
heading's count agrees with the row count; each empty state names its filter;
?site=and?kind=stack on a filter without dropping it; page 2 of a one-page result re-reads at page 1; Check now is
absent on a Series with no URL and on one with no Readers; a pause renders as paused rather than as
stalled; a Lane with a
skipvalue renders its reason rather than a stall; the outcome counts rendernamed with none observed at zero.
migrations): each filter predicate against seeded rows, including the orphan case that only a left
join can see and the disjointness of
no-chapterandunchecked; the paging tie-break with severalrows sharing a zero check stamp; the window total agreeing with the filtered group count; the
aggregate agreeing with the row query for the same filter;
RecordLanePasspruning on insert;LatestLanePassesreturning one row per Site; the pause upsert rejecting a non-future expiry andResumeLanezeroing rather than deleting the row.column list: assert the admin column constant does not mention
latest_raised_by, and thatSeriesRowhas no field for it. This is the one place a template-only guarantee would rot silently.injected clock): a pass records exactly one row with the right
skipfor each of the nine returnpaths; carry-forward fires exactly when the pass's own gap is zero; a paused Lane makes no fetch and
leaves its Series due and unstamped; a durable refusal is honoured across a fresh poller; a forced
Series is fetched ahead of the rest cutoff, the Sighting deferral and the finished bucket, and is
not fetched through a refusal backoff, an empty URL or the Bookmarks join; a forced Series wakes
a sleeping browser Lane; the pending flag self-clears on the check stamp with no second write.
No new fake, no new interface, no framework. The only test-visible seam this spec adds is the store
itself, which the poller already holds.
Out of Scope
boundary.
boundary; only anonymous counts cross into admin views.
it, and no decision on this map needs a graph.
cost and, more importantly, as insufficient: a pass that checked nothing writes nothing, so "the
Lane ran, everything was rested, all healthy" — the primary health signal — would be invisible.
cannot be aggregated.
retention instinct, and a Forced Poll is already self-evidencing — the unserved request ages
visibly. A 14-day pass log would silently expire the one thing worth looking back on.
at once; that is a SQL migration, and the dashboard's library-wide job is finding broken rows, not
batch-repairing them.
state, the completion hint, and outbound notification. All specified, all in the three later specs
in this series.
Further Notes
migrations and ADRs both stop at 0011, and everything the map specified (the map called them
0012–0019) is unbuilt. This spec's schema work is: the
series (latest_checked_at)index;series.force_poll_at;poll_lanes(site, paused_until, refuse_until); andpoll_passes. Take thenext free numbers in whatever order they land — migrations are globbed by version, so contiguity at
merge time is what matters, not agreement with the map's paper numbering.
deletion of its in-memory twin, and the through-the-database command seam with its queue-jump rules.
CONTEXT.mdalready names Lane Pass, Forced Poll, Paused Lane, Stall, Correction andOrphan Series — those edits landed while charting. Nothing new is needed here.
12h, not 24h, and the display window and retention are 12h and 14 days, two windows on
purpose.
AND refused = 0, because the twice-refused break inside the pass loop is not an early return andso writes
due > 0, checked = 0, skip = ''. Implement the test as stated here; that spec carriesthe amendment with its own tests.
than checking inside itself), and the store's query construction (only compile-time constants may be
concatenated). Form bodies on the action routes are capped the way the API path caps them. Say which
invariant you preserved in the PR and run the full backend test suite before calling it done.
Broken out into ten tickets, all labelled
ready-for-agent, blocking edges as body lines:poll_passesandpoll_lanes, plus their store surface#138, #139 and #140 have no blockers and can run in parallel. #139 → #141 → #145 is expand–migrate–contract on the in-memory Lane state: the durable table lands unread, the poller dual-writes, then the twin is deleted atomically in one ticket, because a half-deleted twin means two sources of truth in tree. #140 lands
series.force_poll_atunused for the same reason; #146 wires it. The visual port is folded into each page's ticket with the sharedadmin.cssfoundation in #138, rather than being a horizontal CSS ticket.Two findings against the code while slicing, both about the skip enum.
runLanePasshas tenreturnstatements, not nine.The refused-twice exit does not need the amendment this spec promises. Further Notes says the twice-refused break "writes
due > 0, checked = 0, skip = ''" and that the notification spec will fix the stall test withAND refused = 0. The premise does not hold:st.Checked++runs after the error switch in the pass loop, so a refused Series still counts as checked — it was attempted — and two refusals meanschecked = 2, never zero. The stall test cannot fire on that path. This spec's own outcome arithmetic agrees, since deriving success aschecked - (refused + …)only works ifcheckedcounts attempts including refusals. Recommend dropping theAND refused = 0amendment from the notification spec rather than implementing it.The real gap is the mid-loop browser-unreachable exit, and it is left open deliberately. When
checkOnereports Chrome unreachable the Lane marks the sidecar down and returns immediately, before the checked counter increments. If the first Series in the queue is the one that loses Chrome, that pass writesdue > 0, checked = 0, and this spec gives that exit no skip value — so it recordsskip = ''and renders as the one true stall. A dead Chrome is the most routine event in this deployment (the sidecar is on-demand and the home machine sleeps), so the surface would report it as an unexplained fault: exactly the false positive the spec exists to eliminate. Thesidecar-downvalue is spent on a different thing, a sibling browser Lane declining at the top of a pass.Per decision, #141 implements the enum verbatim as specified and does not invent a tenth value, so this false positive ships and is settled by the later spec in the series. #141 says so explicitly, so nobody improvises. The cheap fix when that spec lands is to record this exit as
sidecar-downtoo, withunreachable = 1— same reason, same word, and the difference between "I lost Chrome" and "my sibling lost Chrome" is not one the owner would act on differently.