feat(webapp): a shareable invite link, a first-run checklist, and guides for the people who use Quest Board (phase 7a of v4.3.0) #530

Merged
claude-bot merged 3 commits from feat/phase7a-onboarding-and-docs into main 2026-09-06 07:37:13 +00:00
Contributor

Closes #387, closes #390. Phase 7a of the v4.3.0 build order (#514). No migration, no contract change.

#387 invite link and first-run checklist. New unauthenticated GET /api/invites/{code} returning {campaign_id, campaign_name, gm_display_name, member_count, archived} and nothing else, on the same security model as the public share token: the code is the secret, it is rate-limited (30/min), and a soft-deleted campaign's old code returns the same 404 as an unknown one, matching what #405 did to the join path. Archived resolves with archived: true because the link is real and the join behind it refuses for a statable reason. Joining reuses the existing authenticated POST /api/campaigns/join. /auth/login?next= gains exactly one new allowlisted shape, ^/join/[A-Za-z0-9_-]{1,64}$ (it feeds a redirect, so it stays an allowlist; five hostile inputs are pinned in tests). The public /join/:code page covers loading, bad code, archived, already a member, signed out and join; membership is checked from the signed-in user's own campaign list rather than exposed on the public preview. On the campaign page the 12 px disclosure is gone: Invite players in the member-list header offers the full URL with a copy button, the raw code and Regenerate, and the same panel is the "+ Add" search's empty state. The checklist (timezone, invite, Discord, first session) derives each item's done state from data the page already has, opens and focuses the relevant control, and is dismissed per campaign in localStorage. Two behaviour changes: players no longer see the invite code (GM action), and the member list's "+ Add" gained an aria-label because it collided with the milestone rail's.

#390 end-user guides. "Running your first campaign" and "Joining a campaign" live in the app as markdown under src/help/, rendered by a small HelpPage at the public routes /help/gm and /help/player (a player reads the second before they have an account), describing the product as it is on main now. Linked from the dashboard, the invite page and Login.jsx, whose tagline now names the real scope and whose button says "Sign in". docs/INTEGRATIONS.md already listed all eleven commands, so the work there is a bot test that parses the table and asserts it matches help.COMMANDS, the same source /help is built from. One honest caveat is in the GM guide: an instance with a registration-gate INVITE_CODE still refuses a brand-new player following a campaign link, since those are different secrets.

Backend full suite 2497 passed / 13 skipped; frontend 720 passed (61 files, +53); bot 433 passed; ruff, eslint (pre-existing warning only) and vite build clean.

🤖 Generated with Claude Code

Closes #387, closes #390. Phase 7a of the v4.3.0 build order (#514). No migration, no contract change. **#387 invite link and first-run checklist.** New unauthenticated `GET /api/invites/{code}` returning `{campaign_id, campaign_name, gm_display_name, member_count, archived}` and nothing else, on the same security model as the public share token: the code is the secret, it is rate-limited (30/min), and a soft-deleted campaign's old code returns the same 404 as an unknown one, matching what #405 did to the join path. Archived resolves with `archived: true` because the link is real and the join behind it refuses for a statable reason. Joining reuses the existing authenticated `POST /api/campaigns/join`. `/auth/login?next=` gains exactly one new allowlisted shape, `^/join/[A-Za-z0-9_-]{1,64}$` (it feeds a redirect, so it stays an allowlist; five hostile inputs are pinned in tests). The public `/join/:code` page covers loading, bad code, archived, already a member, signed out and join; membership is checked from the signed-in user's own campaign list rather than exposed on the public preview. On the campaign page the 12 px disclosure is gone: **Invite players** in the member-list header offers the full URL with a copy button, the raw code and Regenerate, and the same panel is the "+ Add" search's empty state. The checklist (timezone, invite, Discord, first session) derives each item's done state from data the page already has, opens and focuses the relevant control, and is dismissed per campaign in `localStorage`. Two behaviour changes: players no longer see the invite code (GM action), and the member list's "+ Add" gained an `aria-label` because it collided with the milestone rail's. **#390 end-user guides.** "Running your first campaign" and "Joining a campaign" live in the app as markdown under `src/help/`, rendered by a small `HelpPage` at the public routes `/help/gm` and `/help/player` (a player reads the second before they have an account), describing the product as it is on main now. Linked from the dashboard, the invite page and `Login.jsx`, whose tagline now names the real scope and whose button says "Sign in". `docs/INTEGRATIONS.md` already listed all eleven commands, so the work there is a bot test that parses the table and asserts it matches `help.COMMANDS`, the same source `/help` is built from. One honest caveat is in the GM guide: an instance with a registration-gate `INVITE_CODE` still refuses a brand-new player following a campaign link, since those are different secrets. Backend full suite 2497 passed / 13 skipped; frontend 720 passed (61 files, +53); bot 433 passed; ruff, eslint (pre-existing warning only) and `vite build` clean. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A GM creating their first campaign had to invite players and wire up Discord
with no prompting at all, and the invite itself was the worst-hidden thing on
the page: a 12px "▼ Show invite code" disclosure, below the description and the
Archive/Delete row, offering eleven raw characters with no instructions and
nowhere to paste them that did anything. The "+ Add" search next to the member
list only matched people who already had an account, and answered "No users
found." for the one person you were most likely to be looking for.

