"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. 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}

}
); } 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 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)} />} {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 (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 )}
setEditing(null)} />
)}
); }