merge: testing → main (reconcile 2-week divergence)
This commit is contained in:
122
decnet_web/src/components/RealismConfig/RealismConfig.css
Normal file
122
decnet_web/src/components/RealismConfig/RealismConfig.css
Normal file
@@ -0,0 +1,122 @@
|
||||
/* Realism Config — layered on DeckyFleet.css.
|
||||
Adds: weight tables (label + numeric input + percent share),
|
||||
canary-class accent, canary-probability slider row. */
|
||||
|
||||
.realism-config-root .mono { font-family: var(--font-mono); }
|
||||
|
||||
.realism-config-root .info-banner {
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
border: 1px solid var(--border);
|
||||
border-left: 3px solid var(--violet);
|
||||
padding: 10px 14px;
|
||||
font-size: 0.78rem;
|
||||
line-height: 1.5;
|
||||
}
|
||||
.realism-config-root .info-banner em { color: var(--matrix); font-style: normal; }
|
||||
|
||||
/* Section heading above each weight table. */
|
||||
.realism-config-root .section-head {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: baseline;
|
||||
font-size: 0.7rem;
|
||||
letter-spacing: 1.5px;
|
||||
color: var(--dim);
|
||||
text-transform: uppercase;
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
.realism-config-root .section-head .total {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--matrix);
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
.realism-config-root .section-help {
|
||||
font-size: 0.7rem;
|
||||
opacity: 0.45;
|
||||
letter-spacing: 0.5px;
|
||||
margin: -4px 0 8px;
|
||||
}
|
||||
|
||||
/* Weight table: class label · raw enum · weight input · percent share. */
|
||||
.realism-config-root .weight-table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
margin-bottom: 24px;
|
||||
font-size: 0.82rem;
|
||||
}
|
||||
.realism-config-root .weight-table tr {
|
||||
border-bottom: 1px solid var(--border);
|
||||
}
|
||||
.realism-config-root .weight-table tr:last-child { border-bottom: none; }
|
||||
.realism-config-root .weight-table td {
|
||||
padding: 8px 14px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
.realism-config-root .weight-table td.cls { width: 50%; }
|
||||
.realism-config-root .weight-table td.cls .cls-label { font-weight: 600; }
|
||||
.realism-config-root .weight-table td.cls .cls-enum {
|
||||
margin-left: 10px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.7rem;
|
||||
color: var(--dim);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
.realism-config-root .weight-table td.cls.canary .cls-label {
|
||||
color: var(--amber, #f59e0b);
|
||||
}
|
||||
.realism-config-root .weight-table td.weight {
|
||||
width: 130px;
|
||||
}
|
||||
.realism-config-root .weight-table input.weight-input {
|
||||
width: 90px;
|
||||
background: var(--bg-elev, rgba(0, 0, 0, 0.3));
|
||||
border: 1px solid var(--border);
|
||||
color: var(--matrix);
|
||||
padding: 5px 9px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 0.85rem;
|
||||
outline: none;
|
||||
transition: border-color 0.15s, box-shadow 0.15s;
|
||||
text-align: right;
|
||||
}
|
||||
.realism-config-root .weight-table input.weight-input:focus {
|
||||
border-color: var(--violet);
|
||||
box-shadow: 0 0 0 1px var(--violet);
|
||||
}
|
||||
.realism-config-root .weight-table td.share {
|
||||
font-family: var(--font-mono);
|
||||
color: var(--dim);
|
||||
width: 80px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
/* Canary probability slider row. */
|
||||
.realism-config-root .prob-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
background: var(--panel);
|
||||
border: 1px solid var(--border);
|
||||
padding: 12px 16px;
|
||||
margin-bottom: 24px;
|
||||
}
|
||||
.realism-config-root .prob-row input[type="range"] {
|
||||
flex: 1;
|
||||
accent-color: var(--violet);
|
||||
}
|
||||
.realism-config-root .prob-row .prob-value {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 1rem;
|
||||
color: var(--matrix);
|
||||
letter-spacing: 1px;
|
||||
min-width: 70px;
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.realism-config-root .footer-actions {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
264
decnet_web/src/components/RealismConfig/RealismConfig.tsx
Normal file
264
decnet_web/src/components/RealismConfig/RealismConfig.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
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 (
|
||||
<>
|
||||
<div className="section-head">
|
||||
<span>{title}</span>
|
||||
<span className="total">TOTAL {total}</span>
|
||||
</div>
|
||||
<div className="section-help">{help}</div>
|
||||
<table className="weight-table">
|
||||
<tbody>
|
||||
{weights.map((w, i) => {
|
||||
const canary = isCanaryClass(w.content_class);
|
||||
const share =
|
||||
total === 0
|
||||
? '—'
|
||||
: `${((Math.max(0, w.weight) / total) * 100).toFixed(1)}%`;
|
||||
return (
|
||||
<tr key={w.content_class}>
|
||||
<td className={`cls${canary ? ' canary' : ''}`}>
|
||||
<span className="cls-label">{contentClassLabel(w.content_class)}</span>
|
||||
<span className="cls-enum">{w.content_class}</span>
|
||||
</td>
|
||||
<td className="weight">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
step={1}
|
||||
className="weight-input"
|
||||
value={w.weight}
|
||||
onChange={(e) => {
|
||||
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);
|
||||
}}
|
||||
/>
|
||||
</td>
|
||||
<td className="share">{share}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
// ─── Page ────────────────────────────────────────────────────────────────────
|
||||
|
||||
const RealismConfig: React.FC = () => {
|
||||
const { push } = useToast();
|
||||
const [config, setConfig] = useState<ConfigPayload>(DEFAULTS);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [saving, setSaving] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
const fetchConfig = async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await api.get<ConfigPayload>('/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<ConfigPayload>('/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 (
|
||||
<div className="fleet-root realism-config-root">
|
||||
<div className="page-header">
|
||||
<div className="page-title-group">
|
||||
<h1>REALISM CONFIG</h1>
|
||||
<span className="page-sub">
|
||||
USER {totals.user} · SYSTEM {totals.system} · CANARY {totals.canary} ·
|
||||
{' '}CANARY PROB {(config.canary_probability * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
<div className="actions">
|
||||
<button
|
||||
className="btn ghost"
|
||||
onClick={handleReset}
|
||||
disabled={saving || loading}
|
||||
title="Reset form fields to baked-in defaults (does not save until you press SAVE)"
|
||||
>
|
||||
<RotateCcw size={12} /> RESET
|
||||
</button>
|
||||
<button
|
||||
className="btn violet"
|
||||
onClick={handleSave}
|
||||
disabled={saving || loading}
|
||||
title="Persist current values to realism_config; orchestrator picks them up within one refresh tick (~5 min)."
|
||||
>
|
||||
<Save size={12} /> {saving ? 'SAVING…' : 'SAVE'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="info-banner">
|
||||
<div>
|
||||
<strong>Scope:</strong> tunes the orchestrator's <em>realism planner</em>
|
||||
{' '}— how often each kind of synthetic file lands on a decky, and
|
||||
how rare canary plants are. Persisted in the{' '}
|
||||
<span className="mono matrix-text">realism_config</span> table; the
|
||||
orchestrator refreshes from the DB every ~5 minutes.
|
||||
</div>
|
||||
{error && (
|
||||
<div className="info-line alert-text" style={{ marginTop: 8 }}>
|
||||
<AlertTriangle size={12} /> {error}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="dim" style={{ padding: '24px 0' }}>Loading…</div>
|
||||
) : (
|
||||
<>
|
||||
<WeightTable
|
||||
title="User Class Weights"
|
||||
help="Files written by personas during their work hours. The realism win when a persona looks busy."
|
||||
weights={config.user_class_weights}
|
||||
onChange={(next) => setConfig({ ...config, user_class_weights: next })}
|
||||
/>
|
||||
<WeightTable
|
||||
title="System Class Weights"
|
||||
help="Plausible OS-side filler — rotated logs, daemon noise, ephemeral cache."
|
||||
weights={config.system_class_weights}
|
||||
onChange={(next) => setConfig({ ...config, system_class_weights: next })}
|
||||
/>
|
||||
<WeightTable
|
||||
title="Canary Class Weights"
|
||||
help="Callback-bearing artifacts. Uniform across generators by default; raise one to bias toward a specific bait flavour."
|
||||
weights={config.canary_class_weights}
|
||||
onChange={(next) => setConfig({ ...config, canary_class_weights: next })}
|
||||
/>
|
||||
|
||||
<div className="section-head">
|
||||
<span>Canary Probability</span>
|
||||
<span className="total">{(config.canary_probability * 100).toFixed(1)}%</span>
|
||||
</div>
|
||||
<div className="section-help">
|
||||
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.
|
||||
</div>
|
||||
<div className="prob-row">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.005}
|
||||
value={config.canary_probability}
|
||||
onChange={(e) => setConfig({
|
||||
...config,
|
||||
canary_probability: parseFloat(e.target.value),
|
||||
})}
|
||||
/>
|
||||
<span className="prob-value">
|
||||
{(config.canary_probability * 100).toFixed(1)}%
|
||||
</span>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default RealismConfig;
|
||||
Reference in New Issue
Block a user