import React, { useEffect, useState } from 'react'; import api from '../utils/api'; import { Settings, Users, Sliders, Trash2, UserPlus, Key, Save, Shield, AlertTriangle, Palette, Activity, Square, RefreshCw, Play } from '../icons'; import { useToast } from './Toasts/useToast'; import './Dashboard.css'; import './Config.css'; interface UserEntry { uuid: string; username: string; role: string; must_change_password: boolean; } interface ConfigData { role: string; deployment_limit: number; global_mutation_interval: string; users?: UserEntry[]; developer_mode?: boolean; } const Config: React.FC = () => { const [config, setConfig] = useState(null); const [loading, setLoading] = useState(true); const [activeTab, setActiveTab] = useState<'limits' | 'users' | 'globals' | 'appearance' | 'workers'>('limits'); const [accent, setAccent] = useState<'matrix' | 'violet'>(() => { try { const raw = localStorage.getItem('decnet_tweaks'); if (raw) { const parsed = JSON.parse(raw); if (parsed?.accent === 'violet') return 'violet'; } } catch { /* noop */ } return 'matrix'; }); const { push: pushToast } = useToast(); const handleAccentChange = (value: 'matrix' | 'violet') => { setAccent(value); let existing: Record = {}; try { const raw = localStorage.getItem('decnet_tweaks'); if (raw) existing = JSON.parse(raw) ?? {}; } catch { existing = {}; } localStorage.setItem('decnet_tweaks', JSON.stringify({ ...existing, accent: value })); document.documentElement.setAttribute('data-accent', value); pushToast({ text: `ACCENT · ${value.toUpperCase()}`, icon: 'check-circle', tone: 'violet' }); }; // Deployment limit state const [limitInput, setLimitInput] = useState(''); const [limitSaving, setLimitSaving] = useState(false); const [limitMsg, setLimitMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); // Global mutation interval state const [intervalInput, setIntervalInput] = useState(''); const [intervalSaving, setIntervalSaving] = useState(false); const [intervalMsg, setIntervalMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); // Add user form state const [newUsername, setNewUsername] = useState(''); const [newPassword, setNewPassword] = useState(''); const [newRole, setNewRole] = useState<'admin' | 'viewer'>('viewer'); const [addingUser, setAddingUser] = useState(false); const [userMsg, setUserMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); // Confirm delete state const [confirmDelete, setConfirmDelete] = useState(null); // Reset password state const [resetTarget, setResetTarget] = useState(null); const [resetPassword, setResetPassword] = useState(''); // Reinit state const [confirmReinit, setConfirmReinit] = useState(false); const [reiniting, setReiniting] = useState(false); const [reinitMsg, setReinitMsg] = useState<{ type: 'success' | 'error'; text: string } | null>(null); const isAdmin = config?.role === 'admin'; const fetchConfig = async () => { try { const res = await api.get('/config'); setConfig(res.data); setLimitInput(String(res.data.deployment_limit)); setIntervalInput(res.data.global_mutation_interval); } catch (err) { console.error('Failed to fetch config', err); } finally { setLoading(false); } }; useEffect(() => { fetchConfig(); }, []); // If server didn't send users, force tab away from users useEffect(() => { if (config && !config.users && activeTab === 'users') { setActiveTab('limits'); } }, [config, activeTab]); const handleSaveLimit = async () => { const val = parseInt(limitInput); if (isNaN(val) || val < 1 || val > 500) { setLimitMsg({ type: 'error', text: 'VALUE MUST BE 1-500' }); return; } setLimitSaving(true); setLimitMsg(null); try { await api.put('/config/deployment-limit', { deployment_limit: val }); setLimitMsg({ type: 'success', text: 'DEPLOYMENT LIMIT UPDATED' }); fetchConfig(); } catch (err: any) { setLimitMsg({ type: 'error', text: err.response?.data?.detail || 'UPDATE FAILED' }); } finally { setLimitSaving(false); } }; const handleSaveInterval = async () => { if (!/^[1-9]\d*[mdMyY]$/.test(intervalInput)) { setIntervalMsg({ type: 'error', text: 'INVALID FORMAT (e.g. 30m, 1d, 6M)' }); return; } setIntervalSaving(true); setIntervalMsg(null); try { await api.put('/config/global-mutation-interval', { global_mutation_interval: intervalInput }); setIntervalMsg({ type: 'success', text: 'MUTATION INTERVAL UPDATED' }); fetchConfig(); } catch (err: any) { setIntervalMsg({ type: 'error', text: err.response?.data?.detail || 'UPDATE FAILED' }); } finally { setIntervalSaving(false); } }; const handleAddUser = async (e: React.FormEvent) => { e.preventDefault(); if (!newUsername.trim() || !newPassword.trim()) return; setAddingUser(true); setUserMsg(null); try { await api.post('/config/users', { username: newUsername.trim(), password: newPassword, role: newRole, }); setNewUsername(''); setNewPassword(''); setNewRole('viewer'); setUserMsg({ type: 'success', text: 'USER CREATED' }); fetchConfig(); } catch (err: any) { setUserMsg({ type: 'error', text: err.response?.data?.detail || 'CREATE FAILED' }); } finally { setAddingUser(false); } }; const handleDeleteUser = async (uuid: string) => { try { await api.delete(`/config/users/${uuid}`); setConfirmDelete(null); fetchConfig(); } catch (err: any) { alert(err.response?.data?.detail || 'Delete failed'); } }; const handleRoleChange = async (uuid: string, role: string) => { try { await api.put(`/config/users/${uuid}/role`, { role }); fetchConfig(); } catch (err: any) { alert(err.response?.data?.detail || 'Role update failed'); } }; const handleResetPassword = async (uuid: string) => { if (!resetPassword.trim() || resetPassword.length < 8) { alert('Password must be at least 8 characters'); return; } try { await api.put(`/config/users/${uuid}/reset-password`, { new_password: resetPassword }); setResetTarget(null); setResetPassword(''); fetchConfig(); } catch (err: any) { alert(err.response?.data?.detail || 'Password reset failed'); } }; const handleReinit = async () => { setReiniting(true); setReinitMsg(null); try { const res = await api.delete('/config/reinit'); const d = res.data.deleted; setReinitMsg({ type: 'success', text: `PURGED: ${d.logs} logs, ${d.bounties} bounties, ${d.attackers} attacker profiles` }); setConfirmReinit(false); } catch (err: any) { setReinitMsg({ type: 'error', text: err.response?.data?.detail || 'REINIT FAILED' }); } finally { setReiniting(false); } }; if (loading) { return (
LOADING CONFIGURATION...
); } if (!config) { return (

FAILED TO LOAD CONFIGURATION

); } const tabs: { key: string; label: string; icon: React.ReactNode }[] = [ { key: 'limits', label: 'DEPLOYMENT LIMITS', icon: }, ...(config.users ? [{ key: 'users', label: 'USER MANAGEMENT', icon: }] : []), { key: 'globals', label: 'GLOBAL VALUES', icon: }, { key: 'appearance', label: 'APPEARANCE', icon: }, ...(isAdmin ? [{ key: 'workers', label: 'WORKERS', icon: }] : []), ]; return (

SYSTEM CONFIGURATION

{tabs.map((tab) => ( ))}
{/* DEPLOYMENT LIMITS TAB */} {activeTab === 'limits' && (
MAXIMUM DECKIES PER DEPLOYMENT {isAdmin ? ( <>
setLimitInput(e.target.value)} />
{[10, 50, 100, 200].map((v) => ( ))}
{limitMsg && ( {limitMsg.text} )} ) : ( {config.deployment_limit} )}
)} {/* USER MANAGEMENT TAB (only if server sent users) */} {activeTab === 'users' && config.users && (
{config.users.map((user) => ( ))}
USERNAME ROLE STATUS ACTIONS
{user.username} {user.role.toUpperCase()} {user.must_change_password && ( MUST CHANGE PASSWORD )}
{/* Role change dropdown */} {/* Reset password */} {resetTarget === user.uuid ? (
setResetPassword(e.target.value)} style={{ width: '140px' }} />
) : ( )} {/* Delete */} {confirmDelete === user.uuid ? (
CONFIRM?
) : ( )}
setNewUsername(e.target.value)} required minLength={1} maxLength={64} />
setNewPassword(e.target.value)} required minLength={8} maxLength={72} />
{userMsg && ( {userMsg.text} )}
)} {/* GLOBAL VALUES TAB */} {activeTab === 'globals' && (
GLOBAL MUTATION INTERVAL {isAdmin ? ( <>
setIntervalInput(e.target.value)} placeholder="30m" />
FORMAT: <number><unit> — m=minutes, d=days, M=months, y=years (e.g. 30m, 7d, 1M) {intervalMsg && ( {intervalMsg.text} )} ) : ( {config.global_mutation_interval} )}
)} {/* WORKERS TAB (admin only, server-gated too) */} {activeTab === 'workers' && isAdmin && ( )} {/* APPEARANCE TAB */} {activeTab === 'appearance' && (
ACCENT COLOR

Swaps the UI accent (nav bars, hover glows, chip borders) between matrix-green and electric-violet. Persists per-browser.

{(['matrix', 'violet'] as const).map((value) => ( ))}
)} {/* DANGER ZONE — developer mode only, server-gated, shown on globals tab */} {activeTab === 'globals' && config.developer_mode && (
DANGER ZONE — DEVELOPER MODE

Purge all logs, bounty vault entries, and attacker profiles. This action is irreversible.

{!confirmReinit ? ( ) : (
THIS WILL DELETE ALL COLLECTED DATA. ARE YOU SURE?
)} {reinitMsg && ( {reinitMsg.text} )}
)}
); }; // ─── Workers panel ──────────────────────────────────────────────────────────── // Pollster view backed by GET /workers. Every 5s we pull the full snapshot; // the registry is cheap (in-memory dict) so there's no need for SSE here. interface WorkerStatusRow { name: string; status: 'ok' | 'stale' | 'unknown'; last_heartbeat_ts: number | null; seconds_since: number | null; extra: Record; installed: boolean; } interface WorkersPanelProps { pushToast: ReturnType['push']; } // Renders the LLM status of a realism-emitting worker (today: orchestrator). // Sourced from the heartbeat ``extra.realism`` payload published by // :func:`decnet.orchestrator.worker._realism_health_snapshot`. const RealismBadge: React.FC<{ realism: { llm_enabled?: boolean; llm_backend?: string | null; llm_model?: string | null; llm_breaker_state?: 'closed' | 'open' | 'half_open' | null; }; }> = ({ realism }) => { if (!realism.llm_enabled) { return ( LLM OFF ); } const breaker = realism.llm_breaker_state ?? 'closed'; const breakerColor = breaker === 'open' ? '#ff5555' : breaker === 'half_open' ? '#ffaa00' : 'var(--matrix)'; const tooltip = [ `Backend: ${realism.llm_backend ?? '?'}`, realism.llm_model ? `Model: ${realism.llm_model}` : null, `Circuit breaker: ${breaker}`, ].filter(Boolean).join('\n'); return ( LLM {(realism.llm_backend ?? 'on').toUpperCase()} ); }; const WorkersPanel: React.FC = ({ pushToast }) => { const [workers, setWorkers] = useState(null); const [busConnected, setBusConnected] = useState(null); const [err, setErr] = useState(null); const [stopping, setStopping] = useState>({}); const [starting, setStarting] = useState>({}); const [startingAll, setStartingAll] = useState(false); const fetchWorkers = async () => { try { const res = await api.get('/workers'); setWorkers(res.data?.workers ?? []); setBusConnected( typeof res.data?.bus_connected === 'boolean' ? res.data.bus_connected : null, ); setErr(null); } catch (e: any) { setErr(e?.response?.data?.detail || 'Failed to load workers'); } }; const [refreshing, setRefreshing] = useState(false); const [lastRefresh, setLastRefresh] = useState(null); const handleRefresh = async () => { setRefreshing(true); try { await fetchWorkers(); setLastRefresh(Date.now()); } finally { setRefreshing(false); } }; useEffect(() => { handleRefresh(); // eslint-disable-next-line react-hooks/exhaustive-deps }, []); const handleStop = async (name: string) => { setStopping((s) => ({ ...s, [name]: true })); try { await api.post(`/workers/${encodeURIComponent(name)}/stop`); pushToast({ text: `STOP REQUESTED · ${name.toUpperCase()}`, tone: 'violet', icon: 'terminal' }); // Kick a refresh sooner than the 5s tick so the UI feels responsive. setTimeout(fetchWorkers, 1000); } catch (e: any) { const detail = e?.response?.data?.detail || 'Stop failed'; pushToast({ text: `STOP FAILED · ${name.toUpperCase()} — ${detail}`, tone: 'alert', icon: 'alert-triangle' }); } finally { setStopping((s) => ({ ...s, [name]: false })); } }; const handleStart = async (name: string) => { setStarting((s) => ({ ...s, [name]: true })); try { await api.post(`/workers/${encodeURIComponent(name)}/start`); pushToast({ text: `START REQUESTED · ${name.toUpperCase()}`, tone: 'violet', icon: 'terminal' }); setTimeout(fetchWorkers, 1500); // Auto-clear the spinner state after 15s if the heartbeat still // hasn't flipped the row — keeps the UI from getting stuck. setTimeout(() => setStarting((s) => ({ ...s, [name]: false })), 15000); } catch (e: any) { const detail = e?.response?.data?.detail || 'Start failed'; pushToast({ text: `START FAILED · ${name.toUpperCase()} — ${detail}`, tone: 'alert', icon: 'alert-triangle' }); setStarting((s) => ({ ...s, [name]: false })); } }; const handleStartAll = async () => { setStartingAll(true); try { const res = await api.post('/workers/start-all'); const started: string[] = res.data?.started ?? []; const already: string[] = res.data?.already_running ?? []; const failed: Array<{ name: string; reason: string }> = res.data?.failed ?? []; const firstFail = failed[0]; const suffix = firstFail ? ` (first failure: ${firstFail.name} — ${firstFail.reason})` : ''; pushToast({ text: `STARTED · ${started.length} · ALREADY RUNNING · ${already.length} · FAILED · ${failed.length}${suffix}`, tone: failed.length > 0 ? 'alert' : 'violet', icon: failed.length > 0 ? 'alert-triangle' : 'terminal', }); setTimeout(fetchWorkers, 1500); } catch (e: any) { const detail = e?.response?.data?.detail || 'Start-all failed'; pushToast({ text: `START ALL FAILED — ${detail}`, tone: 'alert', icon: 'alert-triangle' }); } finally { setStartingAll(false); } }; const formatLastSeen = (row: WorkerStatusRow): string => { if (row.seconds_since == null) return '—'; const s = row.seconds_since; if (s < 60) return `${Math.floor(s)}s ago`; if (s < 3600) return `${Math.floor(s / 60)}m ago`; return `${Math.floor(s / 3600)}h ago`; }; const dotClass = (status: WorkerStatusRow['status']) => { if (status === 'ok') return 'status-dot active'; if (status === 'stale') return 'status-dot warn'; return 'status-dot idle'; }; if (err) { return (
{err}
); } if (workers === null) { return (
LOADING…
); } const busOffline = busConnected === false; return (
{busOffline && (
BUS OFFLINE — heartbeats cannot be received.
Start with decnet bus (restart the API if it was up first).
)}
HEARTBEATS EVERY 30s · OK < 90s · STALE AFTER {lastRefresh != null && ( · REFRESHED {new Date(lastRefresh).toLocaleTimeString()} )}
{workers.map((w) => { const isStopping = !!stopping[w.name]; const canStop = w.status === 'ok' && !isStopping && !busOffline; const realism = (w.extra && (w.extra as any).realism) as | { llm_enabled?: boolean; llm_backend?: string | null; llm_model?: string | null; llm_breaker_state?: 'closed' | 'open' | 'half_open' | null; } | undefined; return ( ); })}
NAME STATUS LAST SEEN ACTIONS
{w.name.toUpperCase()} {realism && } {w.status.toUpperCase()} {formatLastSeen(w)} {(() => { const isStarting = !!starting[w.name]; const canStart = w.installed && w.status !== 'ok' && !isStarting; const tooltip = !w.installed ? `Unit not installed — deploy decnet-${w.name}.service first.` : w.status === 'ok' ? 'Already running.' : isStarting ? 'Start request in flight…' : 'Start the worker via systemd.'; return ( ); })()}
); }; export default Config;