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
+167
View File
@@ -75,6 +75,88 @@ function sleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
// Reads a single parameter's value from a config instance, coercing it to a
// number. Returns null if the instance/set/param is missing or non-numeric.
async function readConfigParam(instanceId: string, key: string): Promise<number | null> {
try {
const ir = await fetch(`/api/v1/config/instances/${encodeURIComponent(instanceId)}`);
if (!ir.ok) return null;
const inst: { setId: string; setVersion?: number; values?: Record<string, any> } = await ir.json();
const sv = inst.setVersion && inst.setVersion > 0
? `/api/v1/config/sets/${encodeURIComponent(inst.setId)}/versions/${inst.setVersion}`
: `/api/v1/config/sets/${encodeURIComponent(inst.setId)}`;
const sr = await fetch(sv);
if (!sr.ok) return null;
const set: { parameters?: Array<{ key: string; default?: any }> } = await sr.json();
const param = (set.parameters ?? []).find(p => p.key === key);
if (!param) return null;
const raw = inst.values?.[key] ?? param.default;
const n = toNum(raw);
return isNaN(n) ? null : n;
} catch {
return null;
}
}
// Write a single numeric value into a config instance's parameter, creating a
// new revision server-side. Reads the current instance, overwrites values[key],
// and PUTs it back. Silently no-ops on any transport/parse error.
async function writeConfigParam(instanceId: string, key: string, val: number): Promise<void> {
try {
const ir = await fetch(`/api/v1/config/instances/${encodeURIComponent(instanceId)}`);
if (!ir.ok) return;
const inst: any = await ir.json();
inst.values = { ...(inst.values ?? {}), [key]: val };
await fetch(`/api/v1/config/instances/${encodeURIComponent(instanceId)}`, {
method: 'PUT',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(inst),
});
} catch {}
}
// Create a new config instance for `setId`, optionally seeding its values from
// an existing instance `fromId`. Returns the new instance id, or '' on failure.
async function createConfigInstance(setId: string, name: string, fromId: string): Promise<string> {
try {
let values: Record<string, any> = {};
if (fromId) {
const fr = await fetch(`/api/v1/config/instances/${encodeURIComponent(fromId)}`);
if (fr.ok) {
const src: any = await fr.json();
values = { ...(src.values ?? {}) };
}
}
const r = await fetch('/api/v1/config/instances', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name, setId, values }),
});
if (!r.ok) return '';
const out: any = await r.json();
return String(out?.id ?? '');
} catch {
return '';
}
}
// Snapshot the current live values of every target signal of `setId` into a new
// config instance (optional label). Returns the new instance id, or '' on failure.
async function snapshotConfigSet(setId: string, name: string): Promise<string> {
try {
const r = await fetch(`/api/v1/config/sets/${encodeURIComponent(setId)}/snapshot`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ name }),
});
if (!r.ok) return '';
const out: any = await r.json();
return String(out?.instance?.id ?? '');
} catch {
return '';
}
}
function toNum(v: any): number {
if (typeof v === 'number') return v;
if (typeof v === 'boolean') return v ? 1 : 0;
@@ -158,6 +240,20 @@ class LogicEngine {
return toNum(this.live.get(refKey(ds, name)));
}
// Resolve the config-instance id a node operates on. With instanceSource
// 'var' the id is read (as a raw string) from a panel-local variable / signal
// — this is how a config-selector widget feeds the apply/read/write nodes.
// Otherwise the fixed `instance` param (an id chosen at design time) is used.
private resolveInstanceId(node: LogicNode): string {
if ((node.params.instanceSource ?? 'fixed') === 'var') {
const r = parseRef(node.params.instanceVar ?? '');
if (!r) return '';
const v = this.live.get(refKey(r.ds, r.name));
return v == null ? '' : String(v).trim();
}
return (node.params.instance ?? '').trim();
}
/** Activate a panel's logic graph. Replaces any previously loaded graph. */
load(graph: LogicGraph | undefined): void {
this.clear();
@@ -266,6 +362,18 @@ class LogicEngine {
case 'action.write':
case 'action.accumulate':
case 'action.log': collectRefs(node.params.expr ?? '').forEach(want); break;
case 'action.config.apply':
case 'action.config.read':
case 'action.config.write': {
// Dynamic-instance nodes read their target instance id from a panel
// var; subscribe it so the live cache holds the selected id.
if ((node.params.instanceSource ?? 'fixed') === 'var') {
const r = parseRef(node.params.instanceVar ?? '');
if (r) want(r);
}
if (node.kind === 'action.config.write') collectRefs(node.params.expr ?? '').forEach(want);
break;
}
case 'action.dialog.info':
case 'action.dialog.error':
case 'action.dialog.setpoint': {
@@ -390,6 +498,65 @@ class LogicEngine {
return;
}
case 'action.config.apply': {
const id = this.resolveInstanceId(node);
if (id) {
try {
await fetch(`/api/v1/config/instances/${encodeURIComponent(id)}/apply`, { method: 'POST' });
} catch {}
}
await this.follow(node.id, 'out', ctx);
return;
}
case 'action.config.read': {
const id = this.resolveInstanceId(node);
const key = (node.params.key ?? '').trim();
const ref = parseRef(node.params.target ?? '');
if (id && key && ref) {
const val = await readConfigParam(id, key);
if (val !== null && !isNaN(val)) wsClient.write(ref, val);
}
await this.follow(node.id, 'out', ctx);
return;
}
case 'action.config.write': {
const id = this.resolveInstanceId(node);
const key = (node.params.key ?? '').trim();
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
if (id && key && !isNaN(val)) {
await writeConfigParam(id, key, val);
}
await this.follow(node.id, 'out', ctx);
return;
}
case 'action.config.create': {
const setId = (node.params.set ?? '').trim();
const name = (node.params.name ?? '').trim() || 'auto';
const fromId = (node.params.from ?? '').trim();
const target = parseRef(node.params.target ?? '');
if (setId) {
const newId = await createConfigInstance(setId, name, fromId);
if (newId && target) wsClient.write(target, newId);
}
await this.follow(node.id, 'out', ctx);
return;
}
case 'action.config.snapshot': {
const setId = (node.params.set ?? '').trim();
const name = (node.params.name ?? '').trim();
const target = parseRef(node.params.target ?? '');
if (setId) {
const newId = await snapshotConfigSet(setId, name);
if (newId && target) wsClient.write(target, newId);
}
await this.follow(node.id, 'out', ctx);
return;
}
case 'action.delay':
await sleep(Math.max(0, parseInt(node.params.ms ?? '0', 10) || 0));
await this.follow(node.id, 'out', ctx);