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
+180 -6
View File
@@ -406,7 +406,11 @@ const SECTIONS = [
{ id: 'view', label: 'View Mode' },
{ id: 'edit', label: 'Edit Mode' },
{ id: 'widgets', label: 'Widgets' },
{ id: 'plots', label: 'Plot Panels' },
{ id: 'signals', label: 'Signals' },
{ id: 'logic', label: 'Panel Logic' },
{ id: 'control', label: 'Control Logic' },
{ id: 'sharing', label: 'Sharing & Access' },
{ id: 'history', label: 'Historical Data' },
{ id: 'shortcuts', label: 'Keyboard Shortcuts' },
];
@@ -510,6 +514,24 @@ function SectionEdit() {
<h3 class="help-h3">Align &amp; distribute</h3>
<p>When 2+ widgets are selected the alignment toolbar appears. Buttons align edges or centres; with 3+ selected you can distribute spacing evenly.</p>
<h3 class="help-h3">Layout &amp; Logic tabs</h3>
<p>The centre of the editor has two tabs:</p>
<ul class="help-list">
<li><strong>Layout</strong> the drag-and-drop widget canvas described above.</li>
<li><strong>Logic</strong> a visual flow editor that adds interactive behaviour to the
panel (buttons that run actions, thresholds that pop up dialogs, timers, ). See the
<em>Panel Logic</em> section. The tab is hidden if you are not permitted to edit logic.</li>
</ul>
<h3 class="help-h3">Local variables</h3>
<p>
Panels can define their own <strong>local variables</strong> lightweight values that live
only inside the panel (no data source needed). Add them from the signal tree's
<em>Local</em> group (or the Logic palette). They are written by Set-value widgets, buttons
and logic actions, and referenced anywhere a signal is, making them handy for set-points,
toggles and counters used by panel logic.
</p>
<h3 class="help-h3">Saving</h3>
<ul class="help-list">
<li><strong>Save</strong> — stores the panel on the server (XML). A <span style="color:#f59e0b">●</span> indicates unsaved changes.</li>
@@ -520,6 +542,141 @@ function SectionEdit() {
);
}
function SectionPlots() {
return (
<div>
<p class="help-lead">
A <strong>plot panel</strong> is a special interface dedicated to charts. Instead of
free-form boxes, plots <em>fill the viewport</em> and you split the space between them like
panes in a tiling window manager (tmux / IDE style).
</p>
<h3 class="help-h3">Creating one</h3>
<ul class="help-list">
<li>Click <strong>+ Plot</strong> in the interface list to create a plot panel — it opens
with one full-viewport empty plot.</li>
</ul>
<h3 class="help-h3">Splitting &amp; arranging</h3>
<ul class="help-list">
<li>Hover a pane and use its split buttons (<strong>⬌</strong> vertical / <strong>⬍</strong>
horizontal) to divide it — a new empty plot appears in the freed half.</li>
<li>Drag the divider between two panes to resize them; nesting is unlimited.</li>
<li>Click a pane to select it, then configure its plot in the Properties pane (plot type,
time window, range, legend, per-signal colour).</li>
<li>Drag a signal from the tree onto a pane to add it to that pane's plot.</li>
<li>Use a pane's <strong>✕</strong> to remove it; the layout collapses onto its sibling.</li>
</ul>
<p>In view mode the saved split layout fills the screen with live, streaming plots.</p>
</div>
);
}
function SectionLogic() {
return (
<div>
<p class="help-lead">
The <strong>Logic</strong> tab in the panel editor is a node-graph (Node-RED style) flow
editor. You wire <em>trigger</em> nodes to <em>action</em> nodes to give a panel interactive
behaviour. Logic runs entirely client-side while the panel is open in view mode.
</p>
<h3 class="help-h3">Building a flow</h3>
<ul class="help-list">
<li>Drag blocks from the left palette onto the canvas (or click to add), then drag from a
node's output port to another node's input port to connect them.</li>
<li>Select a node to edit its parameters in the right inspector.</li>
<li>Expression fields accept arithmetic/logic over signals — reference a signal as
<code>{'{ds:name}'}</code> and a panel-local variable by its bare name.</li>
<li>The editor has its own undo/redo and copy/paste (see Keyboard Shortcuts).</li>
</ul>
<h3 class="help-h3">Node types</h3>
<div class="help-widget-table">
{[
{ type: 'Triggers', desc: 'Button press, threshold crossing, value change, timer/interval, panel loop, and On-open / On-close lifecycle.' },
{ type: 'Logic', desc: 'AND gate, If (then/else branches), and Loop (count or while).' },
{ type: 'Actions', desc: 'Write a value to a signal/variable, Delay, Log (debug), and Accumulate / Export-CSV / Clear for in-memory data arrays.' },
{ type: 'Dialogs', desc: 'Info and Error pop-ups, and a Set-point prompt that asks the user for a number and writes it to a target.' },
].map(w => (
<div key={w.type} class="help-widget-row">
<span class="help-widget-name">{w.type}</span>
<span class="help-widget-desc">{w.desc}</span>
</div>
))}
</div>
<h3 class="help-h3">System helpers</h3>
<ul class="help-list">
<li><code>{'{sys:time}'}</code> — current time in epoch seconds.</li>
<li><code>{'{sys:dt}'}</code> — seconds since the firing trigger last fired (useful for rates).</li>
</ul>
</div>
);
}
function SectionControl() {
return (
<div>
<p class="help-lead">
<strong>Control logic</strong> is server-side automation. Unlike panel logic (which only
runs while a panel is open in a browser), control-logic graphs run continuously on the
server, independent of any client. Open them with the <strong>⚙ Control logic</strong>
button in the view-mode toolbar.
</p>
<h3 class="help-h3">What it can do</h3>
<ul class="help-list">
<li>React to <em>cron</em> schedules and signal <em>alarm</em>/threshold conditions.</li>
<li>Run a <strong>Lua</strong> block for custom logic and compute writes back to signals.</li>
<li>Each graph can be enabled/disabled independently; changes reload the engine live.</li>
</ul>
<p class="help-callout">
Editing both panel logic and control logic can be restricted to an allowlist of users or
groups (<code>server.logic_editors</code> in the config). When restricted, users not on the
list keep full access to everything else but cannot add or change logic — the Logic tab and
the ⚙ Control logic button are hidden for them.
</p>
</div>
);
}
function SectionSharing() {
return (
<div>
<p class="help-lead">
uopi has graduated, server-enforced access control. Identity comes from a trusted
reverse-proxy header (with a configurable default for unproxied/LAN use) — there is no login
page in the app itself.
</p>
<h3 class="help-h3">Global access levels</h3>
<ul class="help-list">
<li>Every user is trusted with full <strong>write</strong> access by default.</li>
<li>A config blacklist can downgrade specific users to <strong>read-only</strong> or
<strong>no access</strong>. Read-only users see panels but cannot edit or write signals.</li>
<li>Named <strong>groups</strong> are defined in the config and used by panel sharing.</li>
</ul>
<h3 class="help-h3">Panel ownership &amp; sharing</h3>
<ul class="help-list">
<li>New panels are <strong>private</strong> to their owner by default.</li>
<li>Use the <strong>Share</strong> button on a panel (in the interface list) to grant
read or write to specific users/groups, or make it public.</li>
<li>Your global level always caps the per-panel permission (read-only stays read-only).</li>
</ul>
<h3 class="help-h3">Folders</h3>
<ul class="help-list">
<li>Organise panels into nested <strong>folders</strong> in the interface list.</li>
<li>Permissions inherit down the folder chain.</li>
<li>Drag panels to reorder them or move them between folders.</li>
</ul>
</div>
);
}
function SectionWidgets() {
return (
<div>
@@ -602,7 +759,19 @@ function SectionSignals() {
</p>
<h3 class="help-h3">Synthetic signals</h3>
<p>Click <strong>+ Synthetic</strong> to open the wizard. Choose an input signal, a processing node (gain, moving average, lowpass filter, ), and optional metadata. The new signal appears in the tree immediately.</p>
<p>Click <strong>+ Synthetic</strong> to compose a new signal from existing ones. A quick
<em>wizard</em> covers the common case (pick an input + a processing node such as gain,
offset, moving average, lowpass/highpass, derivative, integral, clamp or a custom
formula). For multi-input pipelines, the <strong>node-graph editor</strong> lets you wire
inputs through a chain of DSP blocks visually. The new signal appears in the tree
immediately.</p>
<p>Each synthetic signal has a <strong>visibility scope</strong>: <em>panel</em> (only the
panel that created it), <em>user</em>, or <em>global</em> (shared with everyone).</p>
<h3 class="help-h3">Local variables</h3>
<p>The <strong>Local</strong> group holds panel-local variables — values stored in the
panel itself with no data source. Use them for set-points, toggles and counters driven by
panel logic. They are added in the editor and saved with the panel.</p>
</div>
<DiagramSignalTree />
</div>
@@ -676,14 +845,15 @@ function SectionShortcuts() {
<h3 class="help-h3">API &amp; Metrics</h3>
<ul class="help-list">
<li>REST API: <code>/api/v1/datasources</code>, <code>/api/v1/signals</code>, <code>/api/v1/interfaces</code>, </li>
<li>Prometheus metrics: <code>/metrics</code></li>
<li>Health check: <code>/healthz</code></li>
<li>WebSocket: <code>ws://&lt;host&gt;/ws</code></li>
<li>Signals/data: <code>/api/v1/datasources</code>, <code>/api/v1/signals</code>, <code>/api/v1/synthetic</code></li>
<li>Panels: <code>/api/v1/interfaces</code>, <code>/api/v1/folders</code>, <code>/api/v1/interfaces/{'{id}'}/acl</code></li>
<li>Identity &amp; groups: <code>/api/v1/me</code>, <code>/api/v1/usergroups</code></li>
<li>Control logic: <code>/api/v1/controllogic</code></li>
<li>Prometheus metrics: <code>/metrics</code> · Health check: <code>/healthz</code> · WebSocket: <code>ws://&lt;host&gt;/ws</code></li>
</ul>
<h3 class="help-h3">Configuration file</h3>
<pre class="help-pre">[server]{'\n'}listen = ":8080"{'\n'}storage_dir = "./data"{'\n\n'}[datasource.epics]{'\n'}enabled = true{'\n'}archive_url = "http://archiver:17665"{'\n\n'}[datasource.synthetic]{'\n'}enabled = true</pre>
<pre class="help-pre">[server]{'\n'}listen = ":8080"{'\n'}storage_dir = "./interfaces"{'\n'}# optional: restrict who may edit panel/control logic{'\n'}# logic_editors = ["operators"]{'\n\n'}[datasource.epics]{'\n'}enabled = true{'\n'}archive_url = "http://archiver:17665"{'\n\n'}[datasource.synthetic]{'\n'}enabled = true</pre>
</div>
);
}
@@ -693,7 +863,11 @@ const SECTION_COMPONENTS: Record<string, () => JSX.Element> = {
view: SectionView,
edit: SectionEdit,
widgets: SectionWidgets,
plots: SectionPlots,
signals: SectionSignals,
logic: SectionLogic,
control: SectionControl,
sharing: SectionSharing,
history: SectionHistory,
shortcuts: SectionShortcuts,
};