Add control logic engine, panel logic dialogs, logic-edit restriction

Introduce server-side control-logic flow graphs (cron/alarm triggers,
Lua blocks) with CRUD endpoints, panel-logic lifecycle triggers and
user-interaction dialog nodes, and a synthetic node-graph editor.

Add an optional logic-editor allowlist (server.logic_editors) gating who
may add/edit panel logic and control logic, surfaced via /api/v1/me and
enforced in the API; hide logic affordances in the UI accordingly.

Update README, example config, and functional/technical specs to cover
all current features (plot panels, panel/control logic, local variables,
access control) and refresh the in-app manual and contextual help.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Martino Ferrari
2026-06-19 07:27:35 +02:00
parent aba394b84d
commit afefba3184
35 changed files with 4633 additions and 467 deletions
+65
View File
@@ -0,0 +1,65 @@
// Package controllogic implements a server-side flow-graph engine. It mirrors
// the client-side panel logic engine (web/src/lib/logic.ts) but runs
// continuously on the server under the root context, independent of any panel.
//
// A control-logic graph is a Node-RED-style flow of trigger, gate, control-flow
// and action nodes connected by wires. Triggers are flow entry points; when one
// activates the engine follows the outgoing wires executing downstream nodes.
//
// Compared with the panel engine, control logic has no button trigger (buttons
// are panel-UI driven) and gains two headless triggers — cron (5-field schedule)
// and alarm (signal out of an allowed range) — plus a Lua script action node
// with get/set/log host functions.
//
// Graphs are persisted as JSON in {storageDir}/controllogic.json and CRUD'd via
// the REST API; the engine rebuilds itself (Reload) whenever they change.
package controllogic
// Node kinds. Triggers begin flows; the rest execute downstream.
//
// trigger.threshold — fires on the rising edge of cmp(signal, value).
// trigger.change — fires whenever a watched signal's value changes.
// trigger.timer — fires every `interval` ms.
// trigger.cron — fires when the wall clock matches a 5-field cron `spec`.
// trigger.alarm — fires on the rising edge of signal leaving [min,max].
// gate.and — passes only when all incoming triggers are satisfied.
// flow.if — evaluates `cond`, continues on the 'then'/'else' port.
// flow.loop — repeats the 'body' port (count / while, capped) then 'done'.
// action.write — evaluates `expr`, writes the result to `target`.
// action.delay — waits `ms` before continuing.
// action.log — logs an expression value to the server log.
// action.lua — runs a sandboxed Lua script with get/set/log host funcs.
// Node is a single node in a control-logic graph. Params are stored as strings
// (matching the panel logic model) and parsed per-kind by the engine.
type Node struct {
ID string `json:"id"`
Kind string `json:"kind"`
X float64 `json:"x"`
Y float64 `json:"y"`
Params map[string]string `json:"params,omitempty"`
}
// Wire connects an output port of one node to the input of another.
// FromPort defaults to "out" when empty.
type Wire struct {
From string `json:"from"`
FromPort string `json:"fromPort,omitempty"`
To string `json:"to"`
}
// Graph is a named, independently-enableable control-logic flow.
type Graph struct {
ID string `json:"id"`
Name string `json:"name"`
Enabled bool `json:"enabled"`
Nodes []Node `json:"nodes"`
Wires []Wire `json:"wires"`
}
func (n Node) param(key string) string {
if n.Params == nil {
return ""
}
return n.Params[key]
}