Discord: log node-reported stream state; remove a radio channel or a server

- handle_actual logs every node-reported state TRANSITION (not heartbeats)
  with stream/node/guild/channel/token ids and the node's error text: info
  for connected/connecting/stopped, warning for error, and a dedicated
  "radio bot X failed to log in on node N: <reason>" line for login_failed.
  The node's reason code is now stored in actual.code and shown next to the
  error on Settings > Discord and the node page.
- Remove a radio channel permanently: streams.delete_stream stops it through
  desired state first (node republished without it, token -> releasing per
  #216, freed on the node's ack), then deletes the doc in a transaction that
  refuses if the channel was restarted in between. DELETE
  /discord/streams/{id} (platform admin) and DELETE
  /discord/nodes/{node}/streams/{id} (admin or org owner, same org + node
  only; operators can still stop, not remove). Audited.
- Remove a server: DELETE /discord/guilds/{id} (admin) deletes every radio
  channel in it the same way, then tombstones discord_guilds (org_id None +
  unlinked_at, #218 semantics, plus removed_at) so it never falls back to
  DISCORD_UNLINKED_GUILD_ORG_ID; the admin page hides it, and /drb link or the
  admin link brings it back. Optional leave_radio_bots makes pool bots and the
  server's org's own BYO bots leave via Discord REST; the DRB server bot stays
  until kicked, and the UI says so. Audited.
- UI: Remove (with confirm) on Radio channels, Servers and the node page list.

Refs #213, #221

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-04 22:40:43 -04:00
co-authored by Claude Opus 5.5
parent fabdf5839e
commit c92bdfe841
13 changed files with 836 additions and 25 deletions
+21
View File
@@ -84,6 +84,21 @@ export const c2api = {
channel_name?: string;
preferred_token_id?: string;
}) => request<DiscordStream>("/discord/streams", { method: "POST", body: JSON.stringify(body) }),
/** Permanently remove a radio channel (stopped through desired state first). Admin only. */
deleteDiscordStream: (streamId: string) =>
request<{ ok: boolean; stream_id: string }>(`/discord/streams/${encodeURIComponent(streamId)}`, {
method: "DELETE",
}),
/** Remove a Discord server from DRB: its radio channels are deleted and it is unlinked with no fallback org. */
removeDiscordGuild: (guildId: string, leaveRadioBots: boolean) =>
request<{
ok: boolean;
guild_id: string;
streams_removed: string[];
radio_bots: { name: string; left: boolean; error: string | null }[];
}>(`/discord/guilds/${encodeURIComponent(guildId)}?leave_radio_bots=${leaveRadioBots ? "true" : "false"}`, {
method: "DELETE",
}),
stopDiscordStream: (guildId: string, channelId: string) =>
request<{ stopped: string[] }>(`/discord/guilds/${encodeURIComponent(guildId)}/stop`, {
method: "POST",
@@ -116,6 +131,12 @@ export const c2api = {
method: "POST",
body: JSON.stringify(streamId ? { stream_id: streamId } : {}),
}),
/** Permanently remove one radio channel from this node (org owner or admin). */
removeNodeDiscordStream: (nodeId: string, streamId: string) =>
request<{ ok: boolean; stream_id: string }>(
`/discord/nodes/${nodeId}/streams/${encodeURIComponent(streamId)}`,
{ method: "DELETE" },
),
deleteNode: (id: string) =>
request(`/nodes/${id}`, { method: "DELETE" }),
+2
View File
@@ -41,6 +41,8 @@ export interface RadioBotAddResult {
export interface DiscordStreamActual {
state?: string;
error?: string | null;
/** The node's reason code on error: login_failed, connect_failed, not_in_guild, bad_channel, no_audio. */
code?: string | null;
since?: string | null;
at?: string | null;
}
+3 -1
View File
@@ -22,7 +22,7 @@ export interface DiscordStreamView {
mode: "persistent" | "adhoc" | null;
node_id: string | null;
node_name: string | null;
actual: { state: StreamState; error: string | null; since?: string; at?: string } | null;
actual: { state: StreamState; error: string | null; code?: string | null; since?: string; at?: string } | null;
updated_at: string | null;
}
@@ -56,6 +56,8 @@ export interface DiscordJoinOptions {
system_id: string | null;
system_name: string | null;
can_control: boolean;
/** Permanent removal: platform admin or the org owner only (operators can stop, not remove). */
can_remove?: boolean;
limits: { node_max: number; node_used: number; guild_max: number };
guilds: DiscordGuildOption[];
streams: DiscordStreamView[];