Files
server-26/drb-frontend/app/tokens/page.tsx
T
Logan CusanoandClaude Opus 5.5 d3277c13e1 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>
2026-10-04 20:05:35 -04:00

63 lines
2.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
// The radio-bot pool (bot_tokens). Same table as Settings › Discord › Radio
// bots: validity, server membership, invite links, lease, and the
// POST /tokens/{id}/refresh re-check (server-26#213 item 2). Platform admin
// only: every /tokens route is require_admin_token since server-26#204 item 6.
import { useCallback, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { c2api } from "@/lib/c2api";
import { useAuth } from "@/components/AuthProvider";
import { type DiscordAdminOverview, type RadioBot, parseC2Error } from "@/lib/discord";
import { PageHeader } from "@/components/ui/PageHeader";
import { LinkButton } from "@/components/ui/Button";
import { ErrorBanner } from "@/components/ui/EmptyState";
import { SkeletonCard } from "@/components/ui/Skeleton";
import { RadioBotsPanel } from "@/components/discord/RadioBotsPanel";
export default function TokensPage() {
const { isAdmin, loading: authLoading } = useAuth();
const router = useRouter();
const [bots, setBots] = useState<RadioBot[] | null>(null);
const [overview, setOverview] = useState<DiscordAdminOverview | null>(null);
const [error, setError] = useState<string | null>(null);
useEffect(() => {
if (!authLoading && !isAdmin) router.replace("/network");
}, [authLoading, isAdmin, router]);
const refresh = useCallback(async () => {
try {
const t = await c2api.getTokens();
setBots(t as RadioBot[]);
setError(null);
} catch (err) {
setError(parseC2Error(err).message);
setBots((b) => b ?? []);
}
// Server names for the membership column; the table still works without them.
try {
setOverview(await c2api.getDiscordAdmin());
} catch {
/* membership falls back to raw server ids */
}
}, []);
useEffect(() => { if (isAdmin) refresh(); }, [isAdmin, refresh]);
if (authLoading || !isAdmin) return null;
return (
<div className="space-y-6">
<PageHeader
title="Radio bots"
description="Discord bot tokens that nodes log in with to carry radio into voice channels."
action={<LinkButton href="/settings/discord" variant="secondary" size="sm">Discord settings</LinkButton>}
/>
{error && <ErrorBanner message={error} />}
{bots === null ? <SkeletonCard /> : <RadioBotsPanel bots={bots} overview={overview} onChanged={refresh} />}
</div>
);
}