From d2437e3a0be4d60b7b37c053bbd41b8939963ef5 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 4 Oct 2026 21:36:30 -0400 Subject: [PATCH] Picker review fixes: validate stream_id on stop; accurate offline + no-system copy - POST /discord/nodes/{id}/streams/stop: stream_id must be guild:channel snowflakes, so a malformed id is a 422 instead of a Firestore path error. - Offline footer said channels resume on reconnect; ad-hoc ones stop once the node is gone past the lease grace (reconcile_once). Say so. - Picker: say why Start is disabled when the node has no system. Co-Authored-By: Claude Opus 5.5 --- drb-c2-core/app/routers/discord_web.py | 4 +++- drb-c2-core/tests/test_discord_web_picker.py | 10 ++++++++++ drb-frontend/components/NodeDiscordRadio.tsx | 7 ++++++- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/drb-c2-core/app/routers/discord_web.py b/drb-c2-core/app/routers/discord_web.py index 936e9ba..8174590 100644 --- a/drb-c2-core/app/routers/discord_web.py +++ b/drb-c2-core/app/routers/discord_web.py @@ -274,7 +274,9 @@ async def start_node_stream(node_id: str, body: NodeStreamStart, decoded: dict = class NodeStreamStop(BaseModel): - stream_id: Optional[str] = None + # guild:channel snowflakes (leases.stream_id_for). Validated so a malformed + # id (e.g. one with "/") is a 422, not a Firestore path error / 500. + stream_id: Optional[str] = Field(None, pattern=r"^[0-9]{1,25}:[0-9]{1,25}$") @router.post("/nodes/{node_id}/streams/stop") diff --git a/drb-c2-core/tests/test_discord_web_picker.py b/drb-c2-core/tests/test_discord_web_picker.py index 52d6e8b..d2bf74d 100644 --- a/drb-c2-core/tests/test_discord_web_picker.py +++ b/drb-c2-core/tests/test_discord_web_picker.py @@ -205,3 +205,13 @@ def test_legacy_raw_id_join_refuses_other_orgs_server(fs): with pytest.raises(HTTPException) as e: run(nodes_router.send_command("n1", cmd, ADMIN)) assert e.value.status_code == 404 + + +def test_stop_rejects_malformed_stream_id(): + # "a/b" would otherwise reach Firestore as a document path and 500. + from pydantic import ValidationError + for bad in ("100/1001", "../x", "100:1001/extra", ""): + with pytest.raises(ValidationError): + web.NodeStreamStop(stream_id=bad) + assert web.NodeStreamStop(stream_id="100:1001").stream_id == "100:1001" + assert web.NodeStreamStop().stream_id is None diff --git a/drb-frontend/components/NodeDiscordRadio.tsx b/drb-frontend/components/NodeDiscordRadio.tsx index c516a82..7e65002 100644 --- a/drb-frontend/components/NodeDiscordRadio.tsx +++ b/drb-frontend/components/NodeDiscordRadio.tsx @@ -271,6 +271,11 @@ function DiscordPicker({ )} + {!options.system_id && ( +

+ This node is not listening to a system yet. Assign it one before adding a radio channel. +

+ )} {atNodeCap && !channelOnHere && (

This node already carries {options.limits.node_used} of {options.limits.node_max} radio channels. Stop one @@ -439,7 +444,7 @@ export function NodeDiscordRadio({ nodeId }: { nodeId: string }) { {actionError &&

{actionError}

} {options && !options.node_online && streams.length > 0 && (

- This node is offline — its channels resume when it reconnects. If it stays down, persistent ones move to another node on the same system. + This node is offline — its channels resume if it reconnects shortly. If it stays down, ad-hoc ones stop and persistent ones move to another node on the same system.

)}