import { h } from 'preact'; import { useState, useEffect } from 'preact/hooks'; interface NodeParam { label: string; key: string; type: 'number' | 'text'; default: string; } const NODE_TYPES: Array<{ type: string; label: string; params: NodeParam[] }> = [ { type: 'gain', label: 'Gain', params: [{ label: 'Factor', key: 'gain', type: 'number', default: '1' }] }, { type: 'offset', label: 'Offset', params: [{ label: 'Offset', key: 'offset', type: 'number', default: '0' }] }, { type: 'moving_average', label: 'Moving Average', params: [{ label: 'Window (n)', key: 'window', type: 'number', default: '10' }] }, { type: 'rms', label: 'RMS', params: [{ label: 'Window (n)', key: 'window', type: 'number', default: '10' }] }, { type: 'derivative', label: 'Derivative', params: [] }, { type: 'clamp', label: 'Clamp', params: [{ label: 'Min', key: 'min', type: 'number', default: '0' }, { label: 'Max', key: 'max', type: 'number', default: '100' }] }, { type: 'expr', label: 'Formula', params: [{ label: 'Expression (a=input)', key: 'expr', type: 'text', default: 'a * 1.0' }] }, { type: 'lua', label: 'Lua Script', params: [{ label: 'Script (a=input)', key: 'script', type: 'text', default: 'return a' }] }, ]; interface PipelineNode { type: string; params: Record; } interface SignalDef { name: string; ds: string; signal: string; pipeline: PipelineNode[]; meta: { unit?: string; description?: string; displayLow?: number; displayHigh?: number; }; } interface Props { name: string; onClose: () => void; onSaved: () => void; } export default function SyntheticEditor({ name, onClose, onSaved }: Props) { const [def, setDef] = useState(null); const [pipeline, setPipeline] = useState([]); const [addType, setAddType] = useState('gain'); const [loading, setLoading] = useState(true); const [saving, setSaving] = useState(false); const [error, setError] = useState(''); useEffect(() => { fetch(`/api/v1/synthetic/${encodeURIComponent(name)}`) .then(r => r.ok ? r.json() : Promise.reject(r.statusText)) .then((d: SignalDef) => { setDef(d); setPipeline(d.pipeline ?? []); }) .catch(e => setError(String(e))) .finally(() => setLoading(false)); }, [name]); function nodeLabel(type: string): string { return NODE_TYPES.find(n => n.type === type)?.label ?? type; } function nodeParamDefs(type: string): NodeParam[] { return NODE_TYPES.find(n => n.type === type)?.params ?? []; } function setNodeParam(idx: number, key: string, val: string) { setPipeline(p => p.map((node, i) => { if (i !== idx) return node; const paramDef = nodeParamDefs(node.type).find(pd => pd.key === key); const typed: any = paramDef?.type === 'number' ? parseFloat(val) : val; return { ...node, params: { ...node.params, [key]: typed } }; })); } function removeNode(idx: number) { setPipeline(p => p.filter((_, i) => i !== idx)); } function moveNode(idx: number, dir: -1 | 1) { setPipeline(p => { const next = [...p]; const target = idx + dir; if (target < 0 || target >= next.length) return next; [next[idx], next[target]] = [next[target], next[idx]]; return next; }); } function addNode() { const nodeDef = NODE_TYPES.find(n => n.type === addType)!; const params: Record = {}; for (const p of nodeDef.params) { params[p.key] = p.type === 'number' ? parseFloat(p.default) : p.default; } setPipeline(p => [...p, { type: addType, params }]); } async function handleSave() { if (!def) return; setSaving(true); setError(''); try { const body = { ...def, pipeline }; const res = await fetch(`/api/v1/synthetic/${encodeURIComponent(name)}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); if (!res.ok) { const j = await res.json().catch(() => ({ error: res.statusText })); throw new Error(j.error ?? res.statusText); } onSaved(); onClose(); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setSaving(false); } } return (
e.stopPropagation()} style="max-width:560px;">
Edit Pipeline — {name}
{error &&

{error}

} {loading ? (

Loading…

) : (
Pipeline nodes
{pipeline.length === 0 && (

No processing nodes — input passes through unchanged.

)} {pipeline.map((node, idx) => { const paramDefs = nodeParamDefs(node.type); return (
{idx + 1} {nodeLabel(node.type)}
{paramDefs.map(pd => (
setNodeParam(idx, pd.key, (e.target as HTMLInputElement).value)} />
))}
); })}
)}
); }