# Functional Specification — uopi

## 1. Purpose

uopi is a web-based HMI (Human-Machine Interface) for monitoring and controlling industrial/scientific systems, primarily EPICS-based control systems. It runs as a single server process and is accessed entirely through a web browser, making it suitable for SSH-tunnelled remote access.

---

## 2. Users and Roles

User identity is established from a header set by a trusted authenticating reverse
proxy (`server.trusted_user_header`), with a configurable `default_user` fallback for
unproxied/LAN deployments. There is no login page inside the application.

**Global access levels.** Every user is trusted with full **write** access by default. A
configuration blacklist can downgrade specific users to **read-only** (view only, no
writes) or **no access** (denied). Named **groups** are defined in configuration and
referenced by per-panel sharing.

**Per-panel access.** Panels are owned by their creator and private by default. Owners
share a panel with specific users/groups (read or write) or make it public; the owner's
global level always caps the per-panel permission. Panels are organised into nested
folders whose permissions inherit down the chain. See §9.

**Logic-edit restriction.** Adding/editing panel logic and server-side control logic can
optionally be limited to an allowlist of users/groups (`server.logic_editors`); when
unset, any writer may edit logic.

---

## 3. System Modes

### 3.1 View Mode (default)

The default mode when opening the application.

**Interface list pane (left, collapsible, resizable)**
- Displays all interfaces saved on the server.
- Right-click on an interface: options to open in Edit mode or clone it.
- "New interface" button opens Edit mode with a blank canvas.
- The pane width can be adjusted by dragging the resize handle on its right edge.

**HMI canvas (center)**
- Renders the selected interface as a live, interactive panel.
- Widgets display real-time data; controls (set-value, buttons) are active.
- Panel logic (if any) runs while the panel is open (see §6).
- No drag, resize, or layout operations are possible in this mode.
- Right-clicking any widget opens a context menu:
  - **Signal info** — shows DS name, type, unit, range, current value and timestamp.
  - **Copy signal name** — copies the signal identifier to the clipboard.
  - **Export data to CSV** — downloads buffered data for the signal(s) used by the widget.

> Dedicated multi-signal plotting is provided by **plot panels** — a special interface
> kind whose plots fill the viewport (see §3.3) — rather than a separate live "Plot tab".

**Top toolbar**
- Show/hide interface list pane.
- **⏱ History** button: toggle historical time navigation bar.
- **⚙ Control logic** button: open the server-side control-logic editor (see §7).
  Shown only to users permitted to edit logic.
- Zoom control (A− / % / A+): adjust the UI scale (see §3.4).
- Edit button: switch to Edit mode for the current interface.
- Connection status indicator and signed-in user chip.

**Historical time navigation bar** (shown when History is active)
- Date/time range pickers (start and end).
- **Load** button fetches archive data and replaces live data in all widgets.
- **Live** button discards archive data and resumes real-time streaming.
- Status shown on plot widgets: "Loading history…", "No archive data for this range", "Archive unavailable".

### 3.2 Edit Mode

Activated via the "New interface" button or by clicking Edit in the toolbar.

**Signal tree pane (left, resizable and collapsible)**
- Shows all signals known to each connected data source.
- Sources are shown as top-level nodes; signals are nested within.
- User can add custom entries:
  - For EPICS: manually enter a PV name.
  - For Synthetic: define a new synthetic signal via the Synthetic Wizard (see §5.2).
- Filter/search box to narrow the list.
- Synthetic signals show an edit (✎) button to reopen the wizard.

**Center area — Layout / Logic tabs**
- **Layout**: free-form canvas where widgets can be placed at arbitrary pixel positions,
  over a background grid with snap-to-grid. (For plot panels this is replaced by the
  split-layout editor; see §3.3.)
- **Logic**: a node-graph flow editor for panel behaviour (see §6). Hidden when the user
  is not permitted to edit logic.

**Local variables**
- A panel may declare panel-scoped variables (data source `local`) with initial values.
- They are written by set-value widgets, buttons and logic actions, and referenced
  anywhere a signal is. Added from the signal tree's *Local* group / the Logic palette.

