Lane 2: setting up a new server, and what it leaves waiting (#728) #759

Merged
claude-bot merged 7 commits from build/lane2-setup-ui into main 2026-09-22 21:12:58 +00:00
Collaborator

Lane 2's last PR: PUB-8, IS-251–IS-258, plus J1's setup steps. Frontend and e2e only — every webapp/backend/ file in the diff is #754 arriving through a merge (git diff origin/main..HEAD -- webapp/backend/ is empty).

Stacked on #758, so its four pages appear here until that merges.

The seven steps

The drawing's, in PublicPage: the strip, each step, and Ready with the list of what was skipped and a way back to each. Every step is skippable and every skip is remembered — D70, it reminds and never blocks — and each is recorded through POST /api/setup/steps/{step} as the person leaves it, so the state survives a closed tab.

Nothing writes settings by a second path. The steps drive endpoints that already existed: retention, the backup config and run, the bot settings, regenerate-key, bot/ping, and whisper/test + llm/test — Instance health's own tests, which is what PUB-8's header asked for.

The two departures, verified in the committed code

  • The key step renders no key material. SECRET_KEY appears twice in Setup.jsx: once in the file header explaining this, once as <code>SECRET_KEY</code> naming the environment variable. No value, no Copy button, no endpoint added. The step says where the key lives, says "A backup restored without it cannot read any of them", and reports secrets-health — "All 2 encrypted settings can be read with the key this server is running with", or names the ones that cannot. The checkbox gates Next via Button why (aria-disabled, in tab order, the reason on screen) and never gates Skip.
  • The backup step reads the archive back. "Take one now, and read it back" → "Took a backup and read it back: 214 tables, 1.1 GB, 4 minutes ago." And when the archive went off-host, #753's answer is drawn exactly as the server gives it — "Took a backup, and could not read it back. No local copy of this backup was found…" — rather than dressed up.

One shared-component change, and it is additive

src/ui/Choices/index.jsx gains a named export Check: one .choice row with a checkbox instead of a radio. Choices itself has zero removed lines and Choices.css has no diff at all; src/ui/index.js changes by one line.

The drawing's key step is <label class="choice"><input type="checkbox">, and the kit had no single checkbox — Choices is radios and Toggle is a role="switch" for a setting. The alternatives were restating .choice in Setup.css, which is how a kit stops being one, or a radio group of one, which cannot be unticked.

What could not be built as drawn

  1. Not pub--wide — following Help, Licences, Privacy and Accessibility one PR ago: wide in a still means "fill the still"; in a window it is 1560px, and a form at that measure is unreadable.
  2. The head is the crumb, and the eyebrow is gone. Under the plain wordmark the only doors on the page were the footer's six, none of which goes back into the app — and D70 says this must be a page you can walk away from. The eyebrow then repeated the crumb's sentence one line below it.
  3. "You" can be skipped. The drawing gives it only a Next; the Decided line says "seven steps, each skippable".
  4. The bot's key is made on a button, not on arrival — generating on mount would silently rotate a running bot's key every time anyone opened the step. Still shown exactly once, never in the address, never held past the step.
  5. Retention uses the kit's Segmented, not the drawing's .pick row: .pick was never ported, and Segmented is lane 0's port of that idea with role=radiogroup and arrow keys.
  6. One primary button per step. The drawing makes both "Take one now…" and "Next: the key" primary on the same step, and the models step's "Test both" default — inconsistent with itself.
  7. IS-254 does not repeat "Nobody can keep more." Same reason the privacy page does not: it is not true of the code yet (D24).

And one departure from my own instruction, flagged

I asked for the first-run redirect to fire when the setup state is pristine. It also requires the administrator to have nothing on Home — because pristine is also true of every instance that predates the wizard, so pristine alone would bounce the administrator of a year-old server into "Setting up this server" on upgrade day, which is the nag D70 rules out. It is also why the seeded e2e stack is not a first boot. Good catch; say the word if you want it dropped.

A finding worth carrying into every later lane

Chromium folds CSS generated content into the accessible name and jsdom does not. The step strip's tick and turned arrow are ::before — the drawing's own CSS — so a completed step's accessible name was "✓ You — done" in a real browser and "You — done" under jsdom. The unit tests passed and the journey failed on the same assertion.

That is a real defect, not a test artefact: any state carried only by ::before plus a hidden span is at the mercy of it. The state is an aria-label now, which both agree on and which keeps the visible word inside the name (WCAG 2.5.3), leaving the glyph as decoration.

Checks

lint 0 errors (239 warnings, the unchanged legacy count; this PR's files are warning-free), 1392 tests in 123 files (was 1348), build clean. Journeys: --grep J1 exit 0, 6 passed, 3 skipped — the three are lane 3's placeholder across the three projects — with axe clean on /setup in all three. Every step looked at in the built app, desktop and ~492px, both themes.

🤖 Generated with Claude Code

Lane 2's last PR: **PUB-8**, `IS-251`–`IS-258`, plus J1's setup steps. Frontend and e2e only — every `webapp/backend/` file in the diff is #754 arriving through a merge (`git diff origin/main..HEAD -- webapp/backend/` is empty). **Stacked on #758**, so its four pages appear here until that merges. ## The seven steps The drawing's, in `PublicPage`: the strip, each step, and Ready with the list of what was skipped and a way back to each. Every step is skippable and every skip is remembered — **D70, it reminds and never blocks** — and each is recorded through `POST /api/setup/steps/{step}` as the person leaves it, so the state survives a closed tab. Nothing writes settings by a second path. The steps drive endpoints that already existed: retention, the backup config and run, the bot settings, `regenerate-key`, `bot/ping`, and `whisper/test` + `llm/test` — **Instance health's own tests**, which is what PUB-8's header asked for. ## The two departures, verified in the committed code - **The key step renders no key material.** `SECRET_KEY` appears twice in `Setup.jsx`: once in the file header explaining this, once as `<code>SECRET_KEY</code>` naming the environment variable. No value, no Copy button, no endpoint added. The step says where the key lives, says *"A backup restored without it cannot read any of them"*, and reports `secrets-health` — "All 2 encrypted settings can be read with the key this server is running with", or names the ones that cannot. The checkbox gates Next via `Button why` (aria-disabled, in tab order, the reason on screen) and never gates Skip. - **The backup step reads the archive back.** *"Take one now, and read it back"* → *"Took a backup and read it back: 214 tables, 1.1 GB, 4 minutes ago."* And when the archive went off-host, **#753**'s answer is drawn exactly as the server gives it — *"Took a backup, and could not read it back. No local copy of this backup was found…"* — rather than dressed up. ## One shared-component change, and it is additive `src/ui/Choices/index.jsx` gains a named export **`Check`**: one `.choice` row with a checkbox instead of a radio. `Choices` itself has **zero removed lines** and `Choices.css` has no diff at all; `src/ui/index.js` changes by one line. The drawing's key step is `<label class="choice"><input type="checkbox">`, and the kit had no single checkbox — `Choices` is radios and `Toggle` is a `role="switch"` for a setting. The alternatives were restating `.choice` in `Setup.css`, which is how a kit stops being one, or a radio group of one, which cannot be unticked. ## What could not be built as drawn 1. **Not `pub--wide`** — following Help, Licences, Privacy and Accessibility one PR ago: `wide` in a still means "fill the still"; in a window it is 1560px, and a form at that measure is unreadable. 2. **The head is the crumb, and the eyebrow is gone.** Under the plain wordmark the only doors on the page were the footer's six, none of which goes back into the app — and D70 says this must be a page you can walk away from. The eyebrow then repeated the crumb's sentence one line below it. 3. **"You" can be skipped.** The drawing gives it only a Next; the Decided line says "seven steps, each skippable". 4. **The bot's key is made on a button, not on arrival** — generating on mount would silently rotate a running bot's key every time anyone opened the step. Still shown exactly once, never in the address, never held past the step. 5. **Retention uses the kit's `Segmented`**, not the drawing's `.pick` row: `.pick` was never ported, and `Segmented` is lane 0's port of that idea with `role=radiogroup` and arrow keys. 6. **One primary button per step.** The drawing makes both "Take one now…" and "Next: the key" primary on the same step, and the models step's "Test both" default — inconsistent with itself. 7. **`IS-254` does not repeat "Nobody can keep more."** Same reason the privacy page does not: it is not true of the code yet (**D24**). ### And one departure from my own instruction, flagged I asked for the first-run redirect to fire when the setup state is **pristine**. It also requires the administrator to have nothing on Home — because *pristine is also true of every instance that predates the wizard*, so pristine alone would bounce the administrator of a year-old server into "Setting up this server" on upgrade day, which is the nag D70 rules out. It is also why the seeded e2e stack is not a first boot. Good catch; say the word if you want it dropped. ## A finding worth carrying into every later lane **Chromium folds CSS generated content into the accessible name and jsdom does not.** The step strip's tick and turned arrow are `::before` — the drawing's own CSS — so a completed step's accessible name was `"✓ You — done"` in a real browser and `"You — done"` under jsdom. The unit tests passed and the journey failed on the same assertion. That is a real defect, not a test artefact: any state carried only by `::before` plus a hidden span is at the mercy of it. The state is an `aria-label` now, which both agree on and which keeps the visible word inside the name (WCAG 2.5.3), leaving the glyph as decoration. ## Checks lint **0 errors** (239 warnings, the unchanged legacy count; this PR's files are warning-free), **1392 tests** in 123 files (was 1348), build clean. **Journeys: `--grep J1` exit 0, 6 passed, 3 skipped** — the three are lane 3's placeholder across the three projects — with axe clean on `/setup` in all three. Every step looked at in the built app, desktop and ~492px, both themes. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
The last of lane 1c's twelve: the screens nobody goes looking for, and the three
doors that are nowhere but the footer.

**There is a 404 now** (`SH-009`). The catch-all was `<Navigate to="/dashboard">`,
which is the whole of the issue: a mistyped address and a session that had run
out were the same screen, because both bounced through Home to the sign-in page.
Whoever followed an old link was told, in effect, that they were not logged in.
The route is outside `AuthGuard`, so a signed-in person who mistypes reads the
page as well rather than being silently redirected — which was the other half of
it, and the half with no error to report.

The drawing's one still says *Go to Home*, and that is what a signed-in reader
gets. A signed-out one is offered the front door instead, because for them Home
is `/dashboard` and `/dashboard` is exactly the bounce this page exists to stop,
one click later. The words are the same either way and are on screen
immediately; only the door waits for the `/api/me` probe, since a page that
offers the wrong one and then changes its mind is worse than a page whose action
arrives a moment after its sentence.

**The two crash screens** are one screen and one file (`pages/Crashed.jsx`),
because which of the two boundaries caught the error is the app's business and
not the reader's. What differs is what is true: a page that could not be *shown*
(`SH-010`) against a Quest Board that could not *start* (`SH-012`), and therefore
whether "try again" reloads or drops the error and renders again. Both keep the
detail behind a fold, which matters more than it looks: the first line is
*nothing you were doing is lost; it is still on the server*, and a stack trace
above that sentence has told a great many people the opposite.

The drawing draws the boot crash with nothing behind its fold and this does not.
An error caught above the router still has a message, and it is the only
diagnostic a self-hoster gets for an app that will not start; the fold is drawn
when there is something in it, on both.

That screen is also why `PublicPage` now has a `PublicFrame` under it. The
boundary in `main.jsx` sits above `RouterProvider`, where `useNavigate` throws —
and throwing inside the screen that catches a throw is a loop. Nothing is lost
by it: every link in the frame is a real `<a href>` either way, and after a crash
a real navigation is the better of the two, since it starts the app again rather
than routing inside a broken one.

**The waits stay plain** (`SH-011`, `SH-030`). D65 gives the world's voice to the
stops and to nothing else: a stop is somewhere a reader has arrived and has to
decide what to do, and a joke on a spinner is a joke every time the app is
opened. What the wait screen gained is which wait it is — the session check says
so rather than showing the "Loading…" a page chunk shows.

**`SH-013`** already half existed. `main.jsx` has reloaded once on Vite's
`vite:preloadError` — a chunk whose hash is gone after a deploy — for a long
time, and the service worker is `autoUpdate`. What was missing is that none of it
said anything: the reload was silent, and a page that blanks and comes back by
itself reads as a fault. So the same reload now announces itself. This is not
D13, which is the quiet notice for a version that is *ready but not needed* and
belongs to lane 8; here the files the page is asking for are already gone, so
there is nothing to dismiss.

**Accessibility** (PUB-10, `IS-293`, D4) states the target, what is done, and the
known gaps — the board at the table, the map, long transcripts — because a
statement claiming nothing is wrong is worth nothing to the one reader it is
written for, who has already met the thing it does not mention. It says so of
itself, too: the list goes when a lane fixes something and grows when a lane
breaks something. It is static, and the way to report a barrier is the project or
whoever runs this server, whose address is on the privacy page rather than
copied onto a second one.

**Privacy** (PUB-12, `IS-387`, `IS-388`) is the standard text every server starts
from, with the administrator's own words above it and never instead of it —
nothing an operator writes changes what the software does. The retention lines
are read from `GET /public/policies` rather than typed, which is the entire
point: a policy typed by hand describes the settings on the day it was typed, and
the first time Recording & retention changes it becomes a statement about a
server that no longer exists — about audio of real people. Each mode has its own
sentence and each says what that mode does, so a server keeping recordings for
thirty days cannot read as one that bins them the same night. Recording is
standing consent by joining a campaign, as the repo's rules say; the page states
that and adds no gate.

**Licences and notices** (PUB-6, `IS-186`, `IS-391`, `API-016`, `IS-388`) lists
**nothing** this server has loaded. D64 reversed lane 1c's first drawing of it,
and that reversal is the page: what a table plays with is its own business and a
list of one publisher's bestiary tells a stranger what the group is running. The
attribution a licence actually requires is not skipped — it appears where the
content is read, to the members who read it (`IS-392`). The wording is the
drawing's, which the owner's second look made simpler rather than defensive, and
the address falls back to whoever runs the server when nobody has published one.

The footer finally has its six doors, in the order D15-as-amended-by-D63 gives,
and `AppFrame` draws the same array — the accessibility statement and the privacy
policy are one pair of pages, not two, and the footer is the only door either of
them has.

Two things in the drawing could not be built as drawn, and are the build's rather
than a redesign:

- "A GM can keep less than this. Nobody can keep more." The second half is not
  true of the code: a campaign's retention override is validated but not clamped
  against the instance default (`schemas/campaign.py`, `get_effective_retention`),
  so a GM can set a longer window as well as a shorter one. On the one page whose
  purpose is not to drift from what the server does, a reassuring sentence that is
  false is the worst thing it could carry, so it says a GM can set a different
  span and that this is what the server does otherwise.
- The accessibility statement's lead cites `IS-295` beside the colour rule. That
  is a redesign-ledger id and means nothing to a reader who cannot look it up;
  #367 stays, because it is a real issue in the project anyone can open.

Three things found by looking at the built pages rather than the source, none of
them caught by a test:

- Tailwind's preflight strips the marker from every `ul` in the document, and
  Privacy and Accessibility are the first pages of this frame to write a bulleted
  list in prose — four indented, unmarked paragraphs where the drawing has
  bullets. Restored on the frame's own list rule, which already owns the indent.
- With `/public/policies` down, the privacy page said under *Who runs this
  server* that nobody had set a contact. It did not know that: "nobody has said"
  is a fact about an administrator and a request that never answered is not one.
  It now says which of the two it is.
- The licences page is not `pub--wide` even though the drawing's `licPage` is.
  `wide` in a still means "fill the still"; in the product it means up to 1560px,
  and a prose page at that measure is unreadable. Help settled this one PR ago
  and these three follow it.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The page prints "A campaign's GM can set a different span for their own
campaign. This is what this server does otherwise." where the drawing prints "A
GM can keep less than this. Nobody can keep more." The last PR explained that
the drawing's sentence is false today — a campaign override is bounded below by
`max(1, …)` and not at all above — and stopped there, which leaves the next
reader of the file two ways to be wrong about it. They can "fix" the honest
sentence back to the drawing's, because the drawing is the specification and the
comment does not say the drawing is ahead of the build rather than mistaken. Or
they can leave it long after it stopped being true, because nothing tells them
what would make the drawing's sentence correct or who is building it.

So the comment now carries the decision. **D24** already settled this: instance
retention becomes two numbers, a default every campaign starts from and an
absolute cap no campaign may pass, with a campaign free anywhere between them —
`IS-311` on ADMIN-5/ADMIN-6 in build lane 9 (#735) and `IS-312` on CAMP-13 in
build lane 3 (#729). It cites the D24 row in `CATALOGUE.md` so the pointer
survives a rename of anything else, and it says in as many words that the
drawing's sentence goes back the day the cap exists.

No clamp here and no backend change: the cap belongs to the lanes that own those
two surfaces, and a frontend that enforced it would be a second, invisible
policy on top of the one the server actually applies. What this page owes is to
describe the server it is running on, which is the whole of `IS-387`.

Comment only — no rendered text changes. Lint 0 errors, 1344 tests, build clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The drawing's front door reads "**Amberwake** is run by Dana, *for a few
groups of friends*", and nothing stored that last clause. Lane 2's build found
it by hard-coding it, which would have had every self-hosted Quest Board
telling visitors its operator runs it for a few groups of friends; #752 made
it a field (`blurb` on `GET /public/site`).

`IS-389` names the four things About this server holds — the name, who runs it
and how to reach them, the privacy policy's own words, the numbers switch —
and this is a fifth. Without a row, lane 9 builds the form from `IS-389` and
no real server ever gets a line on its front door.

Placed on ADMIN-15 (primary) and PUB-11, the catalogue rebuilt, and every
coverage script re-run — which is how the second surface turned up:

- `lane1c.py` counts it **drawn** on PUB-11: #751 draws the sentence with the
  line and without it.
- `lane7.py` counts it **deferred** on ADMIN-15, honestly. That form draws Its
  name, Run by, How to reach them and the policy's words, and has no field for
  this one, because the row did not exist when lane 7 was drawn. Left undrawn
  rather than quietly added to a page the owner has already reviewed — lane 9
  (#735) needs one more input, and whether the drawing gains it first is the
  owner's call.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/main' into build/lane2-stops
Some checks failed
CI / Backend lint (ruff) (pull_request) Successful in 32s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m20s
CI / Feedback site tests (pull_request) Successful in 40s
CI / Bot/backend version sync (pull_request) Successful in 37s
CI / Bot tests and audit (pull_request) Successful in 2m34s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m21s
CI / Docker image build (pull_request) Successful in 5m16s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 5m18s
CI / Backend migration, tests, and audit (pull_request) Successful in 13m49s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 18m15s
079db3250e
fix(frontend): the footer's links are a 24px target once there are six (#728)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m38s
CI / Backend lint (ruff) (pull_request) Successful in 27s
CI / Bot/backend version sync (pull_request) Successful in 1m1s
CI / Bot tests and audit (pull_request) Successful in 1m46s
CI / Feedback site tests (pull_request) Successful in 1m29s
CI / Docker image build (pull_request) Successful in 2m0s
CI / Frontend tests, audit, and build (pull_request) Successful in 4m1s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 5m35s
CI / Backend migration, tests, and audit (pull_request) Successful in 15m55s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 19m51s
ba86d30cde
The journeys' axe gate failed on /dashboard in all three phone runs, on WCAG
2.2's 2.5.8 (target size):

    Target has insufficient size (62.8px by 20.1px, should be at least
    24px by 24px). Safe clickable space has a diameter of 23.4px instead
    of at least 24px.

`a:nth-child(3)` is the first footer link — `Footer` renders the version, then
a separator and a link per door — and it was 0.6px short.

Not a pre-existing fault: with two doors the footer never wrapped, so no link
had a vertical neighbour and the spacing exemption carried it. This PR takes
it to six (D15, amended by D63), they wrap on a phone, and vertically adjacent
links then sat 1.6px apart.

So the links carry a real target size, 24px, and the row gap goes to 4px. Both,
deliberately: either alone lands within a rounding error of the threshold, and
a gate that passes by 0.6px is one font-size change from failing again.

24px and not `--touch`'s 44px. That rule is for controls; a footer of six 44px
links is a third of a phone screen, and 2.5.8 AA asks for 24 — 44 is 2.5.5,
which is AAA.

The text does not move: the box grows around the same line.

Docker Desktop was down on this machine after a crash, so this is checked by
CI's axe gate rather than locally — which is the only thing that can really
check it in any case.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
feat(frontend): setting up a new server, and what it leaves waiting (#728, lane 2)
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Docker image build (pull_request) Successful in 42s
CI / Bot/backend version sync (pull_request) Successful in 43s
CI / Bot tests and audit (pull_request) Successful in 1m48s
CI / Backend lint (ruff) (pull_request) Successful in 50s
CI / Feedback site tests (pull_request) Successful in 1m0s
CI / Frontend tests, audit, and build (pull_request) Successful in 3m5s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 3m49s
CI / Backend migration, tests, and audit (pull_request) Successful in 8m59s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 17m19s
38f302d0dc
The last of lane 1c's twelve, and the first screen a self-hoster who has just
brought Quest Board up will ever see: `/setup`, seven steps, admin-only, drawn
in the public frame because a server being set up has no campaign to list,
nothing waiting on it and nowhere to search.

**Every step can be skipped, and every skip is remembered.** That is D70 and it
is not a detail of this page, it is the page: `finish` never refuses because
something is outstanding, Ready is reachable from any step, and what was left
waits as a list in Admin rather than as a banner that nags (`IS-258`). An
administrator setting a server up at midnight will skip, and a server with no
backups should go on saying so — those are the two halves, and a wizard that
enforced the first would only teach people to lie to it.

**Each step is an address.** `?step=backups` is the step, so the back button
works, the strip is a row of real links to a step already walked, and Admin's
waiting list can point at the one thing it is about rather than at the front of
a queue. The outcome of a step is recorded as the person leaves it, not at the
end, which is what makes a closed tab cost nothing; with no `step` in the
address the page opens on the first one the server has not recorded.

**Nothing here writes a setting of its own.** Backups, Retention and Bot
Settings are written through the endpoints that already own them, and the two
connection tests are Instance health's, not a second set — those endpoints
already validate, audit and encrypt, and a second path to the same settings
would be a second set of rules to keep in step with the first. `/api/setup/*`
(#754) records only which steps were done and which were skipped.

**Reaching it without going looking.** An administrator is sent there once from
Home, and the condition is deliberately two things rather than one: the wizard's
own state untouched, *and* nothing at all on their Home — no campaign, none
archived, nothing in the bin. Pristine state alone is also true of every
instance that predates this feature, so on its own it would have bounced the
administrator of a server that had been running for a year into "Setting up this
server" on the day it upgraded, which is exactly the nag D70 rules out. Once per
tab, and the page it lands on has a way off it.

Two departures from the drawing, both settled on #728 before building:

- **The key step shows no key material.** The drawing prints
  `QB_SECRET_KEY=2f9c…a41e` with a Copy button. `SECRET_KEY` is the key every
  other secret is encrypted *with*, the settings layer's rule is that a secret
  is never returned, and the operator put this one in the environment
  themselves — so there is no endpoint that could answer and adding one would be
  the worst thing on this page. The step says where the key lives, says that a
  backup restored without it cannot read the bot token, the AI keys or the mail
  password (the drawing's own sentence, and the one that matters), and confirms
  from `secrets-health` how many encrypted settings are readable, naming any
  that are not. The checkbox and the skip stay.
- **The backup step reads the archive back rather than restoring it.** What runs
  is `pg_restore --list`, so the words say "read it back" and claim no more. A
  backup that has been shipped off-host comes back `verified: false` with a
  sentence saying there is no local copy to read, and the page draws that as it
  is: it is #753, a known defect, and a UI that dressed it up as success would
  be lying about the one thing this step exists to check.

And four smaller ones, all of them the build's rather than a redesign:

- **The card is not `pub--wide`.** `wide` in a still means "fill the still"; in
  the product it means up to 1560px, and a form at that measure is unreadable.
  Help settled this and the three written pages followed it.
- **The head is the frame's crumb, not the plain wordmark.** `Quest Board ›
  Setting up this server`, whose wordmark is a real link to `/` — which for a
  signed-in administrator is Home. That is D70 in one link: under the plain
  wordmark the only doors on the page were the footer's six, none of which leads
  back into the app. The drawing's eyebrow then goes, because it had become the
  same sentence twice one line apart.
- **"You" can be skipped too.** The drawing gives it only a Next; the owner's
  decision is "seven steps, each skippable", and the one step that is an
  exception is the one somebody gets stuck on.
- **The bot's key is made on request, not on arrival.** The drawing shows it
  already made. Generating one the moment the step is opened would silently
  rotate the key of a bot that is already running, every time anybody looked at
  the page. It is still shown exactly once, never put in the address, and never
  held past the step.

`Check` is new in the design system (`src/ui/Choices/`): one of the same
`.choice` rows on its own, for the single thing somebody has to say yes to
before moving on. Purely additive — `Choices` itself is untouched — and it lives
in that folder because it is that stylesheet's row, and a second copy of
`.choice` elsewhere is how a kit stops being one.

**Admin** gets the smallest honest thing (`IS-258`): a warn banner above the
tabs naming the steps that were skipped, with the way back to them. Only once
setup has actually been finished, for the same reason the offer above is two
conditions — an instance that predates the wizard has not left anything undone.
Lane 9 rebuilds that page; this does not.

J1 walks the wizard in front of the campaign it already creates: the steps, a
step recorded on the way out, a step skipped, the skip remembered in the strip,
the key step showing no key, and the list a skip leaves on Ready, with `axe()`
on each. What it cannot walk is the redirect itself — the seeded stack is not a
first boot, Dana has campaigns by the time any journey runs, and reseeding would
cost every other journey its fiction. The spec says so in as many words; both
halves of the redirect are covered in `src/firstRun.test.jsx` instead.

Two things found by looking at the built pages rather than the source:

- The crumb and the drawing's eyebrow were the same sentence, one line apart.
  Reading the markup they are two different pieces of furniture; on screen they
  are a stutter at the top of all seven steps.
- **Chromium folds CSS generated content into the accessible name.** The strip's
  tick and turned arrow are `::before`, so a step's name came out as
  "✓ You — done" in a real browser and "You — done" under jsdom: the unit tests
  passed and the journeys did not. The state is an `aria-label` now — "You,
  done" — which both agree on, keeps the visible word inside the name (WCAG
  2.5.3), and leaves the glyph as the decoration it always was.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign in to join this conversation.
No description provided.