Discord: several radio streams per voice channel; adding never overwrites (#213)
Owner-reported on live: adding a radio channel for a server + voice channel that already had one (different label, system, even bot) overwrote the existing stream and made its bot leave, because stream_id was guild:channel. - New streams get guild:channel:<12 hex> ids (leases.new_stream_id); old guild:channel docs keep working for read/stop/remove/node reports. Route validators accept both (leases.STREAM_ID_PATTERN). - start_stream: no stream_id = ADD. Same system already on in that channel is idempotent (already_on, label/mode untouched); a stopped one of that system is restarted; anything else is a new doc. stream_id = explicit restart/edit (admin Start button, reconciler), incl. moving to another bot. - preferred_token_id is strict: bot already in this server -> 409 preferred_busy naming the stream; on another node -> preferred_other_node; unusable -> preferred_unavailable. No silent swap. pool_exhausted now says one-bot-per-server and to add + invite another bot. - One-token-per-guild was already enforced inside the lease transaction; now covered by race tests. Per-guild asyncio lock serialises the already-on / server-cap check with the create (guild half of #223). - Stop by stream_id (POST /discord/guilds/{id}/stop {stream_id}); admin UI Stop/Start/Remove act on the row's stream; node picker lists every stream in a channel and adds beside other systems. - Bot: /leave stream: (autocomplete) stops one; /leave channel: with several streams asks which unless all:True; bare /leave still stops the server. /status and /join name each stream's bot. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
16a609e35b
commit
8af410aede
@@ -5,6 +5,10 @@
|
||||
// the Discord servers linked to this node's org and the voice channels the
|
||||
// DRB bot reported in them; a pick becomes a radio channel pinned to this node
|
||||
// (desired state — the node reconciles and reports back, shown per row).
|
||||
// A voice channel can carry several radio streams, each its own bot playing
|
||||
// its own system (server-26#213 follow-up): picking a channel that already
|
||||
// plays another system ADDS this node's system beside it; only a stream of
|
||||
// this node's own system there is restarted / moved here.
|
||||
// Layout follows design-canvas/discord-system-mockup.html "Radio channels".
|
||||
|
||||
import { useCallback, useEffect, useMemo, useState } from "react";
|
||||
@@ -64,6 +68,11 @@ function modeLabel(mode: DiscordStreamView["mode"]) {
|
||||
return mode === "persistent" ? "persistent" : "ad-hoc";
|
||||
}
|
||||
|
||||
function streamLabel(s: DiscordStreamView) {
|
||||
const sys = s.system_name ?? "radio";
|
||||
return s.token_name ? `${sys} (${s.token_name})` : sys;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Picker
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -199,6 +208,7 @@ function DiscordPicker({
|
||||
<div className="space-y-1">
|
||||
{grp.channels.map((c) => {
|
||||
const here = c.stream?.node_id === nodeId;
|
||||
const others = (c.streams ?? []).filter((x) => x.stream_id !== c.stream?.stream_id);
|
||||
return (
|
||||
<label
|
||||
key={c.channel_id}
|
||||
@@ -221,8 +231,14 @@ function DiscordPicker({
|
||||
{c.stream && (
|
||||
<span className="text-xs text-ink-muted block">
|
||||
{here
|
||||
? "Already on this node"
|
||||
: `Carried by ${c.stream.node_name ?? "another node"} — picking it moves it to this node`}
|
||||
? `${options.system_name ?? "This system"} is already on this node here`
|
||||
: `${options.system_name ?? "This system"} is carried by ${c.stream.node_name ?? "another node"} — picking it moves it to this node`}
|
||||
</span>
|
||||
)}
|
||||
{others.length > 0 && (
|
||||
<span className="text-xs text-ink-muted block">
|
||||
Also playing: {others.map(streamLabel).join(", ")}
|
||||
{!c.stream && ` — this adds ${options.system_name ?? "this node's system"} beside it with another bot`}
|
||||
</span>
|
||||
)}
|
||||
</span>
|
||||
@@ -356,7 +372,7 @@ export function NodeDiscordRadio({ nodeId }: { nodeId: string }) {
|
||||
|
||||
async function remove(s: DiscordStreamView) {
|
||||
const name = s.channel_name ?? "this channel";
|
||||
if (!confirm(`Remove radio from ${name} permanently? It is stopped first (the node logs its radio bot out), then deleted from DRB.`)) return;
|
||||
if (!confirm(`Remove ${streamLabel(s)} from ${name} permanently? It is stopped first (the node logs its radio bot out), then deleted from DRB. Other streams in that voice channel keep playing.`)) return;
|
||||
setBusy(`rm:${s.stream_id}`);
|
||||
setActionError(null);
|
||||
try {
|
||||
@@ -429,7 +445,9 @@ export function NodeDiscordRadio({ nodeId }: { nodeId: string }) {
|
||||
<tr key={s.stream_id} className="border-b border-line last:border-0 align-top">
|
||||
<td className="px-4 py-2.5">
|
||||
<div className="text-ink font-medium">🔊 {s.channel_name ?? s.channel_id}</div>
|
||||
<div className="text-xs text-ink-muted">{s.guild_name ?? s.guild_id}</div>
|
||||
<div className="text-xs text-ink-muted">
|
||||
{s.guild_name ?? s.guild_id} · {streamLabel(s)}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-4 py-2.5 text-ink-2">{modeLabel(s.mode)}</td>
|
||||
<td className="px-4 py-2.5">
|
||||
|
||||
Reference in New Issue
Block a user