**Properties pane (right, resizable and collapsible)**
- Appears when one or more widgets are selected.
- Displays and edits all options for the selected widget (see §4).
- Width is adjustable by dragging the resize handle on its left edge.

**Top toolbar**
- Show/hide signal pane / properties pane.
- Undo / Redo (also Ctrl+Z / Ctrl+Shift+Z).
- Import / Export interface to/from local XML file.
- Save interface to server.
- Close (return to View mode).
- Zoom control (A− / % / A+).

### 3.3 Plot Panels (Split Layout)

A **plot panel** is a special interface kind dedicated to charts. Rather than free-form
widget boxes, plots **fill the viewport** and the user divides the space between them with
a recursive split layout (tmux/IDE-style tiling). Created via **+ Plot** in the interface
list, opening with a single full-viewport empty plot.

**Editing (Edit mode):**
- Hover a pane and use its split buttons (**⬌** vertical / **⬍** horizontal) to divide it;
  a new empty plot fills the freed half. Nesting is unlimited.
- Drag the divider between two panes to resize them.
- Click a pane to select it and configure its plot in the Properties pane (plot sub-type,
  Y range, time window, legend, per-signal colour). Drag a signal onto a pane to add it.
- A pane's **✕** removes it; the layout collapses onto its sibling.

**View mode:** the saved split layout fills the screen with live, streaming plots; the
per-widget right-click menu (signal info / copy / CSV) and historical time navigation
apply as for any time-series plot.

Per-plot configuration reuses the standard Plot widget (§4.4), so all plot sub-types and
options are available within a pane.

### 3.4 UI Zoom

The toolbar in both modes contains a zoom control (**A−** / **nn%** / **A+**) that adjusts the base font size of the entire UI:

- 11 zoom steps from 50% to 250% (50, 60, 75, 85, 100, 115, 130, 150, 175, 200, 250%).
- The selected zoom level is persisted in `localStorage` and restored on the next page load.
- At 100%, the base font size adapts to viewport height via `clamp(13px, 1.5vh, 18px)`, making the UI naturally usable on 4K displays without any manual adjustment.

---

## 4. Widgets

### 4.1 Creating Widgets

Drag a signal from the signal tree and drop it onto the canvas. A picker appears showing all widget types compatible with the signal's data type. The user selects one and the widget is placed at the drop location with default size.

### 4.2 Selecting Widgets

- Single click: select one widget (deselects others).
- Ctrl+click: add/remove a widget from the current selection.
- Click-drag on empty canvas area: rubber-band area select.

When a widget is selected, a bounding box appears with:
- 8 resize handles (corners + midpoints).
- A delete button (×) in the top-right corner.
- The widget can be moved by dragging its body.

### 4.3 Multi-selection Operations

When multiple widgets are selected:
- Drag any selected widget to move them all together.
- Del key deletes all selected widgets.
- An align/distribute toolbar appears above the canvas with:
  - Align left / center horizontal / right.
  - Align top / center vertical / bottom.
  - Distribute evenly (horizontal/vertical).

### 4.4 Widget Catalogue

| Widget | Compatible signal types | Description |
|--------|-------------------------|-------------|
| Text view | any scalar | Displays `name: value unit` |
| Gauge | numeric scalar | Circular or arc gauge with configurable range |
| Vertical bar | numeric scalar | Vertical level indicator |
| Horizontal bar | numeric scalar | Horizontal level indicator |
| Set value | numeric, string, or enum; writable | Input field or enum dropdown + Set button |
| LED | boolean / numeric | Coloured indicator with configurable condition and label |
| Multi-LED | integer (bitset) | One LED per bit with individual labels and conditions |
| Button | writable | Sends a fixed value or command on click |
| Plot | numeric scalar or array | Multi-signal plot; sub-types below |
| Text label | — | Static text annotation |
| Image | — | Static image |
| Link | — | Button navigating to another interface |

**Plot sub-types:**

| Sub-type | Signal requirement |
|----------|--------------------|
| Time series | numeric scalar(s) |
| FFT | 1-D numeric array |
| Waterfall | 1-D numeric array (repeated) |
| Histogram | numeric scalar(s) |
| Bar chart | numeric scalar(s) |
| Logic analyser | boolean / integer (bitset) |
| Waveform | 1-D numeric array (latest sample, x-vs-index) |

