Lane 3: Members & characters (#729, CAMP-8) #768

Merged
claude-bot merged 5 commits from build/lane3-members into main 2026-09-23 07:14:46 +00:00
Collaborator

Lane 3's third UI PR: CAMP-8, Members & characters — 38 of 42 coverage rows, at /campaigns/:id/members, with an entry in the Contents.

It is a page every member opens rather than a panel only the GM could reach (#656). A row is a person — their name, their own pronouns, their role, the owner's mark — and their characters hang under it with their pronouns, which is the whole point of the page: the two facts can never be mistaken for each other.

A server change, inside a UI PR

Flagging this because build-plan §7 wants server work first, as its own PR.

IS-230 asks for a person's pronouns beside their name, and there was no wire for them. MemberResponse carried character_pronouns and not the person's, so the page built to stop #653's confusion had only the character's to print next to a human being. User.pronouns exists and ThisWeekRsvp already carries it (#765).

The change is MemberResponse.pronouns, read from the account in list_members — null when unsaid, nothing inferred from a name, nothing borrowed from the character below. Purely additive, and every member endpoint is built by list_members, so one line covers list/add/role/patch/assign. No BOT_CONTRACT_VERSION move: the bot reads /api/bot/* only.

Splitting eight lines and two tests into their own PR would have cost more than it bought, so it is here with this note rather than silently.

Not built: the Foundry panel (IS-272–IS-275)

Four drawn rows, absent. There is no server for them. The only Foundry code in the backend is app/reference/adapters/foundry.py, which imports reference content for content packs; nothing fetches a Foundry actor, stores one, or records a sync time, and CampaignResponse has no Foundry connection field. Build-plan §3.3's D36 is exactly this case — "a Discord or Foundry action is never a dead button" — so the panel is absent rather than drawn dead.

This is not a gap nobody owns: D82 ("Foundry both ways and Pathbuilder in", owner, 2026-09-21) assigns the integration to WIKI-19, lane 9. IS-276, the typed sheet link that stands in for it meanwhile, is built.

Where the drawing and the server disagreed

  • Three invented numbers — "Rogue 6", "named in four approved summaries", "all 11 recorded sessions". None is on the wire. Followed the server: the character line reads pronouns and heard-as and stops; the erase says "every session this campaign has recorded" without a count. The same call CampaignAnalytics.jsx made for "3 h 38 m a session".
  • Leaving. The drawing gates it on !gm — only a player may leave. #644 says the last GM may leave, so it is gated on "not the owner" instead, and tested from both sides. A guard here would have quietly reverted a decision taken on purpose.
  • The invite panel's hint is hardcoded in the drawing; it depends on join_requires_approval, so there are two wordings.
  • "All links and joining rules" points at Settings → Invites, which this PR does not build. Omitted per the rollout rule; it comes back with CAMP-10.

Where the drawing is wrong

  • aria-label="Add a member" on a button reading "Add someone" — the visible label is not inside the accessible name, a WCAG 2.5.3 (label-in-name) failure, inherited from the old page's "+ Add" button which genuinely needed one. Dropped. The "Close" state keeps a label that contains its visible word.
  • The lead's second sentence — "A person's pronouns and their character's are separate facts, and this page keeps them apart." That is the page explaining itself on a surface people open constantly, which D20 rules out. The lead is the count alone, generated from the data. One line to put back if you want it.
  • No phone still exists for a member row, and the row did not survive one. At 500 px the three controls took the line and flex: 1; min-width: 0 wrapped the person's name mid-word, on the one page where that is unaffordable. Fixed with a container query giving the name and the actions a line each. The render caught it; reading the CSS did not.

One default worth your eye

The erase's "Rewrite the summaries afterwards" defaults ON, as all three drawn stills show (aria-pressed="true"); the old page defaulted it off. The caption calls it "the part people miss", and leaving a person named in GM-approved summaries defeats the erase. But it does mean the default run regenerates approved text. One line to flip if you would rather it were opt-in.

Merges

Two, both with a resolution git could not see:

  • AppFrame.test.jsx auto-merged without a conflict and was wrong — This week asserts Settings is absent from the Contents; CAMP-8's split is what puts it there.
  • vocabulary.js — MEMBERS_AND_SETTINGS retires the moment CAMP-8 has its own page, which its own comment said would happen. What is left behind …/manage is the nine Settings sections, so the entry is SETTINGS.

Checks

Frontend 1591 tests in 127 files, green on the merged tree. Backend 289 passed across the member, ownership, character, erasure, invite and this-week suites; ruff clean. lane2.py prints with no unclassified rows and no classification changed. Rendered lane2.html?only=camp8 in both themes, then served the built dist and screenshotted the real page with real data at desktop and 500 px — which is what found the wrapping bug.

🤖 Generated with Claude Code

Lane 3's third UI PR: **CAMP-8, Members & characters** — 38 of 42 coverage rows, at `/campaigns/:id/members`, with an entry in the Contents. It is a page every member opens rather than a panel only the GM could reach (#656). A row is a person — their name, their own pronouns, their role, the owner's mark — and their characters hang under it with *their* pronouns, which is the whole point of the page: the two facts can never be mistaken for each other. ## A server change, inside a UI PR Flagging this because build-plan §7 wants server work first, as its own PR. `IS-230` asks for a person's pronouns beside their name, and **there was no wire for them**. `MemberResponse` carried `character_pronouns` and not the person's, so the page built to stop #653's confusion had only the character's to print next to a human being. `User.pronouns` exists and `ThisWeekRsvp` already carries it (#765). The change is `MemberResponse.pronouns`, read from the account in `list_members` — null when unsaid, nothing inferred from a name, nothing borrowed from the character below. Purely additive, and every member endpoint is built by `list_members`, so one line covers list/add/role/patch/assign. No `BOT_CONTRACT_VERSION` move: the bot reads `/api/bot/*` only. Splitting eight lines and two tests into their own PR would have cost more than it bought, so it is here with this note rather than silently. ## Not built: the Foundry panel (`IS-272`–`IS-275`) Four drawn rows, absent. **There is no server for them.** The only Foundry code in the backend is `app/reference/adapters/foundry.py`, which imports *reference content* for content packs; nothing fetches a Foundry actor, stores one, or records a sync time, and `CampaignResponse` has no Foundry connection field. Build-plan §3.3's D36 is exactly this case — "a Discord or Foundry action is never a dead button" — so the panel is absent rather than drawn dead. This is not a gap nobody owns: **D82** ("Foundry both ways and Pathbuilder in", owner, 2026-09-21) assigns the integration to **WIKI-19, lane 9**. `IS-276`, the typed sheet link that stands in for it meanwhile, **is** built. ## Where the drawing and the server disagreed - **Three invented numbers** — "Rogue 6", "named in four approved summaries", "all 11 recorded sessions". None is on the wire. Followed the server: the character line reads pronouns and heard-as and stops; the erase says "every session this campaign has recorded" without a count. The same call `CampaignAnalytics.jsx` made for "3 h 38 m a session". - **Leaving.** The drawing gates it on `!gm` — only a player may leave. **#644 says the last GM may leave**, so it is gated on "not the owner" instead, and tested from both sides. A guard here would have quietly reverted a decision taken on purpose. - **The invite panel's hint** is hardcoded in the drawing; it depends on `join_requires_approval`, so there are two wordings. - **"All links and joining rules"** points at Settings → Invites, which this PR does not build. Omitted per the rollout rule; it comes back with CAMP-10. ## Where the drawing is wrong - **`aria-label="Add a member"` on a button reading "Add someone"** — the visible label is not inside the accessible name, a **WCAG 2.5.3** (label-in-name) failure, inherited from the old page's "+ Add" button which genuinely needed one. Dropped. The "Close" state keeps a label that contains its visible word. - **The lead's second sentence** — "A person's pronouns and their character's are separate facts, and this page keeps them apart." That is the page explaining itself on a surface people open constantly, which **D20** rules out. The lead is the count alone, generated from the data. One line to put back if you want it. - **No phone still exists for a member row, and the row did not survive one.** At 500 px the three controls took the line and `flex: 1; min-width: 0` wrapped the *person's name* mid-word, on the one page where that is unaffordable. Fixed with a container query giving the name and the actions a line each. **The render caught it; reading the CSS did not.** ## One default worth your eye The erase's **"Rewrite the summaries afterwards" defaults ON**, as all three drawn stills show (`aria-pressed="true"`); the old page defaulted it off. The caption calls it "the part people miss", and leaving a person named in GM-approved summaries defeats the erase. But it does mean the default run regenerates approved text. One line to flip if you would rather it were opt-in. ## Merges Two, both with a resolution git could not see: - **`AppFrame.test.jsx` auto-merged without a conflict and was wrong** — This week asserts Settings is absent from the Contents; CAMP-8's split is what puts it there. - `vocabulary.js` — `MEMBERS_AND_SETTINGS` retires the moment CAMP-8 has its own page, which its own comment said would happen. What is left behind `…/manage` is the nine Settings sections, so the entry is `SETTINGS`. ## Checks Frontend **1591 tests in 127 files**, green on the merged tree. Backend 289 passed across the member, ownership, character, erasure, invite and this-week suites; `ruff` clean. `lane2.py` prints with no unclassified rows and no classification changed. Rendered `lane2.html?only=camp8` in both themes, then served the built `dist` and screenshotted the **real page** with real data at desktop and 500 px — which is what found the wrapping bug. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Members & characters (CAMP-8) draws the person and the character on separate
lines, each with its own pronouns, and the whole reason #653 made it a page is
that one must never be read as the other. The character's have always been on
the wire; the person's were not, so the page had nothing to print for the
person but the character's — which is precisely the mistake it exists to
prevent.

So `MemberResponse` carries `pronouns`, read from the account, null when
somebody has not said. Nothing is inferred from a display name and nothing is
borrowed from the character below. `ThisWeekRsvp` already carries the same
field for the same reason (#765); this is the members list catching up.

Additive, and every member endpoint is built by `list_members`, so one change
covers the list, the add, the role change, the character patch and the
unlinked-member assignment. The bot reads `/api/bot/*` and not this, so no
contract version moves.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
CAMP-8, at `/campaigns/:id/members`, with its entry in the Contents. #656 took
it out of Settings, where only the GM could reach it, and gave it to everyone:
it is the page a player opens to remember who plays whom, which is why it is a
page and not a settings panel.

A row is a person — their name, their own pronouns, their role, the owner's
mark — and their characters hang under it with theirs. Every control is inline
on the row it affects, and one banner at the top says what did not happen when
something fails.

The rules it carries, rather than the layout:

- **D57.** The owner may step down as GM and keep the campaign, but only while
  another member is GM; with no other GM the control is absent rather than
  greyed on their own row on every visit. The server refuses it too, and that
  refusal is surfaced if the two ever disagree.
- **#644.** A campaign with no GM is a legitimate state. The last GM may leave
  and the owner may take the role away — neither is guarded, and a guard here
  would be the bug. Only the owner's own step-down is conditional.
- **D56.** The line under an empty search box says whether it can find anyone
  on the server or only people you have played with, because that changes what
  "no account found" means. Both searches — Add someone, and attaching an
  imported member — go through the one endpoint and so follow the one rule.
  When nobody is found the invite link is offered there, since somebody with no
  account is the one person a name search can never reach.
- **D20.** No paragraph explaining roles on the roster: the lead is a count,
  and the drawing's second sentence ("a person's pronouns and their character's
  are separate facts") is left out because it explains the page to itself on
  the surface a player opens most often. The character editor keeps its field
  help, which D20 exempts, and so do the one-off flows.
- **D12.** Erasing a member's recordings is the typed-name tier and says what
  it reaches before it asks; removing somebody, deleting a character and
  leaving are the stated-consequence tier.
- **D36.** The Foundry panel (`IS-272` to `IS-275`) is not drawn at all.
  Nothing in the backend fetches a Foundry actor, so there is no state in which
  it could work, and a target is drawn only when it can.

Two more things the drawing has and the wire has not: a character's class and
level, and the counts inside the erase confirmation. Both are left out rather
than invented.

The old campaign page is untouched, member list and all: it is the holding pen
for the parts of itself this lane has not replaced, and it goes with the last
of them.

The narrow render found one placement the drawing does not specify — it has no
phone still of a member row — and it matters here more than anywhere: at 500px
the three controls took the row and the *name* wrapped mid-word, so under 640px
the controls take a line of their own.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The stub's character name is a link when the import gave it one — the whole
point of attaching the stub to an account is that the pages come across with
it. The fixture had no entry id, so the branch that draws the link was
never exercised.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This week (#767) and Members & characters were built in parallel off the same
main, and both rearrange the campaign's Contents. Three resolutions:

- `useContents.js` — the campaign entries are This week, Sessions, Highlights,
  Storyline, **Members & characters** and **Settings**, in that order. Each
  side's comment said the other's page did not exist yet.
- `vocabulary.js` — `MEMBERS_AND_SETTINGS` ("Members & settings") is retired
  the moment CAMP-8 has a page of its own, which is what its own comment said
  would happen. What is left behind `…/manage` is the nine Settings sections,
  so the entry is `SETTINGS` and says so. The new-session form and the
  milestone timeline are behind the same door and deliberately unnamed by it:
  the sessions list and This week reach them directly, and a rail entry naming
  three things would describe the page's history rather than what a person
  wants from it.
- `AppFrame.test.jsx` — **auto-merged without a conflict and was wrong.** This
  week asserts Settings is absent from the Contents; CAMP-8's split is what
  puts it there. Both doors are now asserted, and Table rules & safety takes
  its place in the "drawn, not built" list.

Full frontend suite on the merged tree: 1591 tests in 127 files, green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Merge remote-tracking branch 'origin/build/lane3-thisweek' into build/lane3-members
All checks were successful
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m37s
CI / Backend lint (ruff) (pull_request) Successful in 1m4s
CI / Feedback site tests (pull_request) Successful in 1m35s
CI / Bot/backend version sync (pull_request) Successful in 47s
CI / Bot tests and audit (pull_request) Successful in 3m33s
CI / Frontend tests, audit, and build (pull_request) Successful in 5m1s
CI / Docker image build (pull_request) Successful in 3m52s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 4m14s
CI / Backend migration, tests, and audit (pull_request) Successful in 20m51s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 23m44s
347faae272
Sign in to join this conversation.
No description provided.