Build lane 3: the campaign core and Settings (CAMP-1 to CAMP-17, SH-7) #729

Closed
opened 2026-09-22 00:12:20 +00:00 by claude-bot · 9 comments
Collaborator

Build lane 3 of the v4.5.0 redesign. The brief is docs/design/v4.5.0-brief.md (§9, the lanes in order); the build plan is docs/design/v4.5.0-build-plan.md (the rollout rule, the design-system mapping, the journeys as tests). Umbrella: #366. Goes after lane 2.

Scope

Surfaces: CAMP-1 to CAMP-17 (This week, Sessions, the session form, series, Highlights, Storyline, Analytics, Members & characters, Settings in its nine sections) and SH-7 (the shared patterns).
The drawing: /feedback/prototype/lane2, lane2b and lane2c — the page the build must match, still by still.
Decisions: the brief §7, and each surface's header in redesign-surfaces/sources/headers.md.

Server changes it needs (in this lane or an earlier one)

  • search scope (IS-382, #702); step down as GM (IS-383); the invite management split; the retention default and cap (IS-311)
  • the storyline timeline (IS-094) with milestones (CA-192); series management (IS-368); sessions played in the last 30 days (CA-239)

Acceptance

  • Every placement coverage/lane2.py, coverage/lane2b.py and coverage/lane2c.py classifies as drawn or replaced is built as the prototype draws it; the script is the checklist. Rows it marks behaviour are tested, not drawn.
  • J1 (create, time zone, invite, Discord) and J6 (Highlights, Storyline) pass on desktop, on a phone and keyboard-only in the journeys suite (#726), with the axe scan clean (WCAG 2.2 AA, #367).
  • The brief's vocabulary (§5) throughout, from vocabulary.js; no page-level error destroys rendered content; every async operation shows progress and a next action on failure; disabled controls say why.
  • Main stays deployable after every PR of the lane: the new shell carries the old pages until each is replaced (the rollout rule).
  • The surfaces' headers in redesign-surfaces/sources/headers.md (Decided lines and the owner's review notes) are honoured; a departure is a decision change on #659 first.

Notes

  • SH-7 is the patterns sheet: the stops, errors, empty states and the voice rule are components from lane 0, applied here first.

🤖 Generated with Claude Code

**Build lane 3 of the v4.5.0 redesign.** The brief is `docs/design/v4.5.0-brief.md` (§9, the lanes in order); the build plan is `docs/design/v4.5.0-build-plan.md` (the rollout rule, the design-system mapping, the journeys as tests). Umbrella: #366. Goes after lane 2. ## Scope **Surfaces:** CAMP-1 to CAMP-17 (This week, Sessions, the session form, series, Highlights, Storyline, Analytics, Members & characters, Settings in its nine sections) and SH-7 (the shared patterns). **The drawing:** `/feedback/prototype/lane2`, `lane2b` and `lane2c` — the page the build must match, still by still. **Decisions:** the brief §7, and each surface's header in `redesign-surfaces/sources/headers.md`. ## Server changes it needs (in this lane or an earlier one) - search scope (`IS-382`, #702); step down as GM (`IS-383`); the invite management split; the retention default and cap (`IS-311`) - the storyline timeline (`IS-094`) with milestones (`CA-192`); series management (`IS-368`); sessions played in the last 30 days (`CA-239`) ## Acceptance - [ ] Every placement `coverage/lane2.py`, `coverage/lane2b.py` and `coverage/lane2c.py` classifies as **drawn** or **replaced** is built as the prototype draws it; the script is the checklist. Rows it marks **behaviour** are tested, not drawn. - [ ] J1 (create, time zone, invite, Discord) and J6 (Highlights, Storyline) pass on desktop, on a phone and keyboard-only in the journeys suite (#726), with the axe scan clean (WCAG 2.2 AA, #367). - [ ] The brief's vocabulary (§5) throughout, from `vocabulary.js`; no page-level error destroys rendered content; every async operation shows progress and a next action on failure; disabled controls say why. - [ ] Main stays deployable after every PR of the lane: the new shell carries the old pages until each is replaced (the rollout rule). - [ ] The surfaces' headers in `redesign-surfaces/sources/headers.md` (Decided lines and the owner's review notes) are honoured; a departure is a decision change on #659 first. ## Notes - SH-7 is the patterns sheet: the stops, errors, empty states and the voice rule are components from lane 0, applied here first. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Picking this up. Lane 2 (#728) is merged — all twelve public pages — so lane 3 is next in the build plan's order.

Read first: the three drawings (/feedback/prototype/lane2, lane2b, lane2c), the three checklists, and the surfaces' headers.

This is much the biggest lane yet

Script Surfaces Rows to build
lane2.py CAMP-1 This week, CAMP-2 Sessions list, CAMP-3 New/edit session, CAMP-8 Members & characters 106
lane2b.py CAMP-9 to CAMP-17 — Settings in its nine sections 128
lane2c.py CAMP-4 series, CAMP-5 Highlights, CAMP-6 Storyline, CAMP-7 Analytics, SH-7 patterns 75

309 placements to build, against lane 2's 93 and lane 1's roughly 150. CAMP-8 alone is 42 and CAMP-15 is 35 — each about a third of lane 2 on its own. So this is a longer series than the last two rather than a bigger PR.

The PR series

Working agreements §7: the lane's server changes first, then UI only; every PR names its coverage rows.

1 — Server: retention's default and its cap (D24). IS-311 and IS-312. Today get_effective_retention takes a campaign's override verbatim under max(1, …), so a campaign can keep audio longer than the instance — the instance setting is a default in name and nothing in practice. D24 makes it two numbers: a default every campaign starts from, and an absolute cap no campaign may pass, with the form naming both so a GM can see the room they have.

Worth saying because it closes a loop from last lane: this is what makes the privacy page's sentence true. PUB-12 currently cannot print the drawing's "A GM can keep less than this. Nobody can keep more.", because it is not true of the code; Privacy.jsx carries a comment saying to restore the drawn sentence when the cap lands. That happens in this PR.

2 — Server: the rest. The storyline's timeline (IS-094) and milestones (CA-192), series management (IS-368), search scope (IS-382, #702), stepping down as GM (IS-383), and the invite-management split. Two server PRs rather than one because the first is a data-governance change that deserves reviewing on its own, and the rest are additive.

3 — This week, the Sessions list, and the session form. CAMP-1, CAMP-2, CAMP-3 (64 rows). Splits CampaignDetail.jsx; its session parts stay for lane 4.

4 — Members & characters. CAMP-8 (42 rows), on its own because it is the single largest surface in the lane.

5 — Settings, the first five sections. CAMP-9 General, CAMP-10 Invites & joining, CAMP-11 Ownership, CAMP-12 Discord, CAMP-13 Recording, retention & AI (73 rows). CAMP-13 is where PR 1's cap surfaces.

6 — Settings, the last four. CAMP-14 Wiki, CAMP-15 Content packs, CAMP-16 Foundry, CAMP-17 Archive, export, clone & delete (55 rows).

7 — Series, Highlights, Storyline, Analytics, and the patterns sheet. CAMP-4 to CAMP-7 and SH-7 (75 rows). SH-7 goes last on purpose: it is the sheet the whole rebuild is measured against, and by then six PRs of this lane will have applied its patterns, so it is drawn from what was actually built rather than ahead of it.

J1 (create, time zone, invite, Discord) and J6 (Highlights, Storyline) grow with the PRs that build what they walk, as in lanes 1 and 2, rather than in a PR of their own.

Departures

None decided in advance. Anything the build cannot draw as the prototype draws it gets recorded here before it is built, per §7 — the build does not redesign.

🤖 Generated with Claude Code

**Picking this up.** Lane 2 (#728) is merged — all twelve public pages — so lane 3 is next in the build plan's order. Read first: the three drawings (`/feedback/prototype/lane2`, `lane2b`, `lane2c`), the three checklists, and the surfaces' headers. ## This is much the biggest lane yet | Script | Surfaces | Rows to build | |---|---|---| | `lane2.py` | CAMP-1 This week, CAMP-2 Sessions list, CAMP-3 New/edit session, CAMP-8 Members & characters | **106** | | `lane2b.py` | CAMP-9 to CAMP-17 — Settings in its nine sections | **128** | | `lane2c.py` | CAMP-4 series, CAMP-5 Highlights, CAMP-6 Storyline, CAMP-7 Analytics, SH-7 patterns | **75** | **309 placements to build**, against lane 2's 93 and lane 1's roughly 150. CAMP-8 alone is 42 and CAMP-15 is 35 — each about a third of lane 2 on its own. So this is a longer series than the last two rather than a bigger PR. ## The PR series Working agreements §7: the lane's server changes first, then UI only; every PR names its coverage rows. **1 — Server: retention's default and its cap (D24).** `IS-311` and `IS-312`. Today `get_effective_retention` takes a campaign's override verbatim under `max(1, …)`, so a campaign can keep audio **longer** than the instance — the instance setting is a default in name and nothing in practice. D24 makes it two numbers: a default every campaign starts from, and an absolute cap no campaign may pass, with the form naming both so a GM can see the room they have. Worth saying because it closes a loop from last lane: **this is what makes the privacy page's sentence true.** PUB-12 currently cannot print the drawing's *"A GM can keep less than this. Nobody can keep more."*, because it is not true of the code; `Privacy.jsx` carries a comment saying to restore the drawn sentence when the cap lands. That happens in this PR. **2 — Server: the rest.** The storyline's timeline (`IS-094`) and milestones (`CA-192`), series management (`IS-368`), search scope (`IS-382`, #702), stepping down as GM (`IS-383`), and the invite-management split. Two server PRs rather than one because the first is a data-governance change that deserves reviewing on its own, and the rest are additive. **3 — This week, the Sessions list, and the session form.** CAMP-1, CAMP-2, CAMP-3 (64 rows). Splits `CampaignDetail.jsx`; its session *parts* stay for lane 4. **4 — Members & characters.** CAMP-8 (42 rows), on its own because it is the single largest surface in the lane. **5 — Settings, the first five sections.** CAMP-9 General, CAMP-10 Invites & joining, CAMP-11 Ownership, CAMP-12 Discord, CAMP-13 Recording, retention & AI (73 rows). CAMP-13 is where PR 1's cap surfaces. **6 — Settings, the last four.** CAMP-14 Wiki, CAMP-15 Content packs, CAMP-16 Foundry, CAMP-17 Archive, export, clone & delete (55 rows). **7 — Series, Highlights, Storyline, Analytics, and the patterns sheet.** CAMP-4 to CAMP-7 and SH-7 (75 rows). SH-7 goes last on purpose: it is the sheet the whole rebuild is measured against, and by then six PRs of this lane will have applied its patterns, so it is drawn from what was actually built rather than ahead of it. J1 (create, time zone, invite, Discord) and J6 (Highlights, Storyline) grow with the PRs that build what they walk, as in lanes 1 and 2, rather than in a PR of their own. ## Departures None decided in advance. Anything the build cannot draw as the prototype draws it gets recorded here before it is built, per §7 — the build does not redesign. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

A correction to my own plan, and the server series refined, after mapping what actually exists.

IS-382 is not what I assumed

I filed it under "search scope" next to lane 1's search-all, as if it meant content search. It does not. Checked against the ledger and D56:

IS-382 — Who "Add someone" and "Attach to an account" can find is an instance setting: anyone on this server, or only people the GM has played with — and the line under the search box says which is in force.

It is a privacy control for who a GM can enumerate, which is harmless on a private server and a way to list strangers on a hosted one. Today campaign_service.search_non_members ILIKEs across every non-system account on the instance with no scoping at all, and both "Add someone" and "Attach to an account" call the same endpoint. There is no "played with" concept anywhere in the backend.

So this is a new instance setting plus a scoped query, on CAMP-8, not a change to the search box in the app bar. Same shape as blurb last lane: lane 3 reads the setting, lane 9 draws the admin control that writes it.

IS-383 is narrower than it sounds, and cuts against an earlier decision

D57: the owner can step down as GM while another member is GM, from their own row; ownership itself is unchanged. Not any GM.

The gotcha is that #644 deliberately removed exactly this kind of guard — the code says so in four places: the last GM can leave, the owner may remove the last GM, "a campaign with no GM is a legitimate state". So IS-383 adds a sole-GM check to one specific path (the owner changing their own role) without reintroducing it for leaving or for demoting somebody else. Worth stating plainly because the obvious implementation would undo #644.

The invite split needs no server work at all

D23 is an IA change: today's single InvitePanel mixes "get a link to paste into Discord" with "manage the set of links". The backend already separates cleanly by function — nine endpoints, each doing one thing. So the split is entirely PRs 4 and 5's business, and drops out of the server series.

The server series, revised to three

Front-loaded, each reviewable on its own:

1 — Retention's default and its cap (D24). In progress. The crux that the plan's one-line summary hid: retention is a (mode, quantity) pair, so a cap expressed as a number is not a cap — a campaign could keep audio for ever by changing the mode rather than the number. An absent cap means "no cap", so every existing instance behaves exactly as today until an administrator sets one; a cap that appeared retroactively would start deleting people's audio. The clamp goes in get_effective_retention's read path, which is what the nightly sweep obeys, and a stored over-cap override is read as the cap without its row being rewritten — the GM's stated preference is worth keeping for the day the cap is raised.

2 — Members' server. IS-382's scope setting and scoped query, and IS-383's step-down. Both are CAMP-8's, both are about who can do what to whom, so they review together.

3 — What the reading surfaces need. The campaign timeline (IS-094, D47) — a new endpoint merging sessions, milestones and the wiki's timeline_events, which exist but have never been merged; sessions_last_12_months on the authenticated analytics response (two lines, already unblocked by lane 2's service change); a This week aggregate for CAMP-1; and a read of which sessions belong to a series.

On that last: CAMP-1 has no endpoint today and would otherwise be four client calls on the campaign's most-visited surface. Lane 1 gave Home the same treatment with GET /api/me/home, so an aggregate here is consistent rather than novel.

One gotcha found and worth writing down for whoever builds the timeline: GET /{id}/milestones does not only return Milestone rows — it synthesises "reappeared" entries for NPCs' repeat appearances, with id set to a session id rather than a real primary key. MilestoneResponse types id as UUID | str to allow it. Anything consuming that list expecting a real key will be wrong.

So the lane is eight PRs, three server and five UI, rather than the seven I first posted.

🤖 Generated with Claude Code

**A correction to my own plan, and the server series refined**, after mapping what actually exists. ## `IS-382` is not what I assumed I filed it under "search scope" next to lane 1's `search-all`, as if it meant content search. It does not. Checked against the ledger and D56: > **`IS-382`** — Who **"Add someone" and "Attach to an account"** can find is an instance setting: *anyone on this server*, or *only people the GM has played with* — and the line under the search box says which is in force. It is a **privacy control for who a GM can enumerate**, which is harmless on a private server and a way to list strangers on a hosted one. Today `campaign_service.search_non_members` ILIKEs across every non-system account on the instance with no scoping at all, and both "Add someone" and "Attach to an account" call the same endpoint. There is no "played with" concept anywhere in the backend. So this is a new instance setting plus a scoped query, on **CAMP-8**, not a change to the search box in the app bar. Same shape as `blurb` last lane: lane 3 reads the setting, lane 9 draws the admin control that writes it. ## `IS-383` is narrower than it sounds, and cuts against an earlier decision D57: *the **owner** can step down as GM while another member is GM, from their own row; ownership itself is unchanged.* Not any GM. The gotcha is that **#644 deliberately removed exactly this kind of guard** — the code says so in four places: the last GM *can* leave, the owner *may* remove the last GM, "a campaign with no GM is a legitimate state". So `IS-383` adds a sole-GM check to one specific path (the owner changing their own role) without reintroducing it for leaving or for demoting somebody else. Worth stating plainly because the obvious implementation would undo #644. ## The invite split needs no server work at all D23 is an IA change: today's single `InvitePanel` mixes "get a link to paste into Discord" with "manage the set of links". The backend already separates cleanly by function — nine endpoints, each doing one thing. So the split is entirely PRs 4 and 5's business, and drops out of the server series. ## The server series, revised to three Front-loaded, each reviewable on its own: **1 — Retention's default and its cap (D24).** In progress. The crux that the plan's one-line summary hid: retention is a `(mode, quantity)` **pair**, so a cap expressed as a number is not a cap — a campaign could keep audio for ever by changing the mode rather than the number. An absent cap means "no cap", so every existing instance behaves exactly as today until an administrator sets one; a cap that appeared retroactively would start deleting people's audio. The clamp goes in `get_effective_retention`'s read path, which is what the nightly sweep obeys, and a stored over-cap override is *read* as the cap without its row being rewritten — the GM's stated preference is worth keeping for the day the cap is raised. **2 — Members' server.** `IS-382`'s scope setting and scoped query, and `IS-383`'s step-down. Both are CAMP-8's, both are about who can do what to whom, so they review together. **3 — What the reading surfaces need.** The campaign **timeline** (`IS-094`, D47) — a new endpoint merging sessions, milestones and the wiki's `timeline_events`, which exist but have never been merged; `sessions_last_12_months` on the authenticated analytics response (two lines, already unblocked by lane 2's service change); a **This week** aggregate for CAMP-1; and a read of which sessions belong to a series. On that last: **CAMP-1 has no endpoint today** and would otherwise be four client calls on the campaign's most-visited surface. Lane 1 gave Home the same treatment with `GET /api/me/home`, so an aggregate here is consistent rather than novel. One gotcha found and worth writing down for whoever builds the timeline: `GET /{id}/milestones` does not only return `Milestone` rows — it **synthesises** "reappeared" entries for NPCs' repeat appearances, with `id` set to a session id rather than a real primary key. `MilestoneResponse` types `id` as `UUID | str` to allow it. Anything consuming that list expecting a real key will be wrong. So the lane is **eight PRs**, three server and five UI, rather than the seven I first posted. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Lane 3 progress, and the departures the owner has settled

Merged so far: #763 (retention's cap, D24), #764 (the members' server, D56/D57), #765 (the reading surfaces' server), #766 (Highlights, Storyline, Analytics + the D14 filter), #767 (This week, the sessions list, and the D17 clock).

Open: #768, Members & characters (CAMP-8). Behind it, the Settings frame plus General, Invites & joining and Ownership (CAMP-9, CAMP-10, CAMP-11) is built and waiting on #768 to land. In flight: the new-session form and the series (CAMP-3, CAMP-4).

Departures — owner, 2026-09-23: all three fine

  1. Erasing a member's recordings defaults "Rewrite the summaries afterwards" to ON. The drawing shows aria-pressed="true" in all three stills and its caption calls it "the part people miss"; leaving somebody named in approved summaries defeats the erase. The old page defaulted it off, and the consequence of this default is that an ordinary erase regenerates GM-approved text.
  2. Two sentences dropped from CAMP-8. The lead's "A person's pronouns and their character's are separate facts, and this page keeps them apart" is the page explaining itself on a surface people open constantly, which D20 rules out — the page demonstrates it instead, and the lead is the count alone. And aria-label="Add a member" on a button reading "Add someone" is a WCAG 2.5.3 label-in-name failure inherited from the old "+ Add" button; that one would have gone regardless.
  3. Settings' sub-list ends with a row neither drawing has: "Everything else" → /campaigns/:id/manage. Six of the nine sections — Discord, AI & recording, the wiki's rules, content packs, Foundry, and archive, export & delete — are still on the old campaign page, and that page has no address per section (it draws them inline; ?open= takes only the first-run actions). Without the row they would leave the navigation entirely the day Settings arrived, and archiving or deleting a campaign is not something to reach only by typing an address. It goes with the last of them.

Four drawn rows not built, and why

IS-272–IS-275, CAMP-8's Foundry panel. There is no server for it anywhere — the only Foundry code in the backend imports reference content for content packs; nothing fetches an actor, stores one or records a sync time. D36: a Foundry action is never a dead button, so the panel is absent rather than drawn dead. IS-276, the typed sheet link standing in for it, is built.

Checking where it belongs found that it currently belongs to nobody: lane 11 (#737) names "WIKI-19 and its seams on … CAMP-8" in scope, but its own notes put Foundry-both-ways in "phase 2 … their own issues later". Recorded on #737 so those four rows land in phase 2's scope rather than surfacing later as a hole in CAMP-8.

One server change inside a UI PR

MemberResponse.pronouns (#768). IS-230 asks for a person's pronouns beside their name and there was no wire for them — MemberResponse carried the character's only, on the page built to stop #653's confusion. Eight lines and two tests; §7 wants server work first and as its own PR, so it is flagged here rather than split.

Two test-infrastructure fixes this lane paid for

  • CI's frontend job and the local suite disagreed on Home.test.jsx. vite.config.js raises testTimeout to 15s, but that is not the clock these fail on: Testing Library keeps its own 1000ms asyncUtilTimeout, and Home's first render measures ~570ms on a single core. configure({ asyncUtilTimeout: 5000 }) in the shared setup (#767). Same hazard #755 fixed one instance of.
  • A merge resolution git could not see. AppFrame.test.jsx auto-merged without a conflict and was wrong twice over: once asserting Storyline was absent after #766 built it, once asserting Settings was absent after CAMP-8's split put it there. Both caught by running the suite on the merged tree before pushing, which is now worth doing on every lane-3 merge.

🤖 Generated with Claude Code

## Lane 3 progress, and the departures the owner has settled **Merged so far:** #763 (retention's cap, D24), #764 (the members' server, D56/D57), #765 (the reading surfaces' server), #766 (Highlights, Storyline, Analytics + the D14 filter), #767 (This week, the sessions list, and the D17 clock). **Open:** #768, Members & characters (CAMP-8). Behind it, the Settings frame plus General, Invites & joining and Ownership (CAMP-9, CAMP-10, CAMP-11) is built and waiting on #768 to land. In flight: the new-session form and the series (CAMP-3, CAMP-4). ### Departures — **owner, 2026-09-23: all three fine** 1. **Erasing a member's recordings defaults "Rewrite the summaries afterwards" to ON.** The drawing shows `aria-pressed="true"` in all three stills and its caption calls it "the part people miss"; leaving somebody named in approved summaries defeats the erase. The old page defaulted it off, and the consequence of this default is that an ordinary erase regenerates GM-approved text. 2. **Two sentences dropped from CAMP-8.** The lead's *"A person's pronouns and their character's are separate facts, and this page keeps them apart"* is the page explaining itself on a surface people open constantly, which **D20** rules out — the page demonstrates it instead, and the lead is the count alone. And `aria-label="Add a member"` on a button reading "Add someone" is a **WCAG 2.5.3** label-in-name failure inherited from the old "+ Add" button; that one would have gone regardless. 3. **Settings' sub-list ends with a row neither drawing has: "Everything else" → `/campaigns/:id/manage`.** Six of the nine sections — Discord, AI & recording, the wiki's rules, content packs, Foundry, and archive, export & delete — are still on the old campaign page, and that page has no address per section (it draws them inline; `?open=` takes only the first-run actions). Without the row they would leave the navigation entirely the day Settings arrived, and archiving or deleting a campaign is not something to reach only by typing an address. It goes with the last of them. ### Four drawn rows not built, and why `IS-272`–`IS-275`, CAMP-8's Foundry panel. There is no server for it anywhere — the only Foundry code in the backend imports *reference content* for content packs; nothing fetches an actor, stores one or records a sync time. **D36**: a Foundry action is never a dead button, so the panel is absent rather than drawn dead. `IS-276`, the typed sheet link standing in for it, is built. Checking where it belongs found that it currently belongs to **nobody**: lane 11 (#737) names "WIKI-19 and its seams on … CAMP-8" in scope, but its own notes put Foundry-both-ways in "phase 2 … their own issues later". Recorded on #737 so those four rows land in phase 2's scope rather than surfacing later as a hole in CAMP-8. ### One server change inside a UI PR `MemberResponse.pronouns` (#768). `IS-230` asks for a *person's* pronouns beside their name and there was no wire for them — `MemberResponse` carried the character's only, on the page built to stop #653's confusion. Eight lines and two tests; §7 wants server work first and as its own PR, so it is flagged here rather than split. ### Two test-infrastructure fixes this lane paid for - **CI's frontend job and the local suite disagreed** on `Home.test.jsx`. `vite.config.js` raises `testTimeout` to 15s, but that is not the clock these fail on: Testing Library keeps its own 1000ms `asyncUtilTimeout`, and Home's first render measures ~570ms on a single core. `configure({ asyncUtilTimeout: 5000 })` in the shared setup (#767). Same hazard #755 fixed one instance of. - **A merge resolution git could not see.** `AppFrame.test.jsx` auto-merged without a conflict and was *wrong* twice over: once asserting Storyline was absent after #766 built it, once asserting Settings was absent after CAMP-8's split put it there. Both caught by running the suite on the merged tree before pushing, which is now worth doing on every lane-3 merge. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Lane 3 is built: every surface, and both acceptance journeys

Acceptance

  • J1 (create, time zone, invite, Discord, first session) — #787
  • J6 (recap, highlights, storyline, wiki) — #770

Both pass on desktop, phone and keyboard-only, with the axe scan clean at WCAG 2.2 AA on every page they reach. a11y-baseline.js is still {}.

Merged, in order

PR What
#763 Retention cap (D24)
#764 The members' server (D56, D57)
#765 The reading surfaces' server
#766 Highlights, Storyline, Analytics, and the D14 filter
#767 This week, Sessions, and the D17 clock correction
#768 Members & characters
#769 Settings: the frame, General, Invites, Ownership
#774 The session form and the series
#776 Settings: Discord; Recording, retention & AI
#782 #777: the AI settings read while switched off
#783 Settings: Content packs; Archive, export & delete; SH-7 with D22's leave prompt; and a regression fix for players
#787 J1
#770 J6

Drawn rows deliberately not built

Each has somewhere to go, and none is drawn as a dead control (D36).

  • Foundry: CAMP-8's panel (IS-272–IS-275) and all of CAMP-16 (IS-268–IS-271, IS-321). There's no Foundry server at all, and scenes aren't first-class (#689). Nine rows in all, recorded on #737 for phase 2.
  • CAMP-14, the wiki's rules (IS-053, IS-088). No server behind either; tracked in #779.
  • Clone (IS-224–IS-226). Belongs to #125.
  • Giving a content pack a licence (IS-315's second half). No endpoint sets one.

Found along the way and filed

  • #771: a11y-baseline can't key UUID paths
  • #772: faint-grey contrast in 16 places
  • #773: literal --- in the story
  • #775: dark-only chips; the session page's 13 axe nodes, which lane 4 inherits and J5 walks
  • #778: retention-sweep preview
  • #780: no skip link, and no focus move on navigation (shell)
  • #781: raw exception text in content-pack import errors

Also fixed on the way: CI and tests

  • #784 / #786: the synthetic-session harness runs on a PR only when it touches the recording path. A skipped required job was tested and passes CI / *. On a PR that doesn't touch it, runs are about six minutes shorter.
  • #785 and 23c82b4: four timing races, all the same shape. Each test waited for the first thing on the page and asserted something that arrives later.

Still open from J1's findings

These need a decision rather than a fix:

  • The Contents rail doesn't pick up a campaign created in the same tab (lane 1's shell).
  • A played row in the sessions list always says "No recap yet", and search matches titles only. The list carries no summary; the choice is between a first sentence served for the row and full text for search.
  • Home's fresh-campaign card still links "Schedule the first session" and "Invite players" to where lane 3 used to be.

Leaving this open for the owner's review, as with #727.

🤖 Generated with Claude Code

## Lane 3 is built: every surface, and both acceptance journeys **Acceptance** - **J1** (create, time zone, invite, Discord, first session) — #787 - **J6** (recap, highlights, storyline, wiki) — #770 Both pass on desktop, phone and keyboard-only, with the axe scan clean at WCAG 2.2 AA on every page they reach. `a11y-baseline.js` is still `{}`. **Merged, in order** | PR | What | |---|---| | #763 | Retention cap (D24) | | #764 | The members' server (D56, D57) | | #765 | The reading surfaces' server | | #766 | Highlights, Storyline, Analytics, and the D14 filter | | #767 | This week, Sessions, and the D17 clock correction | | #768 | Members & characters | | #769 | Settings: the frame, General, Invites, Ownership | | #774 | The session form and the series | | #776 | Settings: Discord; Recording, retention & AI | | #782 | #777: the AI settings read while switched off | | #783 | Settings: Content packs; Archive, export & delete; SH-7 with D22's leave prompt; and a regression fix for players | | #787 | J1 | | #770 | J6 | ### Drawn rows deliberately not built Each has somewhere to go, and none is drawn as a dead control (D36). - **Foundry**: CAMP-8's panel (`IS-272`–`IS-275`) and all of CAMP-16 (`IS-268`–`IS-271`, `IS-321`). There's no Foundry server at all, and scenes aren't first-class (#689). Nine rows in all, recorded on #737 for phase 2. - **CAMP-14, the wiki's rules** (`IS-053`, `IS-088`). No server behind either; tracked in #779. - **Clone** (`IS-224`–`IS-226`). Belongs to #125. - **Giving a content pack a licence** (`IS-315`'s second half). No endpoint sets one. ### Found along the way and filed - #771: `a11y-baseline` can't key UUID paths - #772: faint-grey contrast in 16 places - #773: literal `---` in the story - #775: dark-only chips; the session page's 13 axe nodes, which lane 4 inherits and J5 walks - #778: retention-sweep preview - #780: no skip link, and no focus move on navigation (shell) - #781: raw exception text in content-pack import errors ### Also fixed on the way: CI and tests - #784 / #786: the synthetic-session harness runs on a PR only when it touches the recording path. A skipped required job was tested and passes `CI / *`. On a PR that doesn't touch it, runs are about six minutes shorter. - #785 and 23c82b4: four timing races, all the same shape. Each test waited for the first thing on the page and asserted something that arrives later. ### Still open from J1's findings These need a decision rather than a fix: - The Contents rail doesn't pick up a campaign created in the same tab (lane 1's shell). - A played row in the sessions list always says "No recap yet", and search matches titles only. The list carries no summary; the choice is between a first sentence served for the row and full text for search. - Home's fresh-campaign card still links "Schedule the first session" and "Invite players" to where lane 3 used to be. Leaving this open for the owner's review, as with #727. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Correction to the summary above: the nine Foundry rows aren't waiting on an unfiled "phase 2". They're already planned under v5.0.0 – FoundryVTT Phase 1: #5 (connection settings, IS-268/IS-269), #6 (health check, IS-270/IS-271) and #8 (member-list sync, CAMP-8's IS-272–IS-275). IS-321 is #689's. Corrected on #737 too.

🤖 Generated with Claude Code

**Correction to the summary above:** the nine Foundry rows aren't waiting on an unfiled "phase 2". They're already planned under **v5.0.0 – FoundryVTT Phase 1**: #5 (connection settings, `IS-268`/`IS-269`), #6 (health check, `IS-270`/`IS-271`) and #8 (member-list sync, CAMP-8's `IS-272`–`IS-275`). `IS-321` is #689's. Corrected on #737 too. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Owner decisions, 2026-09-23: the open build questions B1–B13

Every question left open by lanes 0–3 was gathered and answered in one pass. The design tracker already had none open, and lanes 1–2's were settled earlier (Q1–Q9, D17).

# Question Decision
B1 What a played row in the sessions list says, and what its filter searches (a) A one-line recap (the summary's first sentence, as on Home's card) is served with each row, and the filter covers title plus that line. Full-text search stays in the app bar (D16). → D91
B2 Invite links that never expire Allowed, with the default staying at 7 days. → D92
B3 Build the retention-sweep preview (#778)? Yes, before v4.5.0. The same read gives "erase this member's recordings" an exact count.
B4 Who builds the wiki's rules (#779, CAMP-14)? Lane 8 (#734).
B5 Does the owner see Settings even as a plain player? Yes. The campaign list carries an owner flag, and the Contents shows Settings to a GM or the owner. → D93
B6 Accessibility sweeps #772 (faint grey) and #775 (dark-only chips) Now, in one PR.
B7 Skip link and focus on navigation (#780) Now, as a shell PR, before lane 4.
B8 Page-scoped entries in the a11y baseline (#771) Build it, with every entry naming the issue that clears it. The file stays {} in practice.
B9 Audit the ~50 "exists, then read its text" tests Yes.
B10 #753: off-host backups can't be read back Fix it before release.
B11 The Contents rail goes stale after create/join/leave in the same tab Re-fetch the campaign list on navigation.
B12 Home's buttons on a new campaign "Schedule the first session" → the session form; "Invite players" → Settings → Invites; J1 updated to match.
B13 Departures made during the build All ratified: General's "Saved"; the game-system dropdown with "Free text / Other"; Recording's own Save/Cancel and "Save the models"; "Keep them" hidden under a cap; the 30-day bin; packs "GM only"; the server's affirmation wording; the leave prompt ignoring hashes that are on the page; no Undo on removing an invite.

Also approved, the small fixes:

  • #773: the literal --- in the story
  • #781: raw exception text in pack imports
  • the bin's 30 days read from the server
  • a debounced member search
  • the bell's /profile#notifications alias not prompting
  • the "offer already out" banner moving into the shell's banner stack once that stack exists

🤖 Generated with Claude Code

## Owner decisions, 2026-09-23: the open build questions B1–B13 Every question left open by lanes 0–3 was gathered and answered in one pass. The design tracker already had none open, and lanes 1–2's were settled earlier (Q1–Q9, D17). | # | Question | Decision | |---|---|---| | **B1** | What a played row in the sessions list says, and what its filter searches | **(a)** A one-line recap (the summary's first sentence, as on Home's card) is served with each row, and the filter covers title plus that line. Full-text search stays in the app bar (D16). → **D91** | | **B2** | Invite links that never expire | **Allowed**, with the default staying at **7 days**. → **D92** | | **B3** | Build the retention-sweep preview (#778)? | **Yes, before v4.5.0.** The same read gives "erase this member's recordings" an exact count. | | **B4** | Who builds the wiki's rules (#779, CAMP-14)? | **Lane 8** (#734). | | **B5** | Does the owner see Settings even as a plain player? | **Yes.** The campaign list carries an owner flag, and the Contents shows Settings to a GM *or* the owner. → **D93** | | **B6** | Accessibility sweeps #772 (faint grey) and #775 (dark-only chips) | **Now**, in one PR. | | **B7** | Skip link and focus on navigation (#780) | **Now**, as a shell PR, before lane 4. | | **B8** | Page-scoped entries in the a11y baseline (#771) | **Build it**, with every entry naming the issue that clears it. The file stays `{}` in practice. | | **B9** | Audit the ~50 "exists, then read its text" tests | **Yes.** | | **B10** | #753: off-host backups can't be read back | **Fix it** before release. | | **B11** | The Contents rail goes stale after create/join/leave in the same tab | **Re-fetch the campaign list on navigation.** | | **B12** | Home's buttons on a new campaign | "Schedule the first session" → the session form; "Invite players" → Settings → Invites; J1 updated to match. | | **B13** | Departures made during the build | **All ratified:** General's "Saved"; the game-system dropdown with "Free text / Other"; Recording's own Save/Cancel and "Save the models"; "Keep them" hidden under a cap; the 30-day bin; packs "GM only"; the server's affirmation wording; the leave prompt ignoring hashes that are on the page; no Undo on removing an invite. | **Also approved, the small fixes:** - #773: the literal `---` in the story - #781: raw exception text in pack imports - the bin's 30 days read from the server - a debounced member search - the bell's `/profile#notifications` alias not prompting - the "offer already out" banner moving into the shell's banner stack once that stack exists 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

Lane 3 completions merged in #802:

  • D91: a played session's row on the sessions list shows its recap line, the same cut Home's card uses. Before this it always said "No recap yet".
  • D92: an invite link can be set to Never expire. No migration was needed.
  • #773: the story no longer prints a literal --- between chapters.
  • #781: import errors say what's wrong with the file. Importers' own messages pass through; raw library text is fixed where it's created and logged on the server.
  • Archive, export & delete now shows the bin's real grace period, which comes from the server.

Also merged since the last update:

Filed from this work:

  • #797: the erase dialog's "regenerate summaries" never runs. Owner decision needed.
  • #798: a failed run's retention clock.

Lane 3 stays open for the owner's review.

🤖 Generated with Claude Code

Lane 3 completions merged in #802: - **D91:** a played session's row on the sessions list shows its recap line, the same cut Home's card uses. Before this it always said "No recap yet". - **D92:** an invite link can be set to **Never** expire. No migration was needed. - **#773:** the story no longer prints a literal `---` between chapters. - **#781:** import errors say what's wrong with the file. Importers' own messages pass through; raw library text is fixed where it's created and logged on the server. - Archive, export & delete now shows the bin's real grace period, which comes from the server. Also merged since the last update: - #793: the accessibility sweep (#772, #775 part, #771). - #801: the remaining dark-only colours (#794, #775). - #799: page titles, scroll reset, and the player-owner's line (#796). - #800: the retention clock (#791). Filed from this work: - #797: the erase dialog's "regenerate summaries" never runs. Owner decision needed. - #798: a failed run's retention clock. Lane 3 stays open for the owner's review. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Author
Collaborator

The owner's review of lane 3 on dev, 2026-09-25. Looks right: This week, Sessions, the form, series, Members, erase, General, Discord, Archive, Highlights and Analytics.

Fix in batch R1 (fix/review-r1):

  • R1-5: Settings → Recording, retention & AI renders, then crashes to "The page tripped over its own feet." The dev backend logged no error, so it's a render-time throw. The investigation also checks why the section boundary didn't contain it.

Changes:

  • Storyline needs navigation between chapters and a jump to the timeline. This is built into the wide-screen mock-ups, in every option.

Pending:

  • Content packs: the owner has no pack to test with. #821 commits a sample good pack and a deliberately broken one.
  • The player-owner (D93): waiting for #821's second campaign, where the owner owns the campaign as a player.
**The owner's review of lane 3 on dev, 2026-09-25.** Looks right: This week, Sessions, the form, series, Members, erase, General, Discord, Archive, Highlights and Analytics. **Fix in batch R1 (`fix/review-r1`):** - **R1-5:** Settings → Recording, retention & AI renders, then crashes to "The page tripped over its own feet." The dev backend logged no error, so it's a render-time throw. The investigation also checks why the section boundary didn't contain it. **Changes:** - **Storyline** needs navigation between chapters and a jump to the timeline. This is built into the wide-screen mock-ups, in every option. **Pending:** - **Content packs:** the owner has no pack to test with. #821 commits a sample good pack and a deliberately broken one. - **The player-owner (D93):** waiting for #821's second campaign, where the owner owns the campaign as a player.
Author
Collaborator

Signed off by the owner on the Redesign Review page, 2026-10-01. The R3 re-check for this lane (Settings → Recording) is marked OK. No changes asked for. Closing.

**Signed off by the owner on the Redesign Review page, 2026-10-01.** The R3 re-check for this lane (Settings → Recording) is marked OK. No changes asked for. Closing.
Sign in to join this conversation.
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
rbrooks/Quest-Board#729
No description provided.