### 4.5 Widget Properties (Properties Pane)

Common to all:
- Label text, font size, text colour.
- Position (X, Y) and size (W, H) — editable numerically.
- Data source and signal name.

Per type:
- **Gauge / Bar**: min value, max value, alert thresholds with colours, unit label.
- **LED / Multi-LED**: condition expression (e.g. `value > 0`), colours for true/false states, label.
- **Plot**: plot sub-type selector, Y-axis range (auto or manual min/max), time window duration, legend position (top / bottom / none), value format string.
- **Set value**: no special options — enum mode is detected automatically from signal metadata.
- **Link**: target interface name.

### 4.6 Set-value Widget — Enum Mode

When the signal's metadata reports enum strings (e.g. EPICS mbbi/mbbo records):
- The input field is replaced by a `<select>` dropdown showing all enum options.
- The current live value is shown as the pre-selected option (display only).
- The user selects a value from the dropdown, then clicks **Set** to write it.
- The Set button is always required; changing the dropdown does not write immediately.

---

## 5. Data Sources

### 5.1 EPICS

- Connects to an EPICS environment via Channel Access (CA).
- On connect, retrieves full metadata from the PV name: data type, engineering units, display range, alarm limits, enum strings (for mbbi/mbbo records), read/write mode.
- Uses `ca_add_event` monitors — no polling.
- When an EPICS Archive Appliance is configured, the server can satisfy historical data requests from the toolbar time navigator.

### 5.2 Synthetic Signals

A signal defined by composing one or more input signals through a chain of processing nodes.

**Two authoring surfaces:**
- **Wizard** — for the common single-input case: name the signal, pick an input and a
  processing node, set parameters, Create.
- **Node-graph editor** — a visual editor that wires one or more inputs through a chain of
  DSP blocks, for multi-input pipelines. It compiles to the same inputs + pipeline model.
  It supports undo/redo (Ctrl+Z / Ctrl+Shift+Z) and copy/paste (Ctrl+C / Ctrl+V) of the
  selection; paste remaps ids and preserves internal wires (the permanent output node is
  never duplicated).

**Visibility scope:** each synthetic signal is scoped as *panel* (visible only to the panel
that created it), *user*, or *global* (shared with everyone).

The dialogs are resizable and default to a width that accommodates the Lua editor.

**Editing an existing synthetic signal:** click the ✎ button next to the signal in the tree to reopen the editor.

**Built-in node types:**

| Node | Parameters | Description |
|------|------------|-------------|
| Source | DS, signal name | Input from any data source |
| Gain | factor | Multiply by constant |
| Offset | value | Add constant |
| Moving average | window (samples) | Rolling mean |
| Low-pass filter | frequency (Hz), order (1–8) | IIR low-pass; correct for non-uniform sample rates |
| Formula | expression | Inline math (variables: `a`, `b`, …) |
| Lua script | script | Arbitrary Lua 5.1 with persistent state table |

**Lua editor:** the Lua node parameter uses a code editor with syntax highlighting (keywords, strings, comments, numbers rendered in distinct colours). The editor is a full-height multi-line input that grows with the dialog.

---

## 6. Panel Logic

The **Logic** tab in the panel editor is a node-graph (Node-RED-style) flow editor that
gives a panel interactive behaviour. The flow runs **client-side** while the panel is open
in View mode; it is saved as part of the interface XML. The tab and any logic editing are
gated by the logic-edit restriction (§2).

**Authoring:** drag blocks from the palette (or click to add), connect output ports to
input ports, and edit the selected node in the inspector. Expression fields reference a
signal as `{ds:name}` and a panel-local variable by its bare name, and support arithmetic,
comparisons, boolean logic and common math functions. The editor has its own undo/redo and
copy/paste.

**Node categories:**

| Category | Nodes |
|----------|-------|
| Triggers | Button press, threshold crossing, value change, timer/interval, panel loop, On-open / On-close lifecycle |
| Logic | AND gate, If (then/else), Loop (count or while) |
| Actions | Write to signal/variable, Delay, Log; Accumulate / Export-CSV / Clear for in-memory data arrays; Apply config / Read config / Write config / Create config / Snapshot config (see §11) |
| Dialogs | Info and Error pop-ups; Set-point prompt (asks the user for a number and writes it) |

