- discord_oauth: POST /discord/oauth/start and GET /options take an optional
system_id. The system must be the org's own or shared (another org's or
unknown = 404). Its preferred bot is strict: one that can't connect is a
404, never a quiet swap to Automatic. No preference = the usual order.
The picker's "bot for a system" now always sends system_id.
- #257: _channel_state gives a bot whose token Discord rejected its own
"invalid" state; the picker shows "Radio bot needs replacing" with no
invite links, instead of "No radio bot".
- #256: Start no longer stays disabled on "Needs fix". It re-checks with
Discord first (refresh=1) and starts only if the fresh answer allows it.
"Not a voice channel" still disables Start.
- #258: own tooltip for "Not a voice channel"; "platform admin" -> "DRB
support"; PreferredBotsCard says plainly that adding your own bot isn't
self-serve (no support address exists yet, see #47), and a load failure
gets a plain message + Try again instead of the raw C2 error.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- complete: S6 returns streams_stopped, not stopped — the "N radio
channels were stopped" note never showed.
- start: bot name is nested as bot.name.
- "The bot for a system" with an org-owned preferred bot now sends
token_id (S6 accepts it today); system_id only for a platform-pinned
pool bot, which still needs S6 to accept system_id.
- A 404 for one chosen bot no longer hides Connect and the picker
(the message told the user to try Automatic, but the button was gone).
- Server count uses GET /tokens' per-bot server_cap when present.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- /discord/connected: Discord's OAuth redirect target. Stashes code/state
in sessionStorage, strips them from the URL (replaceState), no-referrer
(metadata + response header), sends a signed-out user to
/login?next=/discord/connected and resumes, access_denied = "Nothing
changed", result card "Linked X: N ready, M need a fix".
- middleware.ts: the callback path joins NO_SESSION_COOKIE_GATE (plan
finding 2). Also needed for signed-in users: drb_session is
SameSite=Strict, so it is not sent on the cross-site redirect back from
discord.com.
- /login honours ?next= for an allowlist of one path (no open redirect).
- DiscordServersCard: Connect is the primary action with a radio-bot
picker (automatic / the bot for a system / the org's own bots); the
/drb link code moves to a collapsed secondary option and opens itself
when /discord/oauth/start answers 404.
- RadioBotsPanel: write-only client-secret field per bot (PUT/DELETE
/tokens/{id}/oauth), "Connect on/off", "N of 100 servers" with the
pool cap (90) warning.
- lib/discordOAuth.ts: the S6 client, kept out of c2api.ts; only ever
navigates to a discord.com /oauth2/authorize URL.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>