From 55e86f606c55b6f714a2e19cbc77ba2c3fc50ae7 Mon Sep 17 00:00:00 2001 From: anti Date: Mon, 27 Apr 2026 17:48:05 -0400 Subject: [PATCH] feat(realism-ui): synthetic files browser MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New /synthetic-files page sits next to Persona Generation and Canary Tokens under the Automation nav group. Operators get a paginated inventory of files realism has grown across the fleet (decky, path, persona, content_class, last_modified, edit_count, hash) with filters on decky / persona / content_class. Decky filter is a dropdown sourced from /deckies — never free text. Row click opens a drawer with the body preview; the drawer surfaces a TRUNCATED chip when the stored body is at the 64KB cap. --- decnet_web/src/App.tsx | 2 + decnet_web/src/components/Layout.tsx | 4 +- .../SyntheticFiles/SyntheticFiles.tsx | 358 ++++++++++++++++++ 3 files changed, 363 insertions(+), 1 deletion(-) create mode 100644 decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx diff --git a/decnet_web/src/App.tsx b/decnet_web/src/App.tsx index dfbb90c7..aef67124 100644 --- a/decnet_web/src/App.tsx +++ b/decnet_web/src/App.tsx @@ -25,6 +25,7 @@ const Campaigns = lazy(() => import('./components/Campaigns')); 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 CanaryTokens = lazy(() => import('./components/CanaryTokens')); const TopologyPersonaGeneration = lazy(() => import('./components/PersonaGeneration').then((m) => ({ default: m.TopologyPersonaGeneration })), @@ -129,6 +130,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 6f3b2df2..c4167fa8 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, + Target, FileText, } from '../icons'; import { prefetchRoute } from '../routePrefetch'; import './Layout.css'; @@ -36,6 +36,7 @@ const ROUTE_LABELS: Record = { '/campaigns': 'CAMPAIGNS', '/orchestrator': 'ORCHESTRATOR', '/persona-generation': 'PERSONA GENERATION', + '/synthetic-files': 'SYNTHETIC FILES', '/canary-tokens': 'CANARY TOKENS', '/config': 'CONFIG', '/swarm-updates': 'REMOTE UPDATES', @@ -142,6 +143,7 @@ const Layout: React.FC = ({ } open={sidebarOpen}> } label="Orchestrator" open={sidebarOpen} indent /> } label="Persona Generation" open={sidebarOpen} indent /> + } label="Synthetic Files" open={sidebarOpen} indent /> } label="Canary Tokens" open={sidebarOpen} indent /> } open={sidebarOpen}> diff --git a/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx b/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx new file mode 100644 index 00000000..b382bc59 --- /dev/null +++ b/decnet_web/src/components/SyntheticFiles/SyntheticFiles.tsx @@ -0,0 +1,358 @@ +import React, { useEffect, useMemo, useRef, useState } from 'react'; +import api from '../../utils/api'; +import { useEscapeKey } from '../../hooks/useEscapeKey'; +import { useFocusTrap } from '../../hooks/useFocusTrap'; +import { X, FileText } from '../../icons'; + +// ─── Types ─────────────────────────────────────────────────────────────────── + +interface SyntheticFileRow { + uuid: string; + decky_uuid: string; + path: string; + persona: string; + content_class: string; + created_at: string; + last_modified: string; + edit_count: number; + content_hash: string; +} + +interface SyntheticFileDetail extends SyntheticFileRow { + last_body: string; + truncated: boolean; +} + +interface PaginatedResponse { + total: number; + limit: number; + offset: number; + data: SyntheticFileRow[]; +} + +interface DeckyOption { + uuid: string; + name: string; +} + +const PAGE_SIZE = 50; + +// Fixed list of content_class values mirroring decnet/realism/taxonomy.py. +// A static dropdown beats free-text — the operator sees what's actually +// available without a typo path failing silently. +const CONTENT_CLASSES = [ + 'note', 'todo', 'draft', 'script', + 'log_cron', 'log_daemon', + 'cache_tmp', 'config_local', + 'canary_aws_creds', 'canary_env_file', 'canary_git_config', + 'canary_ssh_key', 'canary_honeydoc', 'canary_honeydoc_docx', + 'canary_honeydoc_pdf', 'canary_mysql_dump', +] as const; + +// ─── Helpers ───────────────────────────────────────────────────────────────── + +function fmt(iso: string): string { + const d = new Date(iso); + if (Number.isNaN(d.getTime())) return iso; + const pad = (n: number) => String(n).padStart(2, '0'); + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}`; +} + +function deckyLabel(uuid: string, deckies: DeckyOption[]): string { + const d = deckies.find((d) => d.uuid === uuid); + return d ? d.name : `${uuid.slice(0, 8)}…`; +} + +// ─── Drawer ────────────────────────────────────────────────────────────────── + +interface DrawerProps { + uuid: string; + deckies: DeckyOption[]; + onClose: () => void; +} + +const SyntheticFileDrawer: React.FC = ({ uuid, deckies, onClose }) => { + const panelRef = useRef(null); + useEscapeKey(onClose, true); + useFocusTrap(panelRef, true); + useEffect(() => { + const prev = document.body.style.overflow; + document.body.style.overflow = 'hidden'; + return () => { document.body.style.overflow = prev; }; + }, []); + + const [row, setRow] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + useEffect(() => { + let cancelled = false; + setLoading(true); + setError(null); + api.get(`/realism/synthetic-files/${encodeURIComponent(uuid)}`) + .then((res) => { if (!cancelled) setRow(res.data); }) + .catch((err: any) => { + if (cancelled) return; + setError(err?.response?.status === 404 ? 'File no longer exists.' : 'Load failed.'); + }) + .finally(() => { if (!cancelled) setLoading(false); }); + return () => { cancelled = true; }; + }, [uuid]); + + return ( +
{ if (e.target === e.currentTarget) onClose(); }} + style={{ + position: 'fixed', inset: 0, + backgroundColor: 'rgba(0,0,0,0.6)', + display: 'flex', justifyContent: 'flex-end', + zIndex: 1000, + }} + > +
+
+
+
+ SYNTHETIC FILE {row ? `· ${deckyLabel(row.decky_uuid, deckies)}` : ''} +
+
+ {row?.path ?? uuid} +
+
+ +
+ + {loading &&
Loading…
} + {error &&
{error}
} + + {row && ( + <> +
+
PERSONA
{row.persona}
+
CONTENT CLASS
{row.content_class}
+
EDIT COUNT
{row.edit_count}
+
CREATED
{fmt(row.created_at)}
+
LAST MODIFIED
{fmt(row.last_modified)}
+
CONTENT HASH
+
{row.content_hash}
+
+ +
+
+ + BODY PREVIEW ({(row.last_body?.length ?? 0).toLocaleString()} bytes) + + {row.truncated && ( + + TRUNCATED + + )} +
+
+                {row.last_body || }
+              
+
+ + )} +
+
+ ); +}; + +// ─── Page ──────────────────────────────────────────────────────────────────── + +const SyntheticFiles: React.FC = () => { + const [rows, setRows] = useState([]); + const [total, setTotal] = useState(0); + const [page, setPage] = useState(0); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + + const [deckies, setDeckies] = useState([]); + const [deckyFilter, setDeckyFilter] = useState(''); // '' = all + const [personaFilter, setPersonaFilter] = useState(''); + const [classFilter, setClassFilter] = useState(''); + + const [selectedUuid, setSelectedUuid] = useState(null); + + useEffect(() => { + api.get('/deckies') + .then((res) => setDeckies(Array.isArray(res.data) ? res.data : [])) + .catch(() => setDeckies([])); + }, []); + + const personaOptions = useMemo(() => { + const set = new Set(); + rows.forEach((r) => set.add(r.persona)); + return Array.from(set).sort(); + }, [rows]); + + const fetchRows = async () => { + setLoading(true); + setError(null); + try { + const params = new URLSearchParams(); + params.set('limit', String(PAGE_SIZE)); + params.set('offset', String(page * PAGE_SIZE)); + if (deckyFilter) params.set('decky_uuid', deckyFilter); + if (personaFilter) params.set('persona', personaFilter); + if (classFilter) params.set('content_class', classFilter); + const res = await api.get( + `/realism/synthetic-files?${params.toString()}`, + ); + setRows(res.data.data); + setTotal(res.data.total); + } catch (err: any) { + setError(err?.response?.status === 401 ? 'Authentication required.' : 'Load failed.'); + } finally { + setLoading(false); + } + }; + + useEffect(() => { fetchRows(); /* eslint-disable-next-line react-hooks/exhaustive-deps */ }, [ + page, deckyFilter, personaFilter, classFilter, + ]); + + const totalPages = Math.max(1, Math.ceil(total / PAGE_SIZE)); + + return ( +
+
+ +

SYNTHETIC FILES

+ + {total} total + +
+ +
+ + + + + +
+ + {error &&
{error}
} + +
+ + + + + + + + + + + + + + {loading && ( + + )} + {!loading && rows.length === 0 && ( + + )} + {!loading && rows.map((r) => ( + setSelectedUuid(r.uuid)} + style={{ cursor: 'pointer', borderBottom: '1px solid rgba(255,255,255,0.03)' }} + > + + + + + + + + + ))} + +
DECKYPATHPERSONACLASSLAST MODIFIEDEDITSHASH
Loading…
+ No files match the current filters. +
{deckyLabel(r.decky_uuid, deckies)}{r.path}{r.persona}{r.content_class}{fmt(r.last_modified)}{r.edit_count}{r.content_hash.slice(0, 12)}…
+
+ +
+ + + Page {page + 1} / {totalPages} + + +
+ + {selectedUuid && ( + setSelectedUuid(null)} + /> + )} +
+ ); +}; + +export default SyntheticFiles;