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}
>
);
};
// ─── 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)}%
RESET
{saving ? 'SAVING…' : 'SAVE'}
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 && (
)}
{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;