Implemented admin pane + user permission
This commit is contained in:
+71
-18
@@ -204,7 +204,7 @@ interface WireOut { to: string; port: string }
|
||||
// expression resolver that exposes the system signals {sys:time}/{sys:dt}.
|
||||
interface RunCtx { firedTrigger: string; steps: { n: number }; dt: number; resolve: Resolver }
|
||||
|
||||
class LogicEngine {
|
||||
export class LogicEngine {
|
||||
private graph: LogicGraph = { nodes: [], wires: [] };
|
||||
private byId = new Map<string, LogicNode>();
|
||||
// Outgoing wires grouped by source node id.
|
||||
@@ -227,6 +227,37 @@ class LogicEngine {
|
||||
private lastFire = new Map<string, number>();
|
||||
private cleanups: Array<() => void> = [];
|
||||
|
||||
// Dry-run / debug mode: when true the engine performs no external side effects
|
||||
// (signal writes, config mutations, CSV exports, widget commands, dialogs) and
|
||||
// instead records each node's last value/firing time so an editor overlay can
|
||||
// visualise the flow without touching production. Used by a throwaway engine
|
||||
// instance spun up by the Logic editor; the view-mode singleton leaves it off.
|
||||
private dryRun = false;
|
||||
private debugStates = new Map<string, { value: any; ts: number }>();
|
||||
|
||||
/** Enable/disable dry-run mode. Must be set before load(). */
|
||||
setDryRun(on: boolean): void { this.dryRun = on; }
|
||||
|
||||
// Record that a node fired this tick, optionally capturing its computed value.
|
||||
private markActive(id: string, value?: any): void {
|
||||
if (!this.dryRun) return;
|
||||
const prev = this.debugStates.get(id);
|
||||
this.debugStates.set(id, { value: value !== undefined ? value : prev?.value, ts: Date.now() });
|
||||
}
|
||||
|
||||
/** Per-node debug snapshot: a node is "active" if it fired within ~0.8 s. */
|
||||
getDebug(): Map<string, { value: any; active: boolean }> {
|
||||
const now = Date.now();
|
||||
const out = new Map<string, { value: any; active: boolean }>();
|
||||
for (const [id, s] of this.debugStates) out.set(id, { value: s.value, active: now - s.ts < 800 });
|
||||
return out;
|
||||
}
|
||||
|
||||
// Suppress signal writes in dry-run; otherwise forward to the live client.
|
||||
private dryWrite(ref: SignalRef, val: any): void {
|
||||
if (!this.dryRun) wsClient.write(ref, val);
|
||||
}
|
||||
|
||||
// Resolve an expression reference. Two built-in system signals are served
|
||||
// synthetically (never subscribed): {sys:time} = current epoch seconds,
|
||||
// {sys:dt} = seconds since the firing trigger last fired. Everything else
|
||||
@@ -320,9 +351,11 @@ class LogicEngine {
|
||||
this.watchers = new Map();
|
||||
this.arrays = new Map();
|
||||
this.lastFire = new Map();
|
||||
this.debugStates = new Map();
|
||||
// Restore every widget to its default state so reopening a panel (whose
|
||||
// widget ids are stable) does not inherit disabled/hidden/paused flags.
|
||||
resetWidgetCmds();
|
||||
// widget ids are stable) does not inherit disabled/hidden/paused flags. The
|
||||
// dry-run editor engine must not touch the real widget command stores.
|
||||
if (!this.dryRun) resetWidgetCmds();
|
||||
}
|
||||
|
||||
/** Fire every button-trigger node whose `name` param matches. */
|
||||
@@ -435,12 +468,22 @@ class LogicEngine {
|
||||
|
||||
// ── Execution ───────────────────────────────────────────────────────────────
|
||||
|
||||
/** Manually fire a trigger node (debug mode: double-click to force a flow
|
||||
* run). No-op if the id isn't a trigger node. */
|
||||
fireTrigger(nodeId: string): boolean {
|
||||
const node = this.byId.get(nodeId);
|
||||
if (!node || !node.kind.startsWith('trigger.')) return false;
|
||||
this.activate(nodeId);
|
||||
return true;
|
||||
}
|
||||
|
||||
// A trigger activated: walk its outgoing 'out' wires.
|
||||
private activate(triggerId: string): void {
|
||||
const now = Date.now();
|
||||
const last = this.lastFire.get(triggerId);
|
||||
const dt = last === undefined ? 0 : (now - last) / 1000;
|
||||
this.lastFire.set(triggerId, now);
|
||||
this.markActive(triggerId);
|
||||
const ctx: RunCtx = {
|
||||
firedTrigger: triggerId,
|
||||
steps: { n: 0 },
|
||||
@@ -462,15 +505,20 @@ class LogicEngine {
|
||||
if (ctx.steps.n++ > MAX_STEPS) return;
|
||||
const node = this.byId.get(nodeId);
|
||||
if (!node) return;
|
||||
this.markActive(node.id);
|
||||
|
||||
switch (node.kind) {
|
||||
case 'gate.and':
|
||||
if (this.gateSatisfied(node.id, ctx.firedTrigger)) await this.follow(node.id, 'out', ctx);
|
||||
case 'gate.and': {
|
||||
const ok = this.gateSatisfied(node.id, ctx.firedTrigger);
|
||||
this.markActive(node.id, ok);
|
||||
if (ok) await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'flow.if': {
|
||||
const branch = evalBool(node.params.cond ?? '', ctx.resolve) ? 'then' : 'else';
|
||||
await this.follow(node.id, branch, ctx);
|
||||
const pass = evalBool(node.params.cond ?? '', ctx.resolve);
|
||||
this.markActive(node.id, pass);
|
||||
await this.follow(node.id, pass ? 'then' : 'else', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -493,14 +541,15 @@ class LogicEngine {
|
||||
case 'action.write': {
|
||||
const ref = parseRef(node.params.target ?? '');
|
||||
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
||||
if (ref && !isNaN(val)) wsClient.write(ref, val);
|
||||
this.markActive(node.id, val);
|
||||
if (ref && !isNaN(val)) this.dryWrite(ref, val);
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'action.config.apply': {
|
||||
const id = this.resolveInstanceId(node);
|
||||
if (id) {
|
||||
if (id && !this.dryRun) {
|
||||
try {
|
||||
await fetch(`/api/v1/config/instances/${encodeURIComponent(id)}/apply`, { method: 'POST' });
|
||||
} catch {}
|
||||
@@ -513,9 +562,9 @@ class LogicEngine {
|
||||
const id = this.resolveInstanceId(node);
|
||||
const key = (node.params.key ?? '').trim();
|
||||
const ref = parseRef(node.params.target ?? '');
|
||||
if (id && key && ref) {
|
||||
if (id && key && ref && !this.dryRun) {
|
||||
const val = await readConfigParam(id, key);
|
||||
if (val !== null && !isNaN(val)) wsClient.write(ref, val);
|
||||
if (val !== null && !isNaN(val)) this.dryWrite(ref, val);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
@@ -525,7 +574,7 @@ class LogicEngine {
|
||||
const id = this.resolveInstanceId(node);
|
||||
const key = (node.params.key ?? '').trim();
|
||||
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
||||
if (id && key && !isNaN(val)) {
|
||||
if (id && key && !isNaN(val) && !this.dryRun) {
|
||||
await writeConfigParam(id, key, val);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
@@ -537,9 +586,9 @@ class LogicEngine {
|
||||
const name = (node.params.name ?? '').trim() || 'auto';
|
||||
const fromId = (node.params.from ?? '').trim();
|
||||
const target = parseRef(node.params.target ?? '');
|
||||
if (setId) {
|
||||
if (setId && !this.dryRun) {
|
||||
const newId = await createConfigInstance(setId, name, fromId);
|
||||
if (newId && target) wsClient.write(target, newId);
|
||||
if (newId && target) this.dryWrite(target, newId);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
@@ -549,9 +598,9 @@ class LogicEngine {
|
||||
const setId = (node.params.set ?? '').trim();
|
||||
const name = (node.params.name ?? '').trim();
|
||||
const target = parseRef(node.params.target ?? '');
|
||||
if (setId) {
|
||||
if (setId && !this.dryRun) {
|
||||
const newId = await snapshotConfigSet(setId, name);
|
||||
if (newId && target) wsClient.write(target, newId);
|
||||
if (newId && target) this.dryWrite(target, newId);
|
||||
}
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
@@ -565,6 +614,7 @@ class LogicEngine {
|
||||
case 'action.accumulate': {
|
||||
const name = (node.params.array ?? '').trim();
|
||||
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
||||
this.markActive(node.id, val);
|
||||
if (name && !isNaN(val)) {
|
||||
const arr = this.arrays.get(name) ?? this.arrays.set(name, []).get(name)!;
|
||||
arr.push({ t: Date.now(), v: val });
|
||||
@@ -574,7 +624,7 @@ class LogicEngine {
|
||||
}
|
||||
|
||||
case 'action.export': {
|
||||
this.exportArrays(this.exportColumns(node), node.params.align ?? 'common', node.params.filename ?? '');
|
||||
if (!this.dryRun) this.exportArrays(this.exportColumns(node), node.params.align ?? 'common', node.params.filename ?? '');
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
}
|
||||
@@ -589,6 +639,7 @@ class LogicEngine {
|
||||
case 'action.log': {
|
||||
const val = evalExpr(node.params.expr ?? '', ctx.resolve);
|
||||
const label = (node.params.label ?? '').trim();
|
||||
this.markActive(node.id, val);
|
||||
console.log(`[logic]${label ? ' ' + label + ':' : ''}`, val);
|
||||
await this.follow(node.id, 'out', ctx);
|
||||
return;
|
||||
@@ -596,7 +647,7 @@ class LogicEngine {
|
||||
|
||||
case 'action.widget': {
|
||||
const wid = (node.params.widget ?? '').trim();
|
||||
if (wid) {
|
||||
if (wid && !this.dryRun) {
|
||||
switch (node.params.op ?? '') {
|
||||
case 'enable': setWidgetDisabled(wid, false); break;
|
||||
case 'disable': setWidgetDisabled(wid, true); break;
|
||||
@@ -613,6 +664,7 @@ class LogicEngine {
|
||||
|
||||
case 'action.dialog.info':
|
||||
case 'action.dialog.error': {
|
||||
if (this.dryRun) { await this.follow(node.id, 'out', ctx); return; }
|
||||
const specs = this.dialogFieldSpecs(node);
|
||||
await this.showDialog({
|
||||
kind: node.kind === 'action.dialog.error' ? 'error' : 'info',
|
||||
@@ -625,6 +677,7 @@ class LogicEngine {
|
||||
}
|
||||
|
||||
case 'action.dialog.setpoint': {
|
||||
if (this.dryRun) { await this.follow(node.id, 'out', ctx); return; }
|
||||
const specs = this.dialogFieldSpecs(node);
|
||||
const result = await this.showDialog({
|
||||
kind: 'setpoint',
|
||||
|
||||
Reference in New Issue
Block a user