import React, { useEffect, useState } from 'react'; import { Crosshair, Shield, ShieldOff } from '../icons'; import api from '../utils/api'; import EmptyState from './EmptyState/EmptyState'; /* * RuleStateControls — admin-only rule operational state panel. * * Server-gated via require_admin on the API; this component is also * conditionally rendered on the role flag from /config so a non-admin * never sees the controls. Per feedback_serverside_ui.md the * client-side gate is a UX nicety, NOT a security boundary — the * server returns 403 either way. */ interface RuleRow { rule_id: string; rule_version: number; name: string; description: string; state: 'enabled' | 'disabled' | 'clipped'; confidence_max: number | null; expires_at: string | null; reason: string | null; set_by: string | null; set_at: string | null; } const RuleStateControls: React.FC = () => { const [rules, setRules] = useState([]); const [isAdmin, setIsAdmin] = useState(false); const [loaded, setLoaded] = useState(false); const [busy, setBusy] = useState(null); const refresh = async () => { try { const res = await api.get('/ttp/rules'); setRules(Array.isArray(res.data) ? res.data : []); } catch { setRules([]); } finally { setLoaded(true); } }; useEffect(() => { const probe = async () => { try { const cfg = await api.get('/config'); setIsAdmin(cfg.data?.role === 'admin'); } catch { setIsAdmin(false); } refresh(); }; probe(); }, []); const setState = async ( ruleId: string, state: 'enabled' | 'disabled' | 'clipped', confidence_max?: number, ) => { setBusy(ruleId); try { await api.post(`/ttp/rules/${ruleId}/state`, { state, confidence_max: confidence_max ?? null, expires_at: null, reason: null, }); await refresh(); } catch { // best-effort; failures show on next refresh } finally { setBusy(null); } }; const revert = async (ruleId: string) => { setBusy(ruleId); try { await api.delete(`/ttp/rules/${ruleId}/state`); await refresh(); } catch { // ignored } finally { setBusy(null); } }; if (!isAdmin) { return null; } return (
RULE STATE — ADMIN
{!loaded ? null : rules.length === 0 ? ( ) : ( {rules.map((r) => ( ))}
RULE NAME STATE CLIP ACTIONS
{r.rule_id} {r.name} {r.state.toUpperCase()} {r.confidence_max !== null ? r.confidence_max.toFixed(2) : '—'}
)}
); }; export default RuleStateControls;