working epics ioc and tested

This commit is contained in:
Martino Ferrari
2026-05-04 21:13:36 +02:00
parent 90669c5fd6
commit 0a5a85e4c4
25 changed files with 1550 additions and 136 deletions
+194
View File
@@ -0,0 +1,194 @@
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<string, any>;
}
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<SignalDef | null>(null);
const [pipeline, setPipeline] = useState<PipelineNode[]>([]);
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<string, any> = {};
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 (
<div class="wizard-backdrop" onClick={onClose}>
<div class="wizard" onClick={(e) => e.stopPropagation()} style="max-width:560px;">
<div class="wizard-header">
<span>Edit Pipeline {name}</span>
<button class="icon-btn" onClick={onClose}></button>
</div>
<div class="wizard-body">
{error && <p class="wizard-error">{error}</p>}
{loading ? (
<p class="hint">Loading</p>
) : (
<div>
<div class="wizard-section-title">Pipeline nodes</div>
{pipeline.length === 0 && (
<p class="hint" style="padding:0.5rem 0;">No processing nodes input passes through unchanged.</p>
)}
{pipeline.map((node, idx) => {
const paramDefs = nodeParamDefs(node.type);
return (
<div key={idx} class="synth-pipeline-node">
<div class="synth-node-header">
<span class="synth-node-index">{idx + 1}</span>
<span class="synth-node-label">{nodeLabel(node.type)}</span>
<div class="synth-node-actions">
<button class="icon-btn" title="Move up" disabled={idx === 0} onClick={() => moveNode(idx, -1)}></button>
<button class="icon-btn" title="Move down" disabled={idx === pipeline.length - 1} onClick={() => moveNode(idx, 1)}></button>
<button class="icon-btn" title="Remove node" onClick={() => removeNode(idx)}></button>
</div>
</div>
{paramDefs.map(pd => (
<div key={pd.key} class="wizard-field wizard-field-inline">
<label>{pd.label}</label>
<input
class="prop-input"
type={pd.type === 'number' ? 'number' : 'text'}
value={node.params[pd.key] ?? pd.default}
onInput={(e) => setNodeParam(idx, pd.key, (e.target as HTMLInputElement).value)}
/>
</div>
))}
</div>
);
})}
<div class="synth-add-node-row">
<select class="prop-select" value={addType} onChange={(e) => setAddType((e.target as HTMLSelectElement).value)}>
{NODE_TYPES.map(n => <option key={n.type} value={n.type}>{n.label}</option>)}
</select>
<button class="toolbar-btn" onClick={addNode}>+ Add node</button>
</div>
</div>
)}
</div>
<div class="wizard-footer">
<button class="toolbar-btn" onClick={onClose}>Cancel</button>
<button class="toolbar-btn toolbar-btn-primary" onClick={handleSave} disabled={saving || loading}>
{saving ? 'Saving…' : 'Save Pipeline'}
</button>
</div>
</div>
</div>
);
}