Lane 10: Discord pickers for the server, channel and voice channel (D226, #736) #904

Merged
claude-bot merged 7 commits from build/lane10-pickers into main 2026-09-28 15:44:35 +00:00
Collaborator

Part of #736 (lane 10). This builds D226, the owner's choice: pickers for all three fields, listing only the Discord servers the GM belongs to, through their own linked Discord, and only channels the bot can see. Without a link, you type the ID as before.

Bot (additive; the contract stays at v2)

  • GET /discord/guilds?user_id= returns the guilds the bot shares with that Discord user. It never includes a guild the user isn't in.
  • GET /discord/channels?user_id=&guild_id= first re-checks that the user is a member of that guild and returns 404 if not. It then lists the text and voice channels the bot can view_channel.
  • Both are bot-auth only, with tests covering 401 and 400, "shared guilds only", and a 404 for a guild the bot is in but the user isn't.

Backend

  • Routes: GET /campaigns/{id}/discord-guilds and …/discord-guilds/{guild_id}/channels, both require_gm.
  • No Discord id is ever accepted from the client. The route resolves the GM's own verified link server-side. The guild_id in the path is untrusted, and the bot re-validates it.
  • Responses:
    • no link, or an unverified one, returns {linked: false} without calling the bot;
    • no bot configured, or a bot too old for these routes, returns 502;
    • the bot's 404 passes through as 404.
  • Tests (12): a player gets 403, a GM of another campaign gets 403, the GM's own id is forwarded, plus each state above.

The page (Settings → Discord)

  • Three pickers once a link is present and servers are listed. A "Type IDs instead" link switches to typed fields.
  • A "gone" server or channel (one saved but no longer visible) stays selected with a warning. Its channels fall back to typed fields.
  • The voice channel field is added. D159's backend already had it; the page didn't.
  • The e2e fake bot learns both routes, with a fixed fictional fixture and the real bot's 404 rule, so lane 10's journeys can use them.

Render check (real stack)

  • Playwright ran in the stack's container at a true 390×844 and at 1440, in light and dark. Every shot passes scrollWidth <= innerWidth. I looked at them myself.
  • States: linked with pickers, a gone server, the bot unreachable (fake bot stopped), and unlinked.
  • One real fault fixed: a gone server tried to fetch its channels and stacked a second, contradictory error banner.

Checks

  • Bot suite: 668 passed.
  • Backend (targeted): 160 passed.
  • vitest: 2686 passed. eslint: 0 errors.
  • ruff: clean. Version sync: OK.
  • Changelog: a changelog.d/ fragment.

🤖 Generated with Claude Code

Part of #736 (lane 10). This builds **D226**, the owner's choice: pickers for all three fields, listing **only the Discord servers the GM belongs to, through their own linked Discord, and only channels the bot can see.** Without a link, you type the ID as before. ## Bot (additive; the contract stays at v2) - **`GET /discord/guilds?user_id=`** returns the guilds the bot shares with that Discord user. It never includes a guild the user isn't in. - **`GET /discord/channels?user_id=&guild_id=`** first **re-checks that the user is a member of that guild** and returns 404 if not. It then lists the text and voice channels the bot can `view_channel`. - Both are bot-auth only, with tests covering 401 and 400, "shared guilds only", and a 404 for a guild the bot is in but the user isn't. ## Backend - **Routes:** `GET /campaigns/{id}/discord-guilds` and `…/discord-guilds/{guild_id}/channels`, both **`require_gm`**. - **No Discord id is ever accepted from the client.** The route resolves the GM's own **verified** link server-side. The `guild_id` in the path is untrusted, and the bot re-validates it. - **Responses:** - no link, or an unverified one, returns `{linked: false}` without calling the bot; - no bot configured, or a bot too old for these routes, returns 502; - the bot's 404 passes through as 404. - **Tests** (12): a player gets 403, a GM of another campaign gets 403, the GM's own id is forwarded, plus each state above. ## The page (Settings → Discord) - **Three pickers** once a link is present and servers are listed. A **"Type IDs instead"** link switches to typed fields. - **A "gone" server or channel** (one saved but no longer visible) stays selected with a warning. Its channels fall back to typed fields. - **The voice channel field** is added. D159's backend already had it; the page didn't. - **The e2e fake bot** learns both routes, with a fixed fictional fixture and the real bot's 404 rule, so lane 10's journeys can use them. ## Render check (real stack) - Playwright ran in the stack's container at a true 390×844 and at 1440, in light and dark. Every shot passes `scrollWidth <= innerWidth`. I looked at them myself. - **States:** linked with pickers, a gone server, the bot unreachable (fake bot stopped), and unlinked. - **One real fault fixed:** a gone server tried to fetch its channels and stacked a second, contradictory error banner. ## Checks - **Bot suite:** 668 passed. - **Backend (targeted):** 160 passed. - **vitest:** 2686 passed. **eslint:** 0 errors. - **ruff:** clean. **Version sync:** OK. - **Changelog:** a `changelog.d/` fragment. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
GET /discord/guilds and GET /discord/channels back Settings -> Discord's
server/channel/voice-channel pickers. Both are bot-auth, internal HTTP like
/test-channel and /record/test -- additive, BOT_CONTRACT_VERSION stays 2.
Neither ever infers a Discord user id itself: the caller (the backend, from
that user's own verified link) supplies it, and both routes re-check guild
membership for that exact user before answering, so a guild the bot is in
but this Discord account isn't a member of is a 404, never a leak.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
GET /api/campaigns/{id}/discord-guilds and .../discord-guilds/{guild_id}/channels
back Settings -> Discord's three pickers. Gated by require_gm, so a player
or a GM of another campaign is refused (403) before the body runs. The only
Discord id these routes ever ask the bot about is the signed-in GM's own
verified platform_links row -- never anything from the client -- and an
unlinked or unverified GM gets {linked: false} with no bot call at all,
matching the typed-ID fallback. A guild_id the client sends is not trusted
either: the bot re-checks that guild against the GM's own Discord id and a
refusal there passes straight through as 404.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Server, channel and voice channel each become a <select> once the
signed-in GM's own Discord is linked and the bot has answered
GET /api/campaigns/{id}/discord-guilds and .../discord-guilds/{guild}/channels.
Every other case -- no verified link, the bot unreachable, the bot in
none of this GM's servers, or a GM who simply clicks "Type IDs instead"
-- falls back to the typed-ID fields the page already had. A stored id
no longer in the bot's answer (deleted in Discord, or hidden from the
bot) stays selected as its own option with a "can't see it any more"
warning (IS-458), rather than silently reading as "none".

Also adds the voice channel field this page never had (D159's backend
support landed earlier without a frontend to go with it).

CampaignSettingsDiscord.test.jsx: existing tests default fetchDiscordGuilds
to {linked: false}, which is also the typed-ID-fallback scenario they were
already describing, so nothing about them changed in substance. Added
coverage for the linked/picker, gone, nobot, unreachable and manual-toggle
states.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
fake_bot.py grows GET /discord/guilds and GET /discord/channels, answering
from a small fixed table keyed by Discord user id and 404ing a guild that
table doesn't list for that user -- mirroring the real bot's guild-membership
re-check, and letting lane 10's journeys PR exercise the pickers later.
Documented in e2e/README.md alongside the fakes it already covers.

Render-checked Settings -> Discord as Dana in the e2e stack (linked with a
saved pick, a "gone" stored server, the bot unreachable, and unlinked with
the typed-ID fallback), at 390x844 and 1440x900, light and dark -- all
sixteen shots match lane8.html's #voice states with no horizontal overflow.

That check found a real bug: a "gone" server (one the picker doesn't list)
still tried to fetch its channels, which 404s and showed a second, unrelated
"could not ask the bot" banner alongside the server's own "can't see it any
more" warning. CampaignSettingsDiscord.jsx now gates the channel/voice-channel
fetch on the chosen guild actually being one the picker listed (`guildKnown`),
so an unrecognised server falls straight back to the typed channel/voice
fields instead of retrying a fetch that can only fail the same way again.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Merge main into build/lane10-pickers
Some checks failed
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 59s
CI / Backend lint (ruff) (pull_request) Successful in 30s
CI / Bot tests and audit (pull_request) Successful in 1m59s
CI / Bot/backend version sync (pull_request) Successful in 25s
CI / Feedback site tests (pull_request) Successful in 45s
CI / Does this change touch the recording path? (pull_request) Successful in 20s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Docker image build (pull_request) Successful in 1m10s
CI / Backend migration, tests, and audit (pull_request) Successful in 20m11s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Failing after 21m42s
CI / Frontend tests, audit, and build (pull_request) Has been cancelled
9bf9ef12ee
# Conflicts:
#	webapp/backend/app/routers/campaigns.py
D226's page adds a Voice channel field, so a substring match on "Channel"
found two textboxes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Merge main into build/lane10-pickers
All checks were successful
CI / Backend lint (ruff) (pull_request) Successful in 1m20s
CI / Bot tests and audit (pull_request) Successful in 2m2s
CI / Summarisation accuracy eval harness (stub provider) (pull_request) Successful in 3m4s
CI / Bot/backend version sync (pull_request) Successful in 31s
CI / Does this change touch the recording path? (pull_request) Successful in 21s
CI / Synthetic session harness (no GPU, no LLM) (pull_request) Has been skipped
CI / Feedback site tests (pull_request) Successful in 2m1s
CI / Docker image build (pull_request) Successful in 1m18s
CI / Backend migration, tests, and audit (pull_request) Successful in 20m43s
CI / End-to-end journeys (desktop, phone, keyboard-only) (pull_request) Successful in 35m50s
CI / Frontend tests, audit, and build (pull_request) Successful in 45m30s
9baacd527b
Sign in to join this conversation.
No description provided.