Implemented confi snapshots

This commit is contained in:
Martino Ferrari
2026-06-21 23:50:03 +02:00
parent 04d31a15c4
commit 113e5a0fe8
51 changed files with 4921 additions and 241 deletions
+72
View File
@@ -4,6 +4,7 @@ import SignalPicker, { SignalOption } from './SignalPicker';
import LuaEditor from './LuaEditor';
import type { SignalDef, SynthGraph, SynthGraphNode } from './lib/types';
import { inferNodeTypes, SynthType } from './lib/synthTypes';
import { VersionTree, DiffViewer } from './VersionHistory';
interface DataSource { name: string; }
interface SignalInfo { name: string; type?: string; }
@@ -313,6 +314,11 @@ export default function SyntheticGraphEditor({ name, create, panelId, onClose, o
// Quick-add HUD (S = signal, N = node); null when closed.
const [hud, setHud] = useState<null | 'signal' | 'node'>(null);
const [hudFilter, setHudFilter] = useState('');
// Version history pane (existing signals only).
const [showVersions, setShowVersions] = useState(false);
const [viewingVersion, setViewingVersion] = useState<number | null>(null);
const [diff, setDiff] = useState<{ av: number; bv: number } | null>(null);
const [verReload, setVerReload] = useState(0);
// Identity fields — editable only when creating a new signal.
const [newName, setNewName] = useState('');
@@ -710,6 +716,44 @@ export default function SyntheticGraphEditor({ name, create, panelId, onClose, o
}
}
// Apply a fetched SignalDef to the editor (meta fields + rebuilt node graph).
function applyDef(d: SignalDef, asEdit: boolean) {
setDef(d);
setUnit(d.meta?.unit ?? '');
setDesc(d.meta?.description ?? '');
setDispLow(String(d.meta?.displayLow ?? '0'));
setDispHigh(String(d.meta?.displayHigh ?? '100'));
const g = buildInitial(d);
if (asEdit) commit(g); else { undoStack.current = []; redoStack.current = []; setGraph(g); bump(); }
g.nodes.forEach(n => { if (n.kind === 'source' && n.ds) loadSignals(n.ds); });
}
// Load a past revision into the editor as an undoable edit; saving promotes it
// to a new current version (non-destructive).
async function viewVersion(version: number) {
if (canUndo && !confirm('Discard unsaved changes to load this version?')) return;
try {
const res = await fetch(`/api/v1/synthetic/${encodeURIComponent(sigName)}/versions/${version}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
const d: SignalDef = await res.json();
applyDef(d, false);
setViewingVersion(version);
} catch (err) {
setError(`Load version failed: ${err instanceof Error ? err.message : err}`);
}
}
async function reloadCurrent() {
try {
const res = await fetch(`/api/v1/synthetic/${encodeURIComponent(sigName)}`);
if (!res.ok) throw new Error(`HTTP ${res.status}`);
applyDef(await res.json(), false);
setViewingVersion(null);
} catch (err) {
setError(`Reload failed: ${err instanceof Error ? err.message : err}`);
}
}
function nodeClass(n: GNode): string {
const cat = n.kind === 'source' ? 'trigger' : n.kind === 'output' ? 'action' : 'flow';
const err = nodeErrors.has(n.id) ? ' flow-node-error' : '';
@@ -721,6 +765,10 @@ export default function SyntheticGraphEditor({ name, create, panelId, onClose, o
<div class="synth-graph" onClick={(e) => e.stopPropagation()}>
<div class="wizard-header">
<span>Synthetic Signal{create ? ' — new' : `${name}`}</span>
{!create && (
<button class={`toolbar-btn${showVersions ? ' toolbar-btn-primary' : ''}`}
style="margin-left:auto;" onClick={() => setShowVersions(v => !v)}>History</button>
)}
<button class="icon-btn" onClick={onClose}></button>
</div>
@@ -982,6 +1030,30 @@ export default function SyntheticGraphEditor({ name, create, panelId, onClose, o
</div>
)}
{!create && showVersions && (
<div class="ver-pane synth-ver-pane">
<div class="ver-pane-head">Version history
<button class="icon-btn" onClick={() => setShowVersions(false)}>✕</button>
</div>
<VersionTree
base={`/api/v1/synthetic/${encodeURIComponent(sigName)}`}
viewing={viewingVersion}
dirty={canUndo}
reloadKey={verReload}
onView={viewVersion}
onForked={() => { onSaved(); setVerReload(k => k + 1); }}
onPromoted={async () => { await reloadCurrent(); onSaved(); setVerReload(k => k + 1); }}
onCompare={(av, bv) => setDiff({ av, bv })}
onError={setError} />
</div>
)}
{diff && (
<DiffViewer base={`/api/v1/synthetic/${encodeURIComponent(sigName)}`}
av={diff.av} bv={diff.bv} title={`${sigName} — v${diff.av} → v${diff.bv}`}
onClose={() => setDiff(null)} />
)}
<div class="wizard-footer">
{error && <span class="wizard-error" style="flex:1;">{error}</span>}
{!error && firstError && <span class="hint" style="flex:1;">{firstError}</span>}