**System helpers in expressions:** `{sys:time}` (epoch seconds) and `{sys:dt}` (seconds
since the firing trigger last fired).

---

## 7. Control Logic

**Control logic** is server-side automation: flow graphs that run continuously on the
server, independent of any connected client (unlike panel logic, which only runs while a
panel is open). Opened with the **⚙ Control logic** button in the View-mode toolbar and
managed through the REST API.

- Triggers include *cron* schedules and signal *alarm*/threshold conditions.
- A **Lua** block provides custom logic; results are written back to signals.
- **Apply / Read / Write / Create config** action nodes drive the configuration manager
  (§11): *Apply config* writes every value of a chosen instance to its bound signals
  (audited); *Read config* reads one parameter's value into a target signal or variable;
  *Write config* stores a value into a parameter (creating a new instance revision); *Create
  config* makes a new instance for a set, optionally seeded from another instance. Both
  mutating nodes are audited.
- Each graph can be enabled/disabled independently; saving reloads the engine live.
- The editor has its own undo/redo and copy/paste (Ctrl+Z / Ctrl+Shift+Z, Ctrl+C / Ctrl+V;
  also ↩/↪ toolbar buttons). Copy/paste is multi-select aware and preserves wires internal
  to the selection.
- Editing is gated by the logic-edit restriction (§2).

---

## 8. Interface Persistence

- Interfaces are saved to the server in XML format and are available to all connected clients.
- Per-panel access rules and panel-folder placement are stored server-side (sidecar JSON).
- Saved versions are retained; a panel's version history can be listed, tagged, viewed, promoted, forked and diffed — the same git-style versioning shared by all versioned documents (§12).
- Export/Import allows local file exchange of XML files.
- The XML schema records: interface kind (panel/plot) and split layout, widget type,
  position, size, signal bindings, all property values, local variables, and panel logic.

---

## 9. Access Control, Sharing & Folders

**Identity** is resolved per request from the trusted proxy header, falling back to
`default_user`. The `/api/v1/me` endpoint reports the caller's identity, global level,
group memberships and whether they may edit logic; the UI hides affordances accordingly.

**Global levels** (§2): write (default), read-only, or no-access via the config blacklist.

**Per-panel sharing:** the **Share** dialog on a panel grants read or write to specific
users or groups, or marks the panel public. New panels are private to their owner. The
owner's global level caps any per-panel grant.

**Folders:** panels are organised into nested folders; permissions inherit down the folder
chain. Panels can be dragged to reorder or move between folders.

**Logic-editor allowlist:** when `server.logic_editors` is set, only the listed users/
groups may add or change panel logic and control logic; everyone else keeps full access to
the rest of the application.

---

## 10. Historical Data Navigation

When the server has archive access configured:
- The **⏱ History** button in the View mode toolbar reveals a time range bar.
- Users set a start and end date/time and click **Load**.
- Plot widgets fetch and display the archived data for the selected range.
- Point-value widgets (text view, gauge, bar, LED) show the value at the end of the selected range.
- The **Live** button resumes real-time streaming.
- Status overlays on plot widgets indicate loading, empty, or error states.

---

## 11. Configuration Manager

The **Configuration manager** (opened from the View-mode toolbar) manages three kinds of
versioned documents, on **Sets**, **Instances** and **Rules** tabs, in a full-screen modal.

**Configuration sets** are schemas: an ordered list of typed parameters, each binding a
target signal. Parameters can be organised into **groups** and **subgroups** via a
drag-and-drop tree.
- A parameter's **type** (number, integer, boolean, string, enum, array/waveform) and its
  unit/range/enum values are auto-derived from the bound signal's metadata when available;
  the type is read-only.
- Each parameter may declare a **default**, a **mandatory** flag, and **min/max** /
  **enum** constraints.
- Sets can be **exported / imported** as JSON.

**Configuration instances** assign concrete values to a chosen set's parameters.
- The editor renders the right control per type (number input, enum/boolean combo, and a
  waveform editor with live sparkline, table editing, CSV import and a pop-out plot for
  arrays); empty fields fall back to the parameter default.
