"use client"; import { useState } from "react"; import { c2api } from "@/lib/c2api"; import { type DiscordAdminOverview, type DiscordGuild, SNOWFLAKE, fmtWhen, guildLabel, parseC2Error } from "@/lib/discord"; import { Button } from "@/components/ui/Button"; import { DIM, Field, INPUT, Panel, Status, Table, TD } from "./ui"; // Linked Discord servers = discord_guilds docs, plus any server that has had a // radio channel without being linked (it acts for DISCORD_UNLINKED_GUILD_ORG_ID). // Linking is PUT /discord/guilds/{id}/link (platform admin). The owner-side // "/drb link code:" flow is server-26#213 item 1 and is not built here. // Remove is DELETE /discord/guilds/{id}: its radio channels are stopped then // deleted, and the server is unlinked with no fallback org. function LinkForm({ overview, guild, onDone, onCancel, }: { overview: DiscordAdminOverview; guild?: DiscordGuild; onDone: () => void; onCancel: () => void; }) { const [guildId, setGuildId] = useState(guild?.guild_id ?? ""); const [label, setLabel] = useState(guild?.guild_name ?? ""); const [orgId, setOrgId] = useState(guild?.org_id ?? guild?.acting_org_id ?? ""); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); async function submit(e: React.FormEvent) { e.preventDefault(); if (!SNOWFLAKE.test(guildId.trim())) { setError("Server ID is the 17–20 digit number from Discord (right-click the server icon → Copy Server ID)."); return; } const actingNow = guild?.acting_org_id ?? null; if (guild && guild.live_streams > 0 && actingNow && actingNow !== orgId) { const n = guild.live_streams; if (!confirm(`${guildLabel(guild)} has ${n} live radio channel${n === 1 ? "" : "s"} on the current organization's nodes. Moving it stops ${n === 1 ? "it" : "them"}. Continue?`)) return; } setSaving(true); setError(null); try { await c2api.linkDiscordGuild(guildId.trim(), { org_id: orgId, guild_name: label.trim() || undefined }); onDone(); } catch (err) { setError(parseC2Error(err).message); } finally { setSaving(false); } } return (
setGuildId(e.target.value)} required readOnly={!!guild} inputMode="numeric" className={`${INPUT} font-mono ${guild ? "opacity-70" : ""}`} /> setLabel(e.target.value)} className={INPUT} placeholder="Fire Co. 7" /> {error &&

{error}

}
); } type RemoveResult = { guild: string; streams: number; bots: { name: string; left: boolean; error: string | null }[] }; function RemoveForm({ guild, onDone, onCancel, }: { guild: DiscordGuild; onDone: (r: RemoveResult) => void; onCancel: () => void; }) { const [leaveBots, setLeaveBots] = useState(false); const [saving, setSaving] = useState(false); const [error, setError] = useState(null); const n = guild.live_streams; async function submit() { setSaving(true); setError(null); try { const r = await c2api.removeDiscordGuild(guild.guild_id, leaveBots); onDone({ guild: guildLabel(guild), streams: r.streams_removed.length, bots: r.radio_bots }); } catch (err) { setError(parseC2Error(err).message); } finally { setSaving(false); } } return (

Remove {guildLabel(guild)} from DRB?{" "} {n > 0 ? `Its ${n} live radio channel${n === 1 ? " is" : "s are"} stopped first, then every radio channel in it is deleted.` : "Every radio channel in it is deleted."}{" "} It is unlinked and will not fall back to the default organization; linking it again brings it back.

The DRB bot itself stays in the Discord server until someone kicks it there.

{error &&

{error}

}
); } export function ServersPanel({ overview, onChanged, }: { overview: DiscordAdminOverview; onChanged: () => Promise | void; }) { // "new" = blank link form; a guild id = editing that row's link. const [editing, setEditing] = useState(null); const [removing, setRemoving] = useState(null); const [removed, setRemoved] = useState(null); const fallback = overview.limits.unlinked_guild_org_id; const fallbackName = overview.limits.unlinked_guild_org_name || fallback; const max = overview.limits.guild_max_streams; const done = async () => { setEditing(null); await onChanged(); }; return ( setEditing(editing === "new" ? null : "new")}>+ Link a server} note={ fallback ? ( <> Unlinked servers act for {fallbackName} (DISCORD_UNLINKED_GUILD_ORG_ID). Any server the DRB bot is invited to can use that org's systems until it is linked here. Set the variable to an empty string once every real server is linked. ) : ( <>Unlinked servers can't use radio commands until they are linked here. ) } > {editing === "new" && setEditing(null)} />} {removed && (
Removed {removed.guild}: {removed.streams} radio channel{removed.streams === 1 ? "" : "s"} deleted. {removed.bots.length > 0 && ( <> {" "}Radio bots:{" "} {removed.bots.map((b) => `${b.name} ${b.left ? "left" : `did not leave (${b.error ?? "unknown error"})`}`).join("; ")}. )}{" "} The DRB bot is still in that Discord server until someone kicks it.
)} {overview.guilds.length === 0 ? (

No Discord servers yet. Link one, or run a radio command in a server the DRB bot is in.

) : ( {overview.guilds.flatMap((g) => { const row = ( ); if (removing === g.guild_id) { return [ row, , ]; } if (editing !== g.guild_id) return [row]; return [ row, , ]; })}
{guildLabel(g)}
{g.guild_id}
{g.acting_org_name || g.acting_org_id || nobody} {!g.linked && g.acting_org_id &&
fallback, not linked
}
{g.live_streams} / {max} {g.linked ? fmtWhen(g.linked_at) : —} {g.linked ? ( Linked ) : g.acting_org_id ? ( Unlinked · fallback org ) : ( Unlinked )}
setRemoving(null)} onDone={async (r) => { setRemoving(null); setRemoved(r); await onChanged(); }} />
setEditing(null)} />
)}
); }