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
Loading…
Reference in a new issue
No description provided.
Delete branch "feat/phase7a-onboarding-and-docs"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
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 witharchived: truebecause the link is real and the join behind it refuses for a statable reason. Joining reuses the existing authenticatedPOST /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/:codepage 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 inlocalStorage. Two behaviour changes: players no longer see the invite code (GM action), and the member list's "+ Add" gained anaria-labelbecause 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 smallHelpPageat the public routes/help/gmand/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 andLogin.jsx, whose tagline now names the real scope and whose button says "Sign in".docs/INTEGRATIONS.mdalready listed all eleven commands, so the work there is a bot test that parses the table and asserts it matcheshelp.COMMANDS, the same source/helpis built from. One honest caveat is in the GM guide: an instance with a registration-gateINVITE_CODEstill 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 buildclean.🤖 Generated with 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>c1326c2745be59157518