Discord admin page: servers, radio channels, radio bots (server-26#213 item 2)

Frontend, built from design-canvas/discord-system-mockup.html ("DRB admin"):
- Settings > Discord (/settings/discord, platform admin only; the tab is
  hidden from org owners because every route behind it is admin-only).
  KPIs + tabs:
  * Radio channels: every discord_streams doc, desired -> actual, the node's
    real error, red after 2 min desired-on without connecting. Stop
    (POST /discord/guilds/{id}/stop), Start for a stopped channel and
    "+ Add radio channel" (POST /discord/streams), with the invite links a
    "no bot in this server" refusal carries.
  * Servers: linked servers plus servers seen running radio while unlinked
    (acting for DISCORD_UNLINKED_GUILD_ORG_ID); Link / Change org
    (PUT /discord/guilds/{id}/link).
  * Radio bots: valid / check_error, guild membership with a per-server
    invite link for each server the bot is missing from, lease (node,
    since, releasing), fan-out, Refresh (POST /tokens/{id}/refresh),
    Remove, Release all leases (POST /tokens/flush), add with optional BYO org.
  * How it works.
- /tokens now renders the same radio-bot table (new token fields + refresh).
- Left out, no backend yet: alert routes, incident forum/roles/plan columns,
  per-channel talkgroup chips, "Generate link code" (item 1).

Backend (routers/discord.py):
- GET /discord/admin/overview (platform admin): guilds, streams with node
  and bot names, orgs, systems, limits. No token strings, no Discord user
  ids, no linking-admin uid.
- PUT /discord/guilds/{id}/link: digits-only id, optional guild_name label,
  and re-linking a server to a different org stops its live channels
  (the reconciler would otherwise keep re-placing them on the old org's
  nodes, using its stored org_id).
- POST /discord/streams: guild_id/channel_id must be digits (the web form
  types them in; the bot always sent snowflakes).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-04 20:05:35 -04:00
co-authored by Claude Opus 5.5
parent e574bd924a
commit d3277c13e1
11 changed files with 1464 additions and 161 deletions
+5 -1
View File
@@ -14,6 +14,10 @@ const TABS = [
{ href: "/settings/billing", label: "Billing" },
];
// Platform-admin-only tabs: every route behind them is require_admin_token,
// so an org owner would only see 403s (server-26#213 item 2).
const ADMIN_TABS = [{ href: "/settings/discord", label: "Discord" }];
export default function SettingsLayout({ children }: { children: React.ReactNode }) {
// SAAS_PLAN.md B7: this used to gate on isAdmin (platform admin) alone,
// which meant a paying customer who is their own org's owner couldn't
@@ -40,7 +44,7 @@ export default function SettingsLayout({ children }: { children: React.ReactNode
/>
<div className="flex flex-wrap gap-1 bg-gray-900 border border-gray-800 rounded-lg p-1 w-fit max-w-full overflow-x-auto">
{TABS.map((t) => (
{[...TABS, ...(isAdmin ? ADMIN_TABS : [])].map((t) => (
<Link
key={t.href}
href={t.href}