import React, { useEffect, useMemo, useState } from 'react'; import api from '../../utils/api'; import { useToast } from '../Toasts/useToast'; import { Save, RotateCcw, AlertTriangle } from '../../icons'; import { contentClassLabel, isCanaryClass } from '../../realism/labels'; // Reuse the DeckyFleet shell (page-header / btn / fleet-* / dim / mono) and // the persona-page tweaks (info-banner, .input) so the realism config panel // reads the same as the rest of the realism nav group. import '../DeckyFleet.css'; import '../PersonaGeneration.css'; import './RealismConfig.css'; // ─── 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, }; // ─── Subcomponent ──────────────────────────────────────────────────────────── const WeightTable: React.FC<{ title: string; help: string; weights: WeightEntry[]; onChange: (next: WeightEntry[]) => void; }> = ({ title, help, weights, onChange }) => { const total = weights.reduce((s, w) => s + Math.max(0, w.weight), 0); return ( <>
{title} TOTAL {total}
{help}
{weights.map((w, i) => { const canary = isCanaryClass(w.content_class); const share = total === 0 ? '—' : `${((Math.max(0, w.weight) / total) * 100).toFixed(1)}%`; return ( ); })}
{contentClassLabel(w.content_class)} {w.content_class} { const v = parseInt(e.target.value, 10); const next = weights.slice(); next[i] = { ...next[i], weight: Number.isFinite(v) ? Math.max(0, v) : 0, }; onChange(next); }} /> {share}
); }; // ─── 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 saved config on next save.', )) return; setConfig(DEFAULTS); }; const totals = useMemo(() => ({ user: config.user_class_weights.reduce((s, w) => s + Math.max(0, w.weight), 0), system: config.system_class_weights.reduce((s, w) => s + Math.max(0, w.weight), 0), canary: config.canary_class_weights.reduce((s, w) => s + Math.max(0, w.weight), 0), }), [config]); return (

REALISM CONFIG

USER {totals.user} · SYSTEM {totals.system} · CANARY {totals.canary} · {' '}CANARY PROB {(config.canary_probability * 100).toFixed(1)}%
Scope: tunes the orchestrator's realism planner {' '}— how often each kind of synthetic file lands on a decky, and how rare canary plants are. Persisted in the{' '} realism_config table; the orchestrator refreshes from the DB every ~5 minutes.
{error && (
{error}
)}
{loading ? (
Loading…
) : ( <> setConfig({ ...config, user_class_weights: next })} /> setConfig({ ...config, system_class_weights: next })} /> setConfig({ ...config, canary_class_weights: next })} />
Canary Probability {(config.canary_probability * 100).toFixed(1)}%
Share of file picks that materialise a canary. Each plant creates a real canary token row + DNS slug or HTTP URL — keeping this rare prevents a noisy alert surface.
setConfig({ ...config, canary_probability: parseFloat(e.target.value), })} /> {(config.canary_probability * 100).toFixed(1)}%
)}
); }; export default RealismConfig;