diff --git a/drb-c2-core/app/routers/discord.py b/drb-c2-core/app/routers/discord.py index ab3bb69..3ae3258 100644 --- a/drb-c2-core/app/routers/discord.py +++ b/drb-c2-core/app/routers/discord.py @@ -202,7 +202,10 @@ async def admin_overview(_: dict = Depends(require_admin_token)): guilds: dict[str, dict] = {} for gid, link in links.items(): guilds[gid] = {"guild_id": gid, "guild_name": link.get("guild_name"), "linked": bool(link.get("org_id")), - "org_id": link.get("org_id"), "linked_at": link.get("linked_at")} + "org_id": link.get("org_id"), "linked_at": link.get("linked_at"), + # Explicitly unlinked servers get no fallback org (mirrors + # streams.resolve_guild_org once server-26#218 lands). + "_no_fallback": bool(link.get("unlinked_at"))} rows = [] for s in all_streams: gid = str(s.get("guild_id") or leases.guild_of(s.get("stream_id") or "")) @@ -212,14 +215,15 @@ async def admin_overview(_: dict = Depends(require_admin_token)): g["guild_name"] = s["guild_name"] node = nodes.get(s.get("node_id")) or {} tok = tokens.get(s.get("token_id")) or {} - rows.append({**_public_stream(s), "org_id": s.get("org_id"), "guild_name": s.get("guild_name"), + rows.append({**_public_stream(s), "guild_id": gid, "org_id": s.get("org_id"), "guild_name": s.get("guild_name"), "created_at": s.get("created_at"), "node_name": node.get("name"), "node_status": node.get("status"), "token_name": tok.get("name")}) for g in guilds.values(): - acting = g["org_id"] if g["linked"] else fallback + no_fallback = g.pop("_no_fallback", False) + acting = g["org_id"] if g["linked"] else (None if no_fallback else fallback) g["acting_org_id"] = acting g["acting_org_name"] = (orgs.get(acting) or {}).get("name") if acting else None - g["live_streams"] = sum(1 for r in rows if r["guild_id"] == g["guild_id"] and r.get("desired") == "on") + g["live_streams"] = sum(1 for r in rows if r.get("guild_id") == g["guild_id"] and r.get("desired") == "on") rows.sort(key=lambda r: (0 if r.get("desired") == "on" else 1, r.get("guild_name") or r.get("guild_id") or "", r.get("channel_name") or r.get("channel_id") or "")) diff --git a/drb-c2-core/tests/test_discord_admin.py b/drb-c2-core/tests/test_discord_admin.py index 860ede4..e935988 100644 --- a/drb-c2-core/tests/test_discord_admin.py +++ b/drb-c2-core/tests/test_discord_admin.py @@ -115,3 +115,15 @@ def test_relinking_a_server_to_another_org_stops_its_live_channels(fs, monkeypat # moved to a different org: its live channels are stopped r = client.put("/discord/guilds/111/link", json={"org_id": "founding"}).json() assert r["stopped"] == ["111:900"] and calls == [("111", "server linked to another organization")] + + +def test_explicitly_unlinked_server_gets_no_fallback_org(fs): + # server-26#218 unlinks by clearing org_id and stamping unlinked_at; + # resolve_guild_org then refuses the fallback, so the page must not offer it. + _seed(fs) + fs.put("discord_guilds", "444", {"guild_id": "444", "org_id": None, "unlinked_at": "2026-10-02T00:00:00+00:00"}) + app.dependency_overrides[require_admin_token] = lambda: ADMIN + guilds = {g["guild_id"]: g for g in client.get("/discord/admin/overview").json()["guilds"]} + assert guilds["444"]["linked"] is False and guilds["444"]["acting_org_id"] is None + assert "_no_fallback" not in guilds["444"] + assert guilds["222"]["acting_org_id"] == "founding" # never-linked server keeps the fallback diff --git a/drb-frontend/components/discord/RadioBotsPanel.tsx b/drb-frontend/components/discord/RadioBotsPanel.tsx index a79814b..17d4616 100644 --- a/drb-frontend/components/discord/RadioBotsPanel.tsx +++ b/drb-frontend/components/discord/RadioBotsPanel.tsx @@ -185,7 +185,7 @@ export function RadioBotsPanel({ return ( {leased > 0 && ( @@ -209,7 +209,7 @@ export function RadioBotsPanel({ note={ <> Release is ordered: the node stops the bot's gateway session, then acks, then the - token returns to the pool — a token is never live on two nodes. Invite links ask for Connect + Speak only. + token returns to the pool — a token is never live on two nodes. Invite links ask for View Channel, Connect and Speak only. } >