import { useState, useCallback, useEffect, useMemo } from 'react'; import { useConfig, useRawConfig, useSources, useSinks, useSanitizers, useTerminators, useProfiles, } from '../api/queries/config'; import { useAddSource, useDeleteSource, useAddSink, useDeleteSink, useAddSanitizer, useDeleteSanitizer, useAddTerminator, useDeleteTerminator, useAddProfile, useDeleteProfile, useActivateProfile, useToggleTriageSync, useSaveRawConfig, } from '../api/mutations/config'; import { LoadingState } from '../components/ui/LoadingState'; import { ErrorState } from '../components/ui/ErrorState'; import { usePageTitle } from '../hooks/usePageTitle'; import { useToast } from '../contexts/ToastContext'; import { useTheme, type ThemePreference } from '../contexts/ThemeContext'; import type { LabelEntryView, TerminatorView, ProfileView } from '../api/types'; const THEME_OPTIONS: Array<{ value: ThemePreference; label: string }> = [ { value: 'light', label: 'Light' }, { value: 'dark', label: 'Dark' }, { value: 'system', label: 'System' }, { value: 'hc-light', label: 'High-contrast light' }, { value: 'hc-dark', label: 'High-contrast dark' }, ]; const LANG_OPTIONS = [ 'javascript', 'typescript', 'python', 'go', 'java', 'c', 'cpp', 'php', 'ruby', 'rust', ]; const CAP_OPTIONS = [ 'all', 'env_var', 'html_escape', 'shell_escape', 'url_encode', 'json_parse', 'file_io', 'sql_query', 'deserialize', 'ssrf', 'code_exec', 'crypto', ]; type Tab = 'overview' | 'rules' | 'profiles' | 'raw'; // ── Collapsible Config Section ─────────────────────────────────────────────── function ConfigSection({ title, id, defaultCollapsed = false, children, }: { title: string; id?: string; defaultCollapsed?: boolean; children: React.ReactNode; }) { const [collapsed, setCollapsed] = useState(defaultCollapsed); return (
setCollapsed(!collapsed)} > {' '} {title}
{children}
); } // ── Top-of-page settings panel (theme + triage sync) ──────────────────────── function SettingsSection({ triageSyncOn, onToggleTriageSync, }: { triageSyncOn: boolean; onToggleTriageSync: (enabled: boolean) => void; }) { const { preference, setPreference } = useTheme(); return (
Settings
onToggleTriageSync(e.target.checked)} />
); } // ── Read-only key/value grid for effective config display ─────────────────── function KvGrid({ entries }: { entries: Array<[string, React.ReactNode]> }) { return (
{entries.map(([k, v]) => (
{k}
{v}
))}
); } function fmt(v: unknown): React.ReactNode { if (v === null || v === undefined) return -; if (typeof v === 'boolean') return ( {v ? 'on' : 'off'} ); if (Array.isArray(v)) { if (v.length === 0) return []; return ( {v.map(String).map((s, i) => ( {s} ))} ); } if (typeof v === 'object') return {JSON.stringify(v)}; return {String(v)}; } // ── Custom rules table (no built-ins; built-ins live on /rules) ───────────── function CustomLabelSection({ title, id, kind, entries, onAdd, onDelete, }: { title: string; id: string; kind: 'source' | 'sink' | 'sanitizer'; entries: LabelEntryView[]; onAdd: (body: { lang: string; matchers: string[]; cap: string }) => void; onDelete: (entry: LabelEntryView) => void; }) { const [lang, setLang] = useState(''); const [matcher, setMatcher] = useState(''); const [cap, setCap] = useState('all'); const handleAdd = useCallback(() => { if (!lang || !matcher) return; onAdd({ lang, matchers: [matcher], cap }); setMatcher(''); }, [lang, matcher, cap, onAdd]); return (

Custom {kind} rules from your nyx.local. Built-in rules are listed on the Rules page.

setMatcher(e.target.value)} onKeyDown={(e) => { if (e.key === 'Enter') handleAdd(); }} />
{entries.length === 0 ? (

No custom {kind} rules yet

) : ( {entries.map((e, i) => ( ))}
Language Matchers Cap
{e.lang} {e.matchers.join(', ')} {e.cap}
)}
); } // ── Raw TOML editor ───────────────────────────────────────────────────────── function RawEditor() { const { data, isLoading, error, refetch } = useRawConfig(); const save = useSaveRawConfig(); const [draft, setDraft] = useState(null); const [saveError, setSaveError] = useState(null); const [savedAt, setSavedAt] = useState(null); // Seed the editor whenever we load fresh data and have no in-flight edit. useEffect(() => { if (data && draft === null) { setDraft(data.content); } // eslint-disable-next-line react-hooks/exhaustive-deps }, [data]); if (isLoading) return ; if (error) return ; if (!data) return null; const dirty = draft !== null && draft !== data.content; const handleSave = async () => { if (draft === null) return; setSaveError(null); try { await save.mutateAsync(draft); setSavedAt(Date.now()); // refresh disk view so {data.content} reflects what's on disk await refetch(); } catch (e) { setSaveError(e instanceof Error ? e.message : String(e)); } }; const handleDiscard = () => { setDraft(data.content); setSaveError(null); }; return (
nyx.local
{data.exists ? data.path : `${data.path} (will be created on save)`}
{dirty && Unsaved changes} {savedAt && !dirty && Saved}
{saveError && (
Save failed: {saveError}
)}