From e574bd924a725cf427414e3244aa19f932718f3a Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 4 Oct 2026 19:57:14 -0400 Subject: [PATCH] frontend: hide radio-bot pool controls from non-admins (server-26#213 item 4) GET /tokens and PUT /tokens/{id}/prefer became platform-admin only in #216 (server-26#204 item 6). Three frontend surfaces still offered them to operators: - systems page "Preferred Bot Token" panel: now rendered for admins only. No non-admin listing endpoint was added: the write behind the picker is admin-only too, so a list would only feed a control that 403s on save. The panel also no longer turns a fetch error into "No tokens in pool." - /tokens page: admin-only (operators are sent to /network). - /network hub: the Bot Tokens card is shown to admins only. Co-Authored-By: Claude Opus 5.5 --- drb-frontend/app/network/page.tsx | 18 +++++++++++------- drb-frontend/app/systems/page.tsx | 16 ++++++++++++++-- drb-frontend/app/tokens/page.tsx | 12 +++++++----- 3 files changed, 32 insertions(+), 14 deletions(-) diff --git a/drb-frontend/app/network/page.tsx b/drb-frontend/app/network/page.tsx index f207686..d4d90f6 100644 --- a/drb-frontend/app/network/page.tsx +++ b/drb-frontend/app/network/page.tsx @@ -91,13 +91,17 @@ export default function NetworkPage() { count={systemsLoading ? null : systems.length} countLabel={systemsLoading ? "" : "configured"} /> - + {/* The radio-bot pool is platform-admin only (server-26#204 item 6); + an operator following this card only hit a 403 (server-26#213). */} + {isAdmin && ( + + )} ); diff --git a/drb-frontend/app/systems/page.tsx b/drb-frontend/app/systems/page.tsx index c968d34..84c62b5 100644 --- a/drb-frontend/app/systems/page.tsx +++ b/drb-frontend/app/systems/page.tsx @@ -1220,13 +1220,16 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i const [tokens, setTokens] = useState(null); const [saving, setSaving] = useState(false); const [open, setOpen] = useState(false); + const [error, setError] = useState(null); async function load() { if (tokens !== null) return; try { const data = await c2api.getTokens(); setTokens(data as TokenOption[]); - } catch { + } catch (err) { + // An error used to render as "No tokens in pool." — a false empty state. + setError(err instanceof Error ? err.message : "Could not load radio bots."); setTokens([]); } } @@ -1241,6 +1244,8 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i try { await c2api.setPreferredToken(tokenId, systemId); setPreferredId(tokenId); + } catch (err) { + setError(err instanceof Error ? err.message : "Could not save the preference."); } finally { setSaving(false); } @@ -1252,6 +1257,8 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i try { await c2api.setPreferredToken(preferredId, "_none"); setPreferredId(null); + } catch (err) { + setError(err instanceof Error ? err.message : "Could not clear the preference."); } finally { setSaving(false); } @@ -1276,6 +1283,8 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
{tokens === null ? (

Loading tokens…

+ ) : error ? ( +

{error}

) : tokens.length === 0 ? (

No tokens in pool.

) : ( @@ -2016,7 +2025,10 @@ export default function SystemsPage() { Delete
- + {/* GET /tokens and PUT /tokens/{id}/prefer are platform-admin only + (server-26#204 item 6), so for anyone else this panel could + neither list the pool nor save a choice (server-26#213 item 4). */} + {isAdmin && } diff --git a/drb-frontend/app/tokens/page.tsx b/drb-frontend/app/tokens/page.tsx index 9a6611f..8e4b2dd 100644 --- a/drb-frontend/app/tokens/page.tsx +++ b/drb-frontend/app/tokens/page.tsx @@ -15,7 +15,9 @@ interface TokenRecord { } export default function TokensPage() { - const { isAdmin, isOperator, loading: authLoading } = useAuth(); + // Platform admin only: every /tokens route is require_admin_token since + // server-26#204 item 6, so an operator here only ever saw a failed fetch. + const { isAdmin, loading: authLoading } = useAuth(); const router = useRouter(); const [tokens, setTokens] = useState([]); const [loading, setLoading] = useState(true); @@ -26,8 +28,8 @@ export default function TokensPage() { const [error, setError] = useState(null); useEffect(() => { - if (!authLoading && !isAdmin && !isOperator) router.replace("/"); - }, [authLoading, isAdmin, isOperator, router]); + if (!authLoading && !isAdmin) router.replace("/network"); + }, [authLoading, isAdmin, router]); const refresh = useCallback(async () => { try { @@ -38,7 +40,7 @@ export default function TokensPage() { } }, []); - useEffect(() => { refresh(); }, [refresh]); + useEffect(() => { if (isAdmin) refresh(); }, [isAdmin, refresh]); async function handleAdd(e: React.FormEvent) { e.preventDefault(); @@ -67,7 +69,7 @@ export default function TokensPage() { } } - if (authLoading || (!isAdmin && !isOperator)) return null; + if (authLoading || !isAdmin) return null; return (