Discord admin page + hide admin-only bot pool from operators (#213 items 2, 4) #219

Merged
logan merged 3 commits from feat/213-discord-admin into main 2026-10-04 22:05:36 -04:00
Owner

Refs #213 (items 4 and 2). Does not close it: items 1 (/drb link + web code generation) and 3 (node page "Join Discord" -> stream) are sibling slices.

Pairs with: the sibling PRs for #213 item 1 (self-serve link code) and item 3 (node-page Join Discord). The "Generate link code" button from the mockup is deliberately absent here; item 1's PR should add it to the Servers tab (drb-frontend/components/discord/ServersPanel.tsx, action prop of the Panel).

Commit 1 - regression (#213 item 4)

GET /tokens and PUT /tokens/{id}/prefer became platform-admin only in #216. Fixed the three places that still offered them to operators:

  • Systems page "Preferred Bot Token" panel: admins only. No non-admin listing endpoint added: the save behind the picker is admin-only too, so a list would only feed a control that 403s on save (a dead CTA). The panel also stopped turning a fetch error into a false "No tokens in pool."
  • /tokens: admin only (operators go to /network).
  • /network hub: Bot Tokens card admin only.

Commit 2 - Discord admin page (#213 item 2)

Settings > Discord (/settings/discord), built from design-canvas/discord-system-mockup.html "DRB admin". Platform admin only (tab hidden from org owners; every route behind it is require_admin_token). Polls every 10 s while visible.

  • Radio channels: every discord_streams doc, desired -> actual, the node's reported error, red after 2 min desired-on without connecting. Stop -> POST /discord/guilds/{id}/stop; Start (stopped row) and + Add radio channel -> POST /discord/streams; a "no bot in this server" refusal renders its invite links.
  • Servers: linked servers plus servers seen running radio while unlinked (acting for DISCORD_UNLINKED_GUILD_ORG_ID, shown as a warning). Link / Change org -> PUT /discord/guilds/{id}/link.
  • Radio bots: valid / check_error / checked-at, guild membership with a dashed per-server invite chip for each server the bot is missing from, invite_url, lease (node, since, releasing), fan-out vs DISCORD_NODE_MAX_STREAMS. Refresh -> POST /tokens/{id}/refresh, Remove, Release all leases -> POST /tokens/flush, add with optional BYO org.
  • How it works (static).
  • /tokens now renders the same radio-bot table.

Left out because there is no backend (not shown as dead controls): Alert routes tab, incident forum / roles / plan columns, per-channel talkgroup chips, "Alerts today" KPI (replaced by "Bots needing a check").

Backend (routers/discord.py):

  • GET /discord/admin/overview (platform admin): guilds, streams with node/bot names, orgs, systems, limits. No token strings, no Discord user ids, no linking-admin uid (tested).
  • 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. Without that, the reconciler keeps re-placing them with the stored old org_id, so one org's nodes keep serving a server that now belongs to another.
  • POST /discord/streams: guild_id/channel_id must be digits (the web form types them; the bot always sent snowflakes).

Person names: none shown. Labels are server/channel/bot/org names; Discord user ids are dropped server-side.

Verification (real runs, this branch)

  • ~/venvs/drb-5c/bin/python -m pytest -q in drb-c2-core: 613 passed, 4 warnings in 25.96s (new: tests/test_discord_admin.py, 5 tests)
  • tsc --noEmit (TypeScript 5.9.3, Node v20.20.2) on drb-frontend: tsc exit 0
  • Not done: no browser run of the page (no Firebase config on this box), no drb-qa-review pass.

🤖 Generated with Claude Code

Refs #213 (items 4 and 2). Does not close it: items 1 (`/drb link` + web code generation) and 3 (node page "Join Discord" -> stream) are sibling slices. **Pairs with:** the sibling PRs for #213 item 1 (self-serve link code) and item 3 (node-page Join Discord). The "Generate link code" button from the mockup is deliberately absent here; item 1's PR should add it to the Servers tab (`drb-frontend/components/discord/ServersPanel.tsx`, `action` prop of the Panel). ## Commit 1 - regression (#213 item 4) `GET /tokens` and `PUT /tokens/{id}/prefer` became platform-admin only in #216. Fixed the three places that still offered them to operators: - Systems page "Preferred Bot Token" panel: admins only. **No non-admin listing endpoint added**: the save behind the picker is admin-only too, so a list would only feed a control that 403s on save (a dead CTA). The panel also stopped turning a fetch error into a false "No tokens in pool." - `/tokens`: admin only (operators go to `/network`). - `/network` hub: Bot Tokens card admin only. ## Commit 2 - Discord admin page (#213 item 2) **Settings > Discord** (`/settings/discord`), built from `design-canvas/discord-system-mockup.html` "DRB admin". Platform admin only (tab hidden from org owners; every route behind it is `require_admin_token`). Polls every 10 s while visible. - **Radio channels**: every `discord_streams` doc, desired -> actual, the node's reported error, red after 2 min desired-on without connecting. Stop -> `POST /discord/guilds/{id}/stop`; Start (stopped row) and **+ Add radio channel** -> `POST /discord/streams`; a "no bot in this server" refusal renders its invite links. - **Servers**: linked servers plus servers seen running radio while unlinked (acting for `DISCORD_UNLINKED_GUILD_ORG_ID`, shown as a warning). Link / Change org -> `PUT /discord/guilds/{id}/link`. - **Radio bots**: `valid` / `check_error` / checked-at, guild membership with a dashed per-server invite chip for each server the bot is missing from, `invite_url`, lease (node, since, releasing), fan-out vs `DISCORD_NODE_MAX_STREAMS`. Refresh -> `POST /tokens/{id}/refresh`, Remove, Release all leases -> `POST /tokens/flush`, add with optional BYO org. - **How it works** (static). - `/tokens` now renders the same radio-bot table. Left out because there is no backend (not shown as dead controls): Alert routes tab, incident forum / roles / plan columns, per-channel talkgroup chips, "Alerts today" KPI (replaced by "Bots needing a check"). Backend (`routers/discord.py`): - `GET /discord/admin/overview` (platform admin): guilds, streams with node/bot names, orgs, systems, limits. No token strings, no Discord user ids, no linking-admin uid (tested). - `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**. Without that, the reconciler keeps re-placing them with the stored old `org_id`, so one org's nodes keep serving a server that now belongs to another. - `POST /discord/streams`: `guild_id`/`channel_id` must be digits (the web form types them; the bot always sent snowflakes). Person names: none shown. Labels are server/channel/bot/org names; Discord user ids are dropped server-side. ## Verification (real runs, this branch) - `~/venvs/drb-5c/bin/python -m pytest -q` in drb-c2-core: `613 passed, 4 warnings in 25.96s` (new: `tests/test_discord_admin.py`, 5 tests) - `tsc --noEmit` (TypeScript 5.9.3, Node v20.20.2) on drb-frontend: `tsc exit 0` - Not done: no browser run of the page (no Firebase config on this box), no `drb-qa-review` pass. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
logan added 2 commits 2026-10-04 20:07:21 -04:00
GET /tokens and PUT /tokens/{id}/prefer became platform-admin only in #216
(server-26#204 item 6). Three frontend surfaces still offered them to
operators:

- systems page "Preferred Bot Token" panel: now rendered for admins only.
  No non-admin listing endpoint was added: the write behind the picker is
  admin-only too, so a list would only feed a control that 403s on save.
  The panel also no longer turns a fetch error into "No tokens in pool."
- /tokens page: admin-only (operators are sent to /network).
- /network hub: the Bot Tokens card is shown to admins only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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>
logan added 1 commit 2026-10-04 21:36:05 -04:00
PR #218 (server-26#213 item 1) unlinks a server by clearing org_id and
stamping unlinked_at, and resolve_guild_org then refuses the fallback org.
The overview showed such a server as "Unlinked · fallback org" and offered it
in Add radio channel, where start_stream would 403. Mirror the rule here
(forward-compatible: unlinked_at does not exist on main yet). Also key each
stream row by the resolved guild id, and drop panel copy that pointed at a
"Needed" figure the page doesn't show / misstated the invite permissions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Author
Owner

QA review fix pushed as 31ca63b: GET /discord/admin/overview now gives no fallback org to a server with unlinked_at (the field #218 sets), matching resolve_guild_org after #218 merges. Before, such a server showed as "Unlinked · fallback org" and appeared in Add radio channel, which would 403. Also dropped two panel copy lines that were wrong (a "Needed" figure the page never shows; invite permissions also include View Channel). Re-run: pytest 614 passed, tsc exit 0.

Note: #221 item 1 (no unlink) overlaps #218, which adds an org-owner unlink. Re-scope #221 once #218 merges.

QA review fix pushed as 31ca63b: `GET /discord/admin/overview` now gives no fallback org to a server with `unlinked_at` (the field #218 sets), matching `resolve_guild_org` after #218 merges. Before, such a server showed as "Unlinked · fallback org" and appeared in Add radio channel, which would 403. Also dropped two panel copy lines that were wrong (a "Needed" figure the page never shows; invite permissions also include View Channel). Re-run: pytest `614 passed`, tsc `exit 0`. Note: #221 item 1 (no unlink) overlaps #218, which adds an org-owner unlink. Re-scope #221 once #218 merges.
logan merged commit ebd7e893ce into main 2026-10-04 22:05:36 -04:00
Sign in to join this conversation.
No Reviewers
No labels
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: logan/server-26#219