Three changes, in the order a GM meets them.

**A link, not a code.** New `GET /api/invites/{code}` resolves an invite code to
`{campaign_id, campaign_name, gm_display_name, member_count, archived}` and
nothing else. It is unauthenticated — the only such route under `/api` — because
the reader of `/join/<code>` most likely has no account yet and deserves to know
what they are being asked to sign up for. Same model as the public analytics
share token: the code is the secret, a wrong one is 404, a soft-deleted
campaign's old code is the *same* 404 so it cannot confirm the campaign ever
existed (#405), and the whole thing is rate-limited so the code space cannot be
walked. Joining still goes through the existing authenticated `POST
/api/campaigns/join`, with its existing archived guard and already-a-member
answer; no new write path, no migration.

`/auth/login?next=` grows one allowlisted shape, `^/join/[A-Za-z0-9_-]+$`, so a
signed-out visitor comes back to the invite instead of landing on a dashboard
that never mentions which campaign they were invited to. It stays an allowlist
rather than a same-origin check, because the value is handed straight to
RedirectResponse.

**The invite control moved to where inviting happens.** "Invite players" now
sits in the member-list header (GM only) and offers the full
`${origin}/join/<code>` URL with a copy button, the raw code underneath for when
a link will not survive the trip, and Regenerate. The same control is the
"+ Add" search's empty state, which now reads "No account found. Share the
invite link instead" rather than dead-ending.

**A first-run checklist.** Four items — timezone, invite, Discord, first session
— each deriving "done" from data the campaign page already has, so there is no
server-side checklist state to drift and a GM who set everything up last year
never sees the card. Each item navigates to the control that completes it, not
near it: three of the four live behind a disclosure, so opening the disclosure
is part of the navigation. Dismissal is per campaign in localStorage, read
through try/catch, and the component is keyed by campaign id because this page
is not remounted when the route param changes.

Tests: 21 backend (resolve known / unknown / deleted / archived, the payload's
exact shape, founding GM vs later co-GM, the join and its idempotence, and the
`next` allowlist against five hostile inputs), 31 frontend across the Join page,
the invite panel and the checklist, plus 12 on the campaign page.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
docs(webapp): guides for the people who use Quest Board, not just run it (#390)
Some checks failed
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Backend lint (ruff) (pull_request) Successful in 56s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m37s
CI / Frontend tests, audit, and build (pull_request) Successful in 2m14s
CI / Bot tests and audit (pull_request) Successful in 2m46s
CI / Backend migration, tests, and audit (pull_request) Failing after 4m53s
CI / Docker image build (pull_request) Successful in 5m1s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
0120f79860
Everything written down about Quest Board was for whoever installs it —
OPERATIONS, DEVELOPMENT, INTEGRATIONS, API. A GM opening a campaign for the
first time, or a player handed an invite link, had the interface and nothing
else, and the sign-in page they arrived on said "TTRPG session scheduling",
which undersells the product by about four fifths, above a button reading "Sign
in with SSO", which names the mechanism rather than the act.

Two guides, "Running your first campaign" and "Joining a campaign", written for
the reader rather than the maintainer and describing the app as it is now — the
nav bar, "Needs you", RSVP, the `/join/<code>` links from #387, the Prep lane,
the post-session checklist, `/help` and `/record start`'s session picker.

They live in the app rather than in `docs/`, as markdown under
`src/help/`, imported with Vite's `?raw` and rendered by a small `HelpPage` at
`/help/gm` and `/help/player`. Three reasons: they are one click from where the
question is asked, they version with the features they describe rather than
drifting in a directory nobody reading them can see, and they ship in the bundle
rather than being fetched — a help page that fails to load is a poor kind of
help. `HelpPage` is lazy, so react-markdown does not enter the entry chunk for
everyone who never opens it, and it carries its own prose styling rather than
reusing the wiki article's renderer, which is built around `[[wiki link]]`
resolution and needs a campaign id.

The routes are **public**. That is the point rather than an oversight: the
reader who most needs "Joining a campaign" has not signed up yet, so the sign-in
page and the invite page can both link to it and have the link work.

Linked from one place in the app proper — a quiet Help line under the campaign
list on the dashboard — plus the invite page and the login page. The login
tagline now names the real scope, and the button says "Sign in"; there is no
configured provider name to put next to it, so it stays unadorned rather than
inventing one.

`docs/INTEGRATIONS.md` already listed all eleven commands, so the fix there is
to stop it drifting again: a bot-suite test parses the Discord Commands table
and asserts it matches `help.COMMANDS`, which is the same source of truth the
`/help` embed is built from. Both it and the README now point at the in-app
guides so a maintainer knows where the user-facing writing lives.

Tests: 7 for HelpPage (both guides render their headings, the index, an unknown
guide, in-app links stay in the router, and the imported markdown is not empty),
3 more on Login, 1 on Dashboard, 1 on the bot's help cog.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
claude-bot scheduled this pull request to auto merge when all checks succeed 2026-09-06 07:07:22 +00:00
test(webapp): say when the later co-GM joined, and break joined_at ties deterministically (#387)
Some checks failed
CI / Bot/backend version sync (pull_request) Successful in 41s
CI / Docker image build (pull_request) Successful in 44s
CI / Backend lint (ruff) (pull_request) Failing after 46s
CI / Bot tests and audit (pull_request) Has been cancelled
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Has been cancelled
c1326c2745
Both memberships in the test got the same transaction-time now(), so the
"founding GM" ordering was decided by a random uuid and CI picked the wrong
one. The test now dates the later GM explicitly, and the query tie-breaks on
user_id so a genuine tie never flips between requests.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
rbrooks force-pushed feat/phase7a-onboarding-and-docs from c1326c2745
Some checks failed
CI / Bot/backend version sync (pull_request) Successful in 41s
CI / Docker image build (pull_request) Successful in 44s
CI / Backend lint (ruff) (pull_request) Failing after 46s
CI / Bot tests and audit (pull_request) Has been cancelled
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been cancelled
CI / Backend migration, tests, and audit (pull_request) Has been cancelled
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Has been cancelled
to be59157518
All checks were successful
CI / Bot/backend version sync (pull_request) Successful in 21s
CI / Backend lint (ruff) (pull_request) Successful in 36s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 1m14s
CI / Bot tests and audit (pull_request) Successful in 1m44s
CI / Frontend tests, audit, and build (pull_request) Successful in 1m55s
CI / Docker image build (pull_request) Successful in 4m28s
CI / Backend migration, tests, and audit (pull_request) Successful in 9m26s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Successful in 17m29s
2026-09-06 07:19:32 +00:00
Compare
claude-bot deleted branch feat/phase7a-onboarding-and-docs 2026-09-06 07:37:13 +00:00
Sign in to join this conversation.
No description provided.