docs(phase6): AdminUI editors, pickers, deletes, new-script

This commit is contained in:
Joseph Doherty
2026-06-16 17:08:08 -04:00
parent 68f9eef62d
commit cbaf1c39ce
4 changed files with 193 additions and 18 deletions
+9
View File
@@ -240,6 +240,15 @@ AB CIP ALMD) route to AVEVA Historian via the Wonderware sidecar:
[AlarmHistorian.md §Configuration](AlarmHistorian.md#configuration)
for the `AlarmHistorian` appsettings section that enables the real sink.
**Native alarms** (equipment tags carrying an `"alarm"` object in their `TagConfig`)
support the same `HistorizeToAveva` opt-out. The field is `alarm.historizeToAveva`
(`bool?`) and is authored via the **"Historize to AVEVA"** checkbox in the Tag modal's
alarm section. The gate logic is identical (`is not false`): absent or `true` historizes;
explicit `false` suppresses the AVEVA write while leaving the live `/alerts` feed
unaffected. See
[ScriptedAlarms.md §TagConfig alarm fields](ScriptedAlarms.md#tagconfig-alarm-fields)
for the full field reference.
Galaxy-native alarms with `$Alarm*` extensions reach AVEVA Historian
directly via System Platform's `HistorizeToAveva` toggle on the
alarm primitive — no involvement from OtOpcUa. This sidecar path is
+32 -5
View File
@@ -13,9 +13,13 @@ Design reference: [docs/plans/2026-06-14-galaxy-phase-c-historian-design.md](pla
## Historized TagConfig schema
A tag is historized by adding fields to its `TagConfig` blob on the `/uns` equipment page
Tags tab (raw-JSON textarea). No separate UI control exists — Galaxy (the primary use case)
already uses the raw-JSON editor.
A tag is historized by setting the **Historize this tag** checkbox and optional **Historian
tagname (override)** textbox in the Tag modal on the `/uns` equipment page Tags tab. These
controls work for **all drivers** — typed editors (Modbus, S7, OpcUaClient, etc.) and the
raw-JSON textarea (Galaxy) alike. The controls merge `isHistorized` / `historianTagname`
into the existing `TagConfig` JSON blob via the `TagHistorizeConfig` helper, preserving all
other keys byte-stable. Drivers that still use the raw-JSON editor (Galaxy) can also add the
fields directly in the textarea.
### Fields
@@ -199,13 +203,36 @@ bit set at materialization. A session without sufficient permissions receives
## Authoring workflow
1. Open the equipment's **Tags** tab on `/uns/equipment/{id}`.
2. Create or edit the tag. Because Galaxy uses the raw-JSON editor, add `"isHistorized":true`
(and optionally `"historianTagname":"..."`) directly in the TagConfig textarea.
2. Create or edit the tag.
- For **typed-editor drivers** (Modbus, S7, OpcUaClient, etc.): check the
**Historize this tag** checkbox and, if needed, fill in the **Historian tagname
(override)** textbox.
- For **raw-JSON editors** (Galaxy): you can check the same first-class checkboxes
(they appear below the JSON textarea), or add `"isHistorized":true` (and optionally
`"historianTagname":"..."`) directly in the textarea.
3. Save and publish. The server rebuilds its address space; the node materialises with
`Historizing=true` and the `HistoryRead` AccessLevel bit.
4. Confirm with Client.CLI `read` that the node's `Status` is `Good` and that the value
is updating. Then issue a `historyread` to verify the historian connection returns data.
### Native-alarm historian opt-out (`alarm.historizeToAveva`)
A tag carrying a native `"alarm"` object has a **separate** historian opt-out for its
**alarm transitions** (distinct from tag-value historization). On the Tag modal, check or
uncheck **Historize to AVEVA** in the alarm section. This maps to `alarm.historizeToAveva`
(`bool?`) in the `TagConfig` JSON:
- **Absent or `true`** (default) — the alarm's transitions are written to AVEVA Historian
via `HistorianAdapterActor`.
- **`false`** — the durable AVEVA write is suppressed for this alarm's transitions. The
live `/alerts` feed and OPC UA condition events are unaffected.
The gate is applied in `HistorianAdapterActor` using `is not false` semantics, matching the
scripted-alarm `HistorizeToAveva` posture. See
[ScriptedAlarms.md §Native driver alarms](ScriptedAlarms.md#native-driver-alarms-equipment-tag-path)
and [AlarmTracking.md §Historian write-back](AlarmTracking.md#historian-write-back-non-galaxy-alarms)
for the full alarm-historian routing.
---
## Client.CLI historyread examples
+19 -7
View File
@@ -136,12 +136,31 @@ blob and is parsed byte-parity in both the compose (`Phase7Composer`) and deploy
|---|---|---|
| `alarmType` | `"AlarmCondition"`, `"OffNormalAlarm"`, `"DiscreteAlarm"`, `"LimitAlarm"` | `"AlarmCondition"` |
| `severity` | OPC UA 11000 scale | `500` |
| `historizeToAveva` | `true` / `false` / absent | absent (historize) |
An unknown `alarmType` string falls back to the base `AlarmCondition` OPC UA
ObjectType. `severity` seeds the condition's initial severity at materialisation;
the driver's live alarm events may carry a different severity that overrides it at
runtime.
**`historizeToAveva`** is a `bool?` opt-out for the AVEVA Historian durable write.
Set it to `false` to suppress writing this alarm's transitions to AVEVA Historian;
absent or `true` historizes as usual. The live `/alerts` feed and OPC UA condition
events are always unaffected regardless of this flag. The field is gated in
`HistorianAdapterActor` with `is not false` semantics — a missing field (e.g. from
an older config) defaults to historizing rather than silently dropping an audit row
during a rolling restart.
On the Tag modal, the **"Historize to AVEVA"** checkbox (in the alarm section)
controls this field. The **Galaxy address picker** pre-fills a default `alarm`
object (`{"alarmType":"OffNormalAlarm","severity":700}`) when it detects that the
selected attribute is itself an alarm (`IsAlarm == true`); the pre-fill never
overwrites an already-authored alarm section.
> **Distinct from tag-value historization.** `alarm.historizeToAveva` controls
> alarm-transition AVEVA writes only. Tag-value history is controlled by the
> top-level `isHistorized` field (see [Historian.md](Historian.md)).
#### Severity mapping
The authored `severity` (11000) seeds the OPC UA condition node at materialisation
@@ -211,13 +230,6 @@ for the full routing diagram.
backing a native alarm has no enable/disable surface distinct from OPC UA; the
Part 9 enable/disable concept maps to the scripted-alarm engine only.
One item remains explicitly out of scope:
1. **AdminUI Galaxy address-picker pre-fill**: the `alarm` object must be authored
by editing the tag's raw `TagConfig` JSON today; a future picker enhancement
could pre-fill `alarmType` / `severity` from driver discovery
(`DriverAttributeInfo.IsAlarm`).
## Inbound operator ack/shelve
Operators interact with active scripted alarms through two surfaces — both converge on the same `alarm-commands` DPS topic consumed by `ScriptedAlarmHostActor`.
+133 -6
View File
@@ -20,11 +20,11 @@ Enterprise (read-only grouping — ServerCluster.Enterprise)
└─ Equipment (leaf — Equipment; opens its own page)
```
**Enterprise and Site/Cluster are read-only here.** They are derived from
columns on the cluster record, not entities of their own, so you create and
configure clusters on the **Clusters** pages (`/clusters`). On a cluster row
the **⚙ settings** link jumps to that cluster. Editable UNS entities start
at **Area**.
**Enterprise and Site/Cluster can be deleted from the tree** (see
[Cluster and Enterprise delete](#cluster-and-enterprise-delete) below).
Other than deletion, they are configured on the **Clusters** pages
(`/clusters`) — on a cluster row the **⚙ settings** link jumps to that
cluster. Editable UNS entities start at **Area**.
Count badges next to a node show how many direct children it has (for
equipment, the combined tag + virtual-tag count).
@@ -41,7 +41,7 @@ Every editable row has inline actions:
| Node | Actions |
|---|---|
| Cluster | **+ Area** |
| Cluster | **+ Area**, Delete |
| Area | **+ Line**, Edit, Delete |
| Line | **+ Equipment**, Edit, Delete |
| Equipment | **Open**, Delete |
@@ -106,6 +106,133 @@ the tag **Name**, **DataType**, and **AccessLevel** (default: read-only). There
is no alias concept, no `SystemPlatform`-kind namespace, and no relay→alias
converter.
## Typed TagConfig editors
The **Tag modal** dispatches a driver-typed config editor for the following
drivers:
| Driver | Fields in the typed editor |
|---|---|
| Modbus | Register type, address, data type, word order, etc. |
| S7 | Data block, offset, data type, etc. |
| AB CIP | Tag path, data type, etc. |
| AB Legacy (DF1/DH+) | Address, data type, etc. |
| TwinCAT | Symbol path, data type, etc. |
| FOCAS | PMC address, data type, etc. |
| **OpcUaClient** | `FullName` (the remote OPC UA node id string) |
| **Historian.Wonderware** | `FullName` (the Wonderware tagname to read) |
**OpcUaClient** and **Historian.Wonderware** were previously raw-JSON
fallback only; they now have first-class typed editors that expose a single
`FullName` field (PascalCase JSON key, consistent with the Galaxy editor
convention). Both are registered in `TagConfigEditorMap` and
`TagConfigValidator`; unknown keys in the stored JSON blob are preserved on
round-trip.
Drivers not yet listed above (e.g. Galaxy — which uses the Galaxy address
picker described below) still use the generic raw-`TagConfig`-JSON textarea.
### "Build address" pickers in protocol-driver editors
The **Modbus, S7, AB CIP, AB Legacy, TwinCAT, and FOCAS** typed editors
include a **Build address** button. Clicking it opens the driver's existing
address-builder UI inside the shared `DriverTagPicker` overlay; confirming a
selection writes the fully constructed address string back into the editor's
address field. This means you can visually compose a register reference
(e.g. select "Holding Register → 100 → Int16") and have it serialised into
the correct JSON without hand-editing.
## Historizing tags (first-class controls)
The Tag modal exposes **Historize this tag** and **Historian tagname
(override)** as explicit controls that work for **all drivers** — including
protocol drivers (Modbus, S7, etc.) that use the typed editor and raw-JSON
drivers (Galaxy) alike.
| Control | JSON key | Type | Behaviour |
|---|---|---|---|
| **Historize this tag** checkbox | `isHistorized` | bool | When checked, the OPC UA node materialises with `Historizing=true` and the `HistoryRead` AccessLevel bit set. |
| **Historian tagname (override)** textbox | `historianTagname` | string (optional) | Explicit tagname the historian backend will query. When left blank, the server defaults to the tag's driver `FullName`. |
These fields are merged into the `TagConfig` JSON blob via the pure
`TagHistorizeConfig` helper, which preserves all other keys byte-stable.
The server's OPC UA HistoryRead dispatch already consumes these keys from the
`TagConfig` blob — see [Historian.md](Historian.md) for the full server
behaviour, continuation-point paging, and aggregates.
> **Note — native-alarm `HistorizeToAveva`:** a tag that carries a native
> `"alarm"` object has a **separate** opt-out field `alarm.historizeToAveva`
> (a checkbox labelled "Historize to AVEVA" on the Tag modal's alarm section).
> That field controls whether the alarm's **transition events** are written to
> the AVEVA historian — it does not affect tag-value history (which is
> controlled by `isHistorized`). See [ScriptedAlarms.md §Native driver
> alarms](ScriptedAlarms.md#native-driver-alarms-equipment-tag-path) for
> details.
## Galaxy address picker — native-alarm pre-fill
When the Galaxy address picker selects an attribute that is itself an alarm
(`IsAlarm == true` in the Galaxy hierarchy), the Tag modal automatically
seeds a default `alarm` object in the tag config:
```json
{"alarmType":"OffNormalAlarm","severity":700}
```
This lets the operator author the native alarm in a single picker pass
without hand-editing JSON. The pre-fill **never overwrites** an alarm object
that is already present — if the tag already has a custom `alarm` section, the
picker leaves it untouched.
## Cluster and Enterprise delete
### Cluster delete
A **Delete** action is available on Cluster rows in the UNS tree. The server
refuses the delete if the cluster still has any Areas (children) — the same
refuse-if-children guard used by Area and Line delete. Remove all Areas (and
their descendant Lines/Equipment) first, then delete the cluster.
> **No RowVersion concurrency check.** `ServerCluster` does not carry a
> concurrency token, so the delete does not have the last-writer-wins protection
> that Area/Line/Equipment deletes have. A follow-up migration will add the
> token; for now, coordinate cluster deletes manually.
### Enterprise delete
An **Enterprise** row is a read-only grouping label (the `Enterprise` column
of `ServerCluster`) — it is not a separate entity. Deleting an Enterprise row
**deletes all clusters whose `Enterprise` matches that label**, all-or-nothing:
- If any cluster under the enterprise still has children, the entire delete is
refused and no clusters are removed.
- If all clusters are empty, every cluster under that enterprise is deleted in a
single transaction.
Remove all Areas under every cluster in the enterprise first, then delete the
enterprise label.
## Create-new-script inline (virtual-tag panel)
On the equipment page's **Virtual Tags** tab, when a virtual tag is not yet
bound to any script, the inline script panel shows a **Create new script**
button. Clicking it:
1. Generates a new blank Script record with an auto-generated `SC-…` id.
2. Binds the virtual tag to that script.
3. Expands the Monaco editor inline so you can begin authoring immediately.
This removes the previous two-step flow (create a script on a separate page,
then attach it to the virtual tag); the entire lifecycle now lives on the
equipment page.
## Hosts page per-driver-instance rows (deferred)
Phase 6 did **not** implement per-driver-instance status rows on the Hosts
page. This item (H7-runtime) is **F7-runtime-blocked**: the runtime plumbing
needed to surface per-instance health rows is not yet in place. It remains on
the backlog.
## Bulk import
**Import equipment CSV** (toolbar) bulk-creates equipment across many lines