- Validation mirrors the backend (required-but-unset, out-of-range, wrong type, bad enum)
  and is surfaced inline with hover detail.
- **Apply** writes every value to its target signal and reports a per-parameter
  applied / failed / skipped result.
- **Snapshot** (⎙ in the Instances tab) captures the *current live value* of every target
  signal of a chosen set into a new instance — an optional label, otherwise an auto name. This
  records "what the hardware holds right now" as a reusable configuration.
- **Diff vs current** compares a stored instance (resolved with defaults) against the current
  live signal values, so an operator can see how the saved configuration differs from what the
  system currently has.

The Sets and Instances tabs support the shared version history (§12): view, fork and promote
revisions, plus a **structural diff** between any two revisions (per-parameter added / removed
/ changed / unchanged).

**Validation / transformation rules** (Rules tab) attach **CUE** logic to a set. Each rule is
CUE source describing constraints and/or derivations over the parameter values: a field like
`voltage: >=0 & <=24` validates a value, while a concrete derivation like
`power: voltage * current` computes one. When an instance of the bound set is saved, every
rule runs — a failed constraint **blocks the save** and surfaces the violation, and derived
values are written into the stored instance. The rule editor is a CUE-aware code editor with
syntax highlighting and autocomplete (parameter keys, target signal names, and CUE
keywords/types via Ctrl+Space); a live panel re-evaluates the rule against the set's default
values on every keystroke, showing compile errors, violations, or the derived values. Rules
are versioned like the other documents, with a side-by-side / unified source diff, and every
create / edit / delete is audited. (A full CUE language server is intentionally out of scope:
it does not fit the no-dependency, single portable-binary design.)

**Automation:** both the panel-logic (§6) and control-logic (§7) editors expose **Apply
config**, **Read config**, **Write config**, **Create config** and **Snapshot config** action
nodes. *Apply config* applies a chosen instance exactly like the Apply button; *Read config*
reads a single numeric parameter into a target signal or variable; *Write config* stores a
value into a parameter, creating a new instance revision; *Create config* makes a new instance
for a set, optionally seeded from another instance; *Snapshot config* captures every target
signal's current live value of a set into a new instance (like the ⎙ Snapshot button). The
mutating nodes are audited.

**Config Selector widget:** a panel widget that lets an operator pick which configuration a
flow operates on at run time. It lists the instances of a chosen set — all of them or a
defined subset — in a combo and writes the selected instance id to a panel-local string
variable. The panel-logic Apply / Read / Write nodes can take their instance "From variable"
(reading that id) instead of a fixed instance, so the operator's choice drives the flow.
Control-logic nodes use fixed instances only (their variables are numeric).

---

## 12. Version History & Diff

All versioned documents — panels, synthetic signals, control-logic graphs, and
configuration sets/instances — share one git-style version model and a common history
pane:

- A **vertical version tree** lists every revision (one node each); the **current**
  (executed) revision is filled, the **viewed** revision is enlarged, and unsaved edits show
  a dashed connector.
- **View** loads any past revision read-only into the editor — viewing alone is not an edit
  and does not mark the document dirty; saving from a viewed revision creates a new revision
  on top (history is never destroyed).
- **Fork** copies a revision into a brand-new document (version reset to 1).
- **Promote** re-saves an older revision as a new current revision.
- **Diff** compares two revisions, defaulting to *current-vs-selected*, shown either
  **unified** or **side-by-side**. Panels, synthetic and control-logic use a generic line
  diff of the serialized document; configuration sets/instances use a richer per-parameter
  structural diff.

---

## 13. Non-functional Requirements

| Requirement | Target |
|-------------|--------|
| Server binary | Single statically-linked executable; no runtime dependencies |
| Target platform | Linux x86-64; also aarch64 optional |
| Minimum server OS | RHEL/CentOS 7 (glibc 2.17) or equivalent |
| Concurrent clients | ≥ 20 simultaneous browser clients |
| Data fan-out latency | < 5 ms added latency vs. raw EPICS update rate |
| Frontend responsiveness | 60 fps canvas rendering during live updates |
| Screen DPI | UI scales with viewport height; manual zoom 50–250% |
| Plot buffer | 200,000 samples per signal retained in-browser |
