Discord admin page + hide admin-only bot pool from operators (#213 items 2, 4) #219
@@ -91,13 +91,17 @@ export default function NetworkPage() {
|
|||||||
count={systemsLoading ? null : systems.length}
|
count={systemsLoading ? null : systems.length}
|
||||||
countLabel={systemsLoading ? "" : "configured"}
|
countLabel={systemsLoading ? "" : "configured"}
|
||||||
/>
|
/>
|
||||||
<HubCard
|
{/* The radio-bot pool is platform-admin only (server-26#204 item 6);
|
||||||
href="/tokens"
|
an operator following this card only hit a 403 (server-26#213). */}
|
||||||
title="Bot Tokens"
|
{isAdmin && (
|
||||||
description="Discord bot tokens available for nodes to claim when relaying live audio."
|
<HubCard
|
||||||
count={null}
|
href="/tokens"
|
||||||
countLabel="manage"
|
title="Bot Tokens"
|
||||||
/>
|
description="Discord bot tokens available for nodes to claim when relaying live audio."
|
||||||
|
count={null}
|
||||||
|
countLabel="manage"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1220,13 +1220,16 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
|||||||
const [tokens, setTokens] = useState<TokenOption[] | null>(null);
|
const [tokens, setTokens] = useState<TokenOption[] | null>(null);
|
||||||
const [saving, setSaving] = useState(false);
|
const [saving, setSaving] = useState(false);
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
async function load() {
|
async function load() {
|
||||||
if (tokens !== null) return;
|
if (tokens !== null) return;
|
||||||
try {
|
try {
|
||||||
const data = await c2api.getTokens();
|
const data = await c2api.getTokens();
|
||||||
setTokens(data as TokenOption[]);
|
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([]);
|
setTokens([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1241,6 +1244,8 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
|||||||
try {
|
try {
|
||||||
await c2api.setPreferredToken(tokenId, systemId);
|
await c2api.setPreferredToken(tokenId, systemId);
|
||||||
setPreferredId(tokenId);
|
setPreferredId(tokenId);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Could not save the preference.");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1252,6 +1257,8 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
|||||||
try {
|
try {
|
||||||
await c2api.setPreferredToken(preferredId, "_none");
|
await c2api.setPreferredToken(preferredId, "_none");
|
||||||
setPreferredId(null);
|
setPreferredId(null);
|
||||||
|
} catch (err) {
|
||||||
|
setError(err instanceof Error ? err.message : "Could not clear the preference.");
|
||||||
} finally {
|
} finally {
|
||||||
setSaving(false);
|
setSaving(false);
|
||||||
}
|
}
|
||||||
@@ -1276,6 +1283,8 @@ function PreferredTokenPanel({ systemId, initialTokenId }: { systemId: string; i
|
|||||||
<div className="mt-3 space-y-2 font-mono text-xs">
|
<div className="mt-3 space-y-2 font-mono text-xs">
|
||||||
{tokens === null ? (
|
{tokens === null ? (
|
||||||
<p className="text-gray-600 italic">Loading tokens…</p>
|
<p className="text-gray-600 italic">Loading tokens…</p>
|
||||||
|
) : error ? (
|
||||||
|
<p className="text-red-400">{error}</p>
|
||||||
) : tokens.length === 0 ? (
|
) : tokens.length === 0 ? (
|
||||||
<p className="text-gray-600 italic">No tokens in pool.</p>
|
<p className="text-gray-600 italic">No tokens in pool.</p>
|
||||||
) : (
|
) : (
|
||||||
@@ -2016,7 +2025,10 @@ export default function SystemsPage() {
|
|||||||
Delete
|
Delete
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<PreferredTokenPanel systemId={s.system_id} initialTokenId={s.preferred_token_id} />
|
{/* 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 && <PreferredTokenPanel systemId={s.system_id} initialTokenId={s.preferred_token_id} />}
|
||||||
<AiFlagsPanel systemId={s.system_id} initial={(s as unknown as { ai_flags?: SystemAiFlags }).ai_flags ?? {}} />
|
<AiFlagsPanel systemId={s.system_id} initial={(s as unknown as { ai_flags?: SystemAiFlags }).ai_flags ?? {}} />
|
||||||
<AreaContextPanel systemId={s.system_id} />
|
<AreaContextPanel systemId={s.system_id} />
|
||||||
<TalkgroupPendingPanel systemId={s.system_id} />
|
<TalkgroupPendingPanel systemId={s.system_id} />
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ interface TokenRecord {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function TokensPage() {
|
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 router = useRouter();
|
||||||
const [tokens, setTokens] = useState<TokenRecord[]>([]);
|
const [tokens, setTokens] = useState<TokenRecord[]>([]);
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
@@ -26,8 +28,8 @@ export default function TokensPage() {
|
|||||||
const [error, setError] = useState<string | null>(null);
|
const [error, setError] = useState<string | null>(null);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!authLoading && !isAdmin && !isOperator) router.replace("/");
|
if (!authLoading && !isAdmin) router.replace("/network");
|
||||||
}, [authLoading, isAdmin, isOperator, router]);
|
}, [authLoading, isAdmin, router]);
|
||||||
|
|
||||||
const refresh = useCallback(async () => {
|
const refresh = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -38,7 +40,7 @@ export default function TokensPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => { refresh(); }, [refresh]);
|
useEffect(() => { if (isAdmin) refresh(); }, [isAdmin, refresh]);
|
||||||
|
|
||||||
async function handleAdd(e: React.FormEvent) {
|
async function handleAdd(e: React.FormEvent) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
@@ -67,7 +69,7 @@ export default function TokensPage() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (authLoading || (!isAdmin && !isOperator)) return null;
|
if (authLoading || !isAdmin) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
|
|||||||
Reference in New Issue
Block a user