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
+25 -15
View File
@@ -160,7 +160,8 @@ export default function NodeDetailPage() {
<> No timeout is currently enforced (permanent override).</>
)}
</p>
<div className="flex gap-2">
{/* Override routes are require_service_key_or_admin (server-26#222). */}
{isAdmin && <div className="flex gap-2">
{node.override_timeout_at && (
<button
onClick={async () => {
@@ -189,7 +190,7 @@ export default function NodeDetailPage() {
>
Force Revert Config
</button>
</div>
</div>}
</div>
)}
@@ -245,20 +246,29 @@ export default function NodeDetailPage() {
)}
{/* Actions */}
{/* Configure and Restart are admin-only on the server (server-26#222);
non-admins get a note instead of a button that 403s. */}
{!isAdmin && !node.configured && (
<p className="text-sm text-ink-2">This node isn&apos;t configured yet. An admin needs to assign it a radio system.</p>
)}
<div className="flex flex-wrap gap-3">
<button
onClick={() => setShowConfig(true)}
className="px-4 py-2 bg-indigo-700 hover:bg-indigo-600 text-white rounded-lg text-sm font-mono transition-colors"
>
{node.configured ? "Reassign System" : "Configure"}
</button>
<button
disabled={sending}
onClick={() => sendCommand("op25_restart", "Restart OP25")}
className="px-4 py-2 bg-raised hover:bg-gray-700 text-ink-2 rounded-lg text-sm font-mono transition-colors disabled:opacity-50"
>
Restart OP25
</button>
{isAdmin && (
<button
onClick={() => setShowConfig(true)}
className="px-4 py-2 bg-indigo-700 hover:bg-indigo-600 text-white rounded-lg text-sm font-mono transition-colors"
>
{node.configured ? "Reassign System" : "Configure"}
</button>
)}
{isAdmin && (
<button
disabled={sending}
onClick={() => sendCommand("op25_restart", "Restart OP25")}
className="px-4 py-2 bg-raised hover:bg-gray-700 text-ink-2 rounded-lg text-sm font-mono transition-colors disabled:opacity-50"
>
Restart OP25
</button>
)}
{isAdmin && (
<button
disabled={sending}