[Frontend] Add GM onboarding: a first-run checklist and a shareable invite link #387
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
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?
Impact: HIGH
Found in the August 2026 session lifecycle review (#319).
What the user experiences
A GM creating their first campaign has to complete two hard, undiscoverable steps entirely alone: inviting players and wiring up Discord. The invite code — the only self-serve way to add a player who doesn't already have an account — is behind a collapsed 12px "▼ Show invite code" disclosure, below the campaign description and the Archive/Delete row. There is no shareable invite link, only a raw code the GM must copy and explain out-of-band. The "+ Add" search box only searches existing accounts and shows "No users found." with no fallback mention of the invite code, so a GM whose players have never signed in hits a dead end. None of this is prompted anywhere — a GM can create a campaign, invite nobody, never wire up Discord, and schedule sessions, and the app never mentions either step is available.
Evidence
webapp/frontend/src/pages/CampaignDetail.jsx:1387-1392— invite code behind a collapsed 12px disclosure, positioned below the info/action row and above the member list.webapp/frontend/src/pages/CampaignDetail.jsx:1395-1403— a raw code to copy, no shareable link.webapp/frontend/src/pages/CampaignDetail.jsx:1425-1430,1479— "+ Add" searches existing accounts only (searchNonMembers) and shows "No users found." with no fallback to the invite code.webapp/frontend/src/api/campaigns.js/ route table — no route exists today for a self-serve "you've been invited to Campaign" landing page.Why it matters for a hosted product
Step 2 (invite players) and step 3 (wire up Discord — covered by other issues in this milestone) are exactly the two steps a hosted customer must complete alone on day one, and today both are hidden behind disclosures with no prompting at all. This is, per the audit, the single highest-leverage missing component in the whole product.
Proposed fix
Add a
https://…/join/<code>URL that lands on a "You've been invited to Campaign" page (sign in, then auto-join), removing the hardest step of GM onboarding. Add a dismissible first-run checklist card on a new campaign: set a timezone · invite players · connect Discord · schedule your first session — each item currently hidden behind a disclosure, buried in the Edit form, or not mentioned at all. Have the "+ Add" empty state ("No users found.") mention the invite link/code as a fallback. This is the audit's M1 and M2.Acceptance criteria
/join/<code>URL exists that lands new or existing users on an invite-acceptance page and joins them to the campaign on confirmation.Picking this up as v4.3.0 phase 7 (#514), in parallel with #377 since they share no files. Decisions: a small unauthenticated code-resolution endpoint (the code is the secret; wrong code is 404; rate-limited) behind a public
/join/<code>page that signs the visitor in and returns them to the same URL, then joins through the existing path; the invite disclosure becomes an "Invite players" control in the member list header with the full link and a copy button; the member search's empty state offers the link; a dismissible first-run checklist for GMs whose items derive their done state from campaign data (timezone, members, Discord, first session), dismissed per campaign in local storage so no migration is needed.Done in the phase 7a PR (auto-merging on green); ships with v4.3.0.
The link. New unauthenticated
GET /api/invites/{code}returns{campaign_id, campaign_name, gm_display_name, member_count, archived}and nothing else. Same security model as the public analytics share token: the code is the secret, so it is rate-limited (30/min), and a soft-deleted campaign's old code gives the same 404 as an unknown one, matching what #405 did to the join path. An archived campaign resolves witharchived: truerather than 404, because the link is real and "this campaign is archived" beats "no such invite". Joining still goes through the existing authenticated join endpoint; no new write path, no migration./auth/login?next=needed one new allowlisted shape,^/join/[A-Za-z0-9_-]{1,64}$, or a signed-out visitor could never be returned to the invite. It stays an allowlist rather than a same-origin check because the value is handed straight to a redirect; five hostile inputs are pinned in tests.The UI.
/join/:codeis public and outside the auth shell, and 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 added to the public preview, since putting it there would make an invite code a way to ask "is this person in that campaign?"; the backend's already-a-member 400 is folded into the same state, so the race is covered.On the campaign page, the "▼ Show invite code" disclosure is gone. Invite players sits in the member-list header and offers the full
/join/<code>URL with a copy button, the raw code underneath, and Regenerate. The same panel is the "+ Add" search's empty state, which reads "No account found. Share the invite link instead" instead of dead-ending at "No users found."The checklist. Four items (timezone, invite, Discord, first session), each deriving "done" from data the page already has, so nothing server-side can drift and a GM who set everything up last year never sees it. Each item opens the disclosure its control lives behind and focuses the field. Dismissal is per campaign in local storage, keyed by campaign id because the page is not remounted when the route param changes.
Two behaviour changes worth knowing: players can no longer see the invite code (it is a GM action now), and the member list's "+ Add" gained an
aria-label, since it collided with the milestone rail's button of the same name. Tests: 21 backend, 43 frontend.