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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
2457d2d734
commit
d2437e3a0b
@@ -274,7 +274,9 @@ async def start_node_stream(node_id: str, body: NodeStreamStart, decoded: dict =
|
|||||||
|
|
||||||
|
|
||||||
class NodeStreamStop(BaseModel):
|
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")
|
@router.post("/nodes/{node_id}/streams/stop")
|
||||||
|
|||||||
@@ -205,3 +205,13 @@ def test_legacy_raw_id_join_refuses_other_orgs_server(fs):
|
|||||||
with pytest.raises(HTTPException) as e:
|
with pytest.raises(HTTPException) as e:
|
||||||
run(nodes_router.send_command("n1", cmd, ADMIN))
|
run(nodes_router.send_command("n1", cmd, ADMIN))
|
||||||
assert e.value.status_code == 404
|
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
|
||||||
|
|||||||
@@ -271,6 +271,11 @@ function DiscordPicker({
|
|||||||
</fieldset>
|
</fieldset>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{!options.system_id && (
|
||||||
|
<p className="text-xs text-sev-moderate">
|
||||||
|
This node is not listening to a system yet. Assign it one before adding a radio channel.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
{atNodeCap && !channelOnHere && (
|
{atNodeCap && !channelOnHere && (
|
||||||
<p className="text-xs text-sev-moderate">
|
<p className="text-xs text-sev-moderate">
|
||||||
This node already carries {options.limits.node_used} of {options.limits.node_max} radio channels. Stop one
|
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 && <p role="alert" className="px-4 py-2 text-xs text-sev-major border-t border-line">{actionError}</p>}
|
{actionError && <p role="alert" className="px-4 py-2 text-xs text-sev-major border-t border-line">{actionError}</p>}
|
||||||
{options && !options.node_online && streams.length > 0 && (
|
{options && !options.node_online && streams.length > 0 && (
|
||||||
<p className="px-4 py-2 text-xs text-ink-muted bg-raised border-t border-line">
|
<p className="px-4 py-2 text-xs text-ink-muted bg-raised border-t border-line">
|
||||||
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.
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user