fix(frontend): hide admin-only node and system controls from non-admins (#222, #283)

UI gate only — no route auth changes. Node page: override Ack/Force
Revert, Configure/Reassign and Restart OP25 are admin-only; non-admins on
an unconfigured node get a "an admin needs to assign it" note. /nodes
"Needs Configuration" cards open the config modal only for admins, and
link to the node page otherwise. /systems: New/Edit/Duplicate/Delete and
the AI-flags, area-context, talkgroup-pending and vocabulary panels are
admin-only; operators get a read-only list. (Delete's missing catch was
fixed in #290.)

Widening any of these routes to org owners/operators is a separate,
security-reviewed backend change.

Frontend unverified: no Node on the authoring box (#207).

Closes #222
Closes #283

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Logan Cusano
2026-10-25 22:07:22 -04:00
co-authored by Claude Opus 5.5
parent 606b0a103c
commit 9702b90d40
3 changed files with 56 additions and 30 deletions
+22 -12
View File
@@ -1953,12 +1953,16 @@ export default function SystemsPage() {
<div className="space-y-6">
<div className="flex items-center justify-between">
<h1 className="text-xl font-bold text-white font-mono">Systems</h1>
<button
onClick={() => { setEditing("new"); setEditIsDuplicate(false); }}
className="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg text-sm font-mono transition-colors"
>
+ New System
</button>
{/* Every systems write route is require_admin_token (server-26#283);
operators get a read-only view. */}
{isAdmin && (
<button
onClick={() => { setEditing("new"); setEditIsDuplicate(false); }}
className="bg-indigo-600 hover:bg-indigo-500 text-white px-4 py-2 rounded-lg text-sm font-mono transition-colors"
>
+ New System
</button>
)}
</div>
{editing && (
@@ -2006,7 +2010,7 @@ export default function SystemsPage() {
{kindLabel(s)}
</span>
</div>
<div className="mt-3 flex gap-3">
{isAdmin && <div className="mt-3 flex gap-3">
<button
onClick={() => openEdit(s)}
className="text-xs text-indigo-400 hover:text-indigo-300 transition-colors"
@@ -2025,15 +2029,21 @@ export default function SystemsPage() {
>
Delete
</button>
</div>
</div>}
{/* 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 ?? {}} />
<AreaContextPanel systemId={s.system_id} />
<TalkgroupPendingPanel systemId={s.system_id} />
<VocabularyPanel systemId={s.system_id} />
{/* All admin-only routes (server-26#283), incl. area-context and
talkgroup-pending GETs and every vocabulary write. */}
{isAdmin && (
<>
<AiFlagsPanel systemId={s.system_id} initial={(s as unknown as { ai_flags?: SystemAiFlags }).ai_flags ?? {}} />
<AreaContextPanel systemId={s.system_id} />
<TalkgroupPendingPanel systemId={s.system_id} />
<VocabularyPanel systemId={s.system_id} />
</>
)}
</div>
);
})}