From 56a88d7bd4db4f374c130942a8cdac71a3d4b12f Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 27 Apr 2026 18:01:35 -0400 Subject: [PATCH] feat(realism-ui): operator panel for planner weights + canary probability MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New /realism-config page sits next to Persona Generation and Synthetic Files under the Automation nav. Editable weight tables for user / system / canary content classes (with live percent share), plus a slider for canary_probability. Wires GET/PUT /api/v1/realism/config — viewer can read; admin required to save. Validation errors from the API are surfaced inline rather than swallowed; the SAVE button refreshes from the server's canonical snapshot so the operator sees exactly what landed (matters because cross-list entries are silently dropped server-side). --- decnet_web/src/App.tsx | 2 + decnet_web/src/components/Layout.tsx | 4 +- .../RealismConfig/RealismConfig.tsx | 250 ++++++++++++++++++ 3 files changed, 255 insertions(+), 1 deletion(-) create mode 100644 decnet_web/src/components/RealismConfig/RealismConfig.tsx diff --git a/decnet_web/src/App.tsx b/decnet_web/src/App.tsx index aef67124..db77e04d 100644 --- a/decnet_web/src/App.tsx +++ b/decnet_web/src/App.tsx @@ -26,6 +26,7 @@ const CampaignDetail = lazy(() => import('./components/CampaignDetail')); const Orchestrator = lazy(() => import('./components/Orchestrator')); const PersonaGeneration = lazy(() => import('./components/PersonaGeneration')); const SyntheticFiles = lazy(() => import('./components/SyntheticFiles/SyntheticFiles')); +const RealismConfig = lazy(() => import('./components/RealismConfig/RealismConfig')); const CanaryTokens = lazy(() => import('./components/CanaryTokens')); const TopologyPersonaGeneration = lazy(() => import('./components/PersonaGeneration').then((m) => ({ default: m.TopologyPersonaGeneration })), @@ -131,6 +132,7 @@ const AuthedShell: React.FC = ({ onLogout, onSearch, searchQue } /> } /> } /> + } /> } /> } /> } /> diff --git a/decnet_web/src/components/Layout.tsx b/decnet_web/src/components/Layout.tsx index c4167fa8..64517ad2 100644 --- a/decnet_web/src/components/Layout.tsx +++ b/decnet_web/src/components/Layout.tsx @@ -4,7 +4,7 @@ import { Menu, X, Search, Activity, LayoutDashboard, Terminal, Settings, LogOut, Server, Archive, Package, Network, ChevronDown, ChevronRight, HardDrive, ShieldAlert, Bell, Webhook, Lock, Crosshair, Fingerprint, Zap, Cpu, Mail, - Target, FileText, + Target, FileText, Sliders, } from '../icons'; import { prefetchRoute } from '../routePrefetch'; import './Layout.css'; @@ -37,6 +37,7 @@ const ROUTE_LABELS: Record = { '/orchestrator': 'ORCHESTRATOR', '/persona-generation': 'PERSONA GENERATION', '/synthetic-files': 'SYNTHETIC FILES', + '/realism-config': 'REALISM CONFIG', '/canary-tokens': 'CANARY TOKENS', '/config': 'CONFIG', '/swarm-updates': 'REMOTE UPDATES', @@ -144,6 +145,7 @@ const Layout: React.FC = ({ } label="Orchestrator" open={sidebarOpen} indent /> } label="Persona Generation" open={sidebarOpen} indent /> } label="Synthetic Files" open={sidebarOpen} indent /> + } label="Realism Config" open={sidebarOpen} indent /> } label="Canary Tokens" open={sidebarOpen} indent /> } open={sidebarOpen}> diff --git a/decnet_web/src/components/RealismConfig/RealismConfig.tsx b/decnet_web/src/components/RealismConfig/RealismConfig.tsx new file mode 100644 index 00000000..042453aa --- /dev/null +++ b/decnet_web/src/components/RealismConfig/RealismConfig.tsx @@ -0,0 +1,250 @@ +import React, { useEffect, useState } from 'react'; +import api from '../../utils/api'; +import { useToast } from '../Toasts/useToast'; +import { Sliders, Save, RotateCcw } from '../../icons'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +interface WeightEntry { + content_class: string; + weight: number; +} + +interface ConfigPayload { + user_class_weights: WeightEntry[]; + system_class_weights: WeightEntry[]; + canary_class_weights: WeightEntry[]; + canary_probability: number; +} + +const DEFAULTS: ConfigPayload = { + user_class_weights: [ + { content_class: 'note', weight: 30 }, + { content_class: 'todo', weight: 20 }, + { content_class: 'draft', weight: 15 }, + { content_class: 'script', weight: 10 }, + ], + system_class_weights: [ + { content_class: 'log_cron', weight: 12 }, + { content_class: 'log_daemon', weight: 8 }, + { content_class: 'cache_tmp', weight: 5 }, + ], + canary_class_weights: [ + { content_class: 'canary_aws_creds', weight: 1 }, + { content_class: 'canary_env_file', weight: 1 }, + { content_class: 'canary_git_config', weight: 1 }, + { content_class: 'canary_ssh_key', weight: 1 }, + { content_class: 'canary_honeydoc', weight: 1 }, + { content_class: 'canary_honeydoc_docx', weight: 1 }, + { content_class: 'canary_honeydoc_pdf', weight: 1 }, + { content_class: 'canary_mysql_dump', weight: 1 }, + ], + canary_probability: 0.03, +}; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function pct(weights: WeightEntry[], idx: number): string { + const total = weights.reduce((s, w) => s + Math.max(0, w.weight), 0); + if (total === 0) return '—'; + return `${((weights[idx].weight / total) * 100).toFixed(1)}%`; +} + +// ─── Subcomponent ──────────────────────────────────────────────────────────── + +const WeightTable: React.FC<{ + title: string; + weights: WeightEntry[]; + onChange: (next: WeightEntry[]) => void; +}> = ({ title, weights, onChange }) => { + const total = weights.reduce((s, w) => s + Math.max(0, w.weight), 0); + return ( +
+
+ {title} · TOTAL {total} +
+ + + {weights.map((w, i) => ( + + + + + + ))} + +
+ {w.content_class} + + { + const next = weights.slice(); + const v = parseInt(e.target.value, 10); + next[i] = { ...next[i], weight: Number.isFinite(v) ? Math.max(0, v) : 0 }; + onChange(next); + }} + style={{ + width: '80px', + backgroundColor: 'rgba(255,255,255,0.03)', + color: 'var(--text-color)', + border: '1px solid rgba(255,255,255,0.1)', + padding: '4px 8px', fontFamily: 'inherit', + }} + /> + + {pct(weights, i)} +
+
+ ); +}; + +// ─── Page ──────────────────────────────────────────────────────────────────── + +const RealismConfig: React.FC = () => { + const { push } = useToast(); + const [config, setConfig] = useState(DEFAULTS); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [error, setError] = useState(null); + + const fetchConfig = async () => { + setLoading(true); + setError(null); + try { + const res = await api.get('/realism/config'); + setConfig(res.data); + } catch (err: any) { + setError(err?.response?.status === 401 ? 'Authentication required.' : 'Load failed.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchConfig(); }, []); + + const handleSave = async () => { + setSaving(true); + setError(null); + try { + const res = await api.put('/realism/config', config); + setConfig(res.data); + push({ text: 'REALISM CONFIG SAVED', tone: 'matrix', icon: 'terminal' }); + } catch (err: any) { + const detail = err?.response?.data?.detail; + const status = err?.response?.status; + if (status === 403) setError('Admin role required to save.'); + else if (status === 400 && detail) setError(`Validation failed: ${detail}`); + else setError('Save failed.'); + } finally { + setSaving(false); + } + }; + + const handleReset = () => { + if (!window.confirm('Reset to baked-in defaults? This will overwrite the current saved config on next save.')) return; + setConfig(DEFAULTS); + }; + + return ( +
+
+ +

REALISM CONFIG

+
+

+ Operator-tuned planner weights. The orchestrator refreshes from the DB + every ~5 minutes; saved changes land within one refresh window. +

+ + {error &&
{error}
} + + {loading ? ( +
Loading…
+ ) : ( + <> + setConfig({ ...config, user_class_weights: next })} + /> + setConfig({ ...config, system_class_weights: next })} + /> + setConfig({ ...config, canary_class_weights: next })} + /> + +
+
+ CANARY PROBABILITY · share of file picks that materialise a canary +
+
+ setConfig({ + ...config, + canary_probability: parseFloat(e.target.value), + })} + style={{ flex: 1 }} + /> + + {(config.canary_probability * 100).toFixed(1)}% + +
+
+ +
+ + +
+ + )} +
+ ); +}; + +export default RealismConfig;