'use client'; import { useState } from 'react'; import { Bar } from '@/components/ui/primitives'; import { adminFetch } from '@/lib/admin-fetch'; import { fmt } from '@/lib/format'; import { COMPONENT_LABEL } from '@/lib/pressure'; import type { AdminConfig, ComponentId } from '@/lib/types'; import { AdminPage, ErrorNote, Panel, Toast, btnCls, btnPrimary, inputCls, useAdmin } from './shared'; export function Config() { const { data, err, reload } = useAdmin('/config'); // Edits are kept separately and overlay the fetched config (no effect-driven copy). const [edits, setEdits] = useState(null); const cfg: AdminConfig | null = edits ?? data; const setCfg = (c: AdminConfig) => setEdits(c); const [msg, setMsg] = useState(null); const [saveErr, setSaveErr] = useState(null); const [busy, setBusy] = useState(false); if (!cfg) return {err ? :

Loading…

}
; const sum = Object.values(cfg.pressure_weights).reduce((a, b) => a + Number(b || 0), 0); const valid = Math.abs(sum - 1) <= 0.001; const levelsOk = cfg.levels.every((l, i) => i === 0 || l.max > cfg.levels[i - 1]!.max) && cfg.levels.at(-1)?.max === 100; const dirty = JSON.stringify(cfg) !== JSON.stringify(data); const save = async () => { setBusy(true); setSaveErr(null); try { await adminFetch('/config', { method: 'PUT', body: cfg }); setMsg('Configuration stored — applied on the next engine cycle.'); setEdits(null); reload(); } catch (e) { setSaveErr(String(e)); } finally { setBusy(false); setTimeout(() => setMsg(null), 4000); } }; const setEngine = (k: string, v: string) => setCfg({ ...cfg, engine: { ...cfg.engine, [k]: v === '' ? '' : Number.isNaN(Number(v)) ? v : Number(v) } }); return ( } >
Σ {fmt(sum, 3)} {valid ? '✓' : '≠ 1.000'}}>
    {Object.entries(cfg.pressure_weights).map(([id, w]) => (
  • {COMPONENT_LABEL[id as ComponentId] ?? id} setCfg({ ...cfg, pressure_weights: { ...cfg.pressure_weights, [id]: Number(e.target.value) } })} className={`${inputCls} num w-full`} aria-label={`${id} weight`} />
  • ))}
{levelsOk ? 'monotonic, ends at 100' : 'must increase and end at 100'}}>
    {cfg.levels.map((l, i) => (
  • {l.id} setCfg({ ...cfg, levels: cfg.levels.map((x, j) => (j === i ? { ...x, max: Number(e.target.value) } : x)) })} className={`${inputCls} num w-full`} aria-label={`${l.id} max`} /> setCfg({ ...cfg, levels: cfg.levels.map((x, j) => (j === i ? { ...x, label: e.target.value } : x)) })} className={`${inputCls} w-full`} aria-label={`${l.id} label`} />
  • ))}
    {Object.entries(cfg.engine).map(([k, v]) => (
  • {k} setEngine(k, e.target.value)} className={`${inputCls} num w-full`} aria-label={k} />
  • ))}
Full JSON (events, fronts, scheduler, component signal recipes)
{JSON.stringify(cfg, null, 2)}
); }