Files
lmxopcua/docs/Uns.md
T

34 KiB
Raw Blame History

UNS — Global Unified-Namespace Management

The UNS page (/uns in the AdminUI) is the single surface for managing the Unified Namespace across the whole fleet. It replaces the old per-cluster UNS, Equipment, and Tags tabs and the standalone virtual-tags list — those have been removed; everything now lives in one global master tree.

The tree

The page shows every layer of the UNS as one expandable tree. Equipment is a leaf in the tree — tags, virtual tags, and alarms live on a dedicated equipment page, not inline:

Enterprise            (read-only grouping — ServerCluster.Enterprise)
└─ Site / Cluster     (read-only grouping — a ServerCluster row)
   └─ Area            (editable — UnsArea)
      └─ Line         (editable — UnsLine)
         └─ Equipment (leaf — Equipment; opens its own page)

Enterprise and Site/Cluster can be deleted from the tree (see 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).

Navigating

  • Expand all / Collapse all toggle the structural levels.
  • Filter by name does a case-insensitive substring match on the names of a node's direct children.

Creating and editing

Every editable row has inline actions:

Node Actions
Cluster + Area, Delete
Area + Line, Edit, Delete
Line + Equipment, Edit, Delete
Equipment Open, Delete

A + Child action pre-fills the parent for you (e.g. + Line on an area opens the Line modal with that area already selected). Build a branch top-down: Area → Line → Equipment. Tags, virtual tags, and scripted alarms are managed on the equipment page (see below).

Naming: area, line and equipment names are UNS path segments

An area, line or equipment name becomes a level of the OPC UA NodeId (Area/Line/Equipment/EffectiveName), so it must match ^[a-z0-9-]{1,32}$ — lowercase letters, digits and dashes, 1–32 characters. The reserved _default is also accepted. Packaging and Line 1 are not valid; packaging and line-1 are.

Ids are a different thing and a different rule. UnsAreaId / UnsLineId are opaque handles that never reach a NodeId, so they take ^[A-Za-z0-9_-]+$ — SITEA-area1 is a fine id for an area named packaging.

The rule is enforced in UnsTreeService on every authoring path — create, rename, and the CSV import — and again at the deploy gate (UnsSegmentInvalid). Both call the same UnsSegment helper, so they cannot disagree.

⚠️ Why it is enforced at authoring and not only at deploy (Gitea #535): until 2026-08-01 only the equipment form checked it, so an area or line could be named Packaging, save cleanly, and fail the next deployment — and because a deployment snapshots the whole database, one badly-named area blocked every cluster's deploy, usually discovered by someone other than whoever typed it. In the CSV import a bad name is reported as an error, not a skip: a skip means "you already have this row", which would say the equipment exists when it does not.

Equipment page (/uns/equipment/{id})

Clicking Open on an Equipment row (or + Equipment on a Line) navigates to the equipment page. The page is organised into four tabs:

Tab Content
Details Equipment identity — name, description, OPC-40010 namespace fields, driver binding, and served-by cluster. (This is the former EquipmentModal, now a full page.)
Tags Equipment-bound driver tags. Each tag uses the driver-typed config editor (same editors as before — Modbus, S7, AB CIP, etc.) with the same client-side validation.
Virtual Tags Virtual tags driven by C# scripts. The inline Monaco script editor with Roslyn IntelliSense, completions, and live diagnostics is available here per tag.
Alarms Scripted-alarm definitions bound to this equipment. Create, edit, or delete predicates here; the Monaco editor is available for each predicate script.

"Add equipment" under a Line uses the URL /uns/equipment/new?lineId=..., pre-filling the parent line. Saving redirects to the new equipment's page.

Served-by cluster

An area's cluster assignment is its "served-by" cluster — the cluster node that runs it. It's set when you create the area (under a cluster) and changed by editing the area's cluster in the Area modal, which moves the whole branch. There is no separate "served-by" concept and no migration — it is simply UnsArea.ClusterId.

Tags — reference-only (v3)

v3 (Batch 3): UNS equipment no longer authors or binds tags. Device I/O is authored once in the Raw project tree (/raw — see Raw.md); an equipment's Tags tab holds references to those raw tags. The old driver-bound Tag modal on this tab is retired.

The Tags tab is a list of UnsTagReference rows. Each row shows the effective name, the backing tag's RawPath, its inherited DataType and AccessLevel (read-only — they come from the raw tag), and an editable display-name override. The effective name is the override else the raw tag's Name.

"+ Add reference" opens a raw-tree picker (the /raw tree in picker mode) scoped to the equipment's cluster — cross-cluster tags are structurally unreachable, and the service also enforces tag.cluster == equipment.cluster on commit. Tick individual tag leaves, or use a device / tag-group's "Select all tags below" menu to pull many at once.

Effective-name uniqueness: within an equipment the effective name must be unique across references, VirtualTags, and ScriptedAlarms (they share the equipment's UNS NodeId space, {EquipmentId}/{EffectiveName}). This is enforced both at authoring (a readable rejection naming the colliding source) and at the deploy gate (DraftValidator, UnsEffectiveNameCollision) — the deploy gate is what catches rename-induced collisions a raw rename produced after the reference was authored, naming both colliding sources and the equipment.

Removing a raw tag in /raw is blocked while a UnsTagReference points at it (the error names the referencing equipment); renaming a raw tag or any ancestor warns when a beneath-it tag is historized (no historianTagname override), UNS-referenced, or named by a script literal — see Raw.md.

Galaxy / AVEVA System Platform points are now ordinary raw tags on a GalaxyMxGateway driver in /raw, referenced into equipment like any other raw tag. There is no separate alias concept or SystemPlatform-kind namespace.

OPC UA address-space projection (v3 Batch 4 — dual namespace)

v3 (Batch 4): the server now exposes the address space under two OPC UA namespaces instead of the old single https://zb.com/otopcua/ns:

Namespace URI Subtree NodeId s= scheme
https://zb.com/otopcua/raw the /raw device tree (Folder → Driver → Device → TagGroup → Tag) the node's RawPath (e.g. Plant/Modbus/dev1/Speed)
https://zb.com/otopcua/uns the UNS tree (Area → Line → Equipment → signal) the slash-joined Area/Line/Equipment/EffectiveName

Every device value has exactly one source — the raw tag's node in the Raw namespace. A UnsTagReference projects that raw tag into an equipment as a UNS-namespace variable whose NodeId leaf is the reference's effective name (the display-name override else the raw tag's Name). The UNS variable does not bind a driver of its own: it carries an Organizes reference to its backing raw node and mirrors it. A single driver publish for a RawPath fans out to the raw NodeId AND every referencing UNS NodeId with identical value / quality / source-timestamp — the two NodeIds never drift.

  • Reads / subscriptions work through either NodeId and return the same data.
  • Writes route through either NodeId to the same backing driver ref under the same WriteOperate gating (a UNS write is neither more nor less privileged than the raw write it fans from); a failed device write reverts both NodeIds via the shared fan-out.
  • HistoryRead works through either NodeId and returns the same series — both NodeIds register the same historian tagname (see Historian.md).
  • Native alarms materialize once at the raw tag (ConditionId = RawPath) and fan via SDK notifiers to the raw device folder AND every referencing equipment folder — see ScriptedAlarms.md / AlarmTracking.md.

Note the two distinct identity strings: the wire NodeId is the path Area/Line/Equipment/EffectiveName, while the effective-name uniqueness key above is {EquipmentId}/{EffectiveName} (the logical per-equipment collision space the guards enforce). They are related but not the same string.

Virtual tags

A virtual tag is bound to an equipment and driven by a script (no driver). Add and edit virtual tags on the equipment page's Virtual Tags tab; the data type is chosen from the standard OPC UA type list and the Monaco script editor is available inline. Scripts may read the equipment's references relative-to-equipment with ctx.GetTag("{{equip}}/<RefName>") — the token resolves through the equipment's UnsTagReference rows (by effective name) to the backing raw tag's RawPath at deploy; an unresolved <RefName> is a deploy error and a live Monaco diagnostic (editor accepts ⇔ publish accepts). See ScriptEditor.md.

Galaxy tags

GalaxyMxGateway is an Equipment-kind driver — Galaxy points are ordinary equipment tags authored through the standard Tag modal, exactly like Modbus or S7 tags. The Galaxy reference is stored as TagConfig.FullName (tag_name.AttributeName). The Galaxy address picker on the Tags tab lets you browse the live Galaxy hierarchy to select an attribute; after selecting, set 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)

OpcUaClient was previously raw-JSON fallback only; it now has a first-class typed editor that exposes a single FullName field (PascalCase JSON key, consistent with the Galaxy editor convention). It is registered in TagConfigEditorMap and TagConfigValidator; unknown keys in the stored JSON blob are preserved on round-trip.

The historian backend is the external HistorianGateway (no OtOpcUa-side tag driver / tag-config editor). See Historian.md.

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 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 for details.

Array tags (1-D)

A tag can be made a 1-D OPC UA array node by setting two keys in its TagConfig JSON blob. The controls are exposed as first-class UI fields in the Tag modal (an Is array checkbox + an Array length numeric input), available for all drivers — typed editors (Modbus, S7, etc.) and the raw-JSON textarea (Galaxy) alike.

Canonical rule

Condition OPC UA node shape
isArray: true AND arrayLength >= 1 1-D array node (ValueRank = OneDimension, ArrayDimensions = [arrayLength])
isArray: false (any arrayLength) Scalar node (default)
isArray absent Scalar node (default)

A single-element array (isArray: true, arrayLength: 1) is valid and materialises as a [1] node.

Fields

Field Type Required Description
isArray bool no When true (and arrayLength >= 1), makes the node a 1-D array. Absent or false → scalar.
arrayLength uint (≥ 1) when isArray: true The element count for the OPC UA ArrayDimensions declaration. Must be ≥ 1 when isArray is set.

UI validation rejects arrayLength = 0 when isArray is checked.

Examples

{"FullName":"PLC1.TemperatureArray","isArray":true,"arrayLength":10}
{"Register":"HR100","DataType":"Float32","isArray":true,"arrayLength":5}

Combined with historization (values are arrays — history of the whole array snapshot):

{"Register":"HR200","DataType":"Int16","isArray":true,"arrayLength":20,"isHistorized":true}

Per-driver read mechanism and live-verify status

Driver Read mechanism Live-verify
Modbus Contiguous FC03/FC04 block (arrayLength × registers-per-element); String and BitInRegister array modes also supported Mac-verifiable (sim 10.100.0.35:5020)
S7 Contiguous ReadBytesAsync block over the declared address span + per-element decode loop Unit-proven (sim fixture down)
AB CIP libplctag native array read (atomic and UDT member arrays) Unit-proven (sim fixture down)
AB Legacy PCCC multi-element file read via libplctag (cap 256 elements) Unit-proven (sim fixture down)
TwinCAT Raw-bytes read of the declared SymbolPath at the resolved array type's layout + per-element decode; native ADS notifications carry the whole array (same decode). Multi-dim PLC arrays publish flattened; a shorter arrayLength reads the leading sub-range Live-verified 2026-10-07 on two TwinCAT 2.11 PLCs (see TwinCAT.md §Array tags)

Array writes (#610)

An authored-ReadWrite array tag is writable only when its driver can write that array — the server never advertises a write the driver will refuse. The capability is one table, DriverArrayWriteSupport (Core.Abstractions), consulted by the composer at compose time and carried on the plan records (RawTagPlan.ArrayWritable, mirrored onto every UNS reference as UnsReferenceVariable.BackingArrayWritable); the node is writable iff Writable && (!IsArray || ArrayWritable), on the raw node and every UNS mirror alike.

Driver Array writes Element types
Modbus Yes region: Coils (FC15, any element, ≤ 1968 coils); region: HoldingRegisters with Int16/UInt16/Bcd16/Int32/UInt32/Bcd32/Float32/Int64/UInt64/Float64 (FC16) whose arrayLength × registers-per-element fits maxRegistersPerWrite (default 123; the smallest value set on the driver or the tag's device wins) — the driver refuses rather than splits an over-cap FC16, so e.g. a 62-element Int32 array (124 registers) stays read-only. Not BitInRegister / String / Bool-in-register; input regions are read-only
S7 Yes Byte/Int16/UInt16/Int32/UInt32/Float32 (one contiguous block). Not Bool (would clobber neighbouring bits) nor the wide / String / DateTime types
OpcUaClient Yes All — forwarded upstream unchanged; the upstream server decides (shape included)
Galaxy Yes OPC UA Boolean/Int32/Int64/Float/Double/String/DateTime (the MxValueEncoder array variants) — only with a declared arrayLength: an undeclared length materialises an unbounded node, and a short whole-array write would resize the Galaxy attribute (mxaccessgw contract), so such a node stays read-only
AB CIP, AB Legacy, TwinCAT, FOCAS, MQTT, MTConnect, Sql, Calculation No — scalar-only write encoders; array nodes stay read-only and a client write is BadNotWritable —

Modbus, S7 and Galaxy additionally need a declared length — isArray: true and an integer arrayLength >= 1. The Modbus and S7 tag factories read anything else as a scalar tag (whose scalar encoder cannot take an array), even though the composer still materialises an array node; that node stays read-only.

Each "Yes" row is pinned against the driver's real encoder by a parity test in that driver's test project (*ArrayWriteSupportParityTests), so a driver gaining or losing an array encoder fails a test until the table follows.

What a client sees on a writable array node:

  • Whole-array writes only. The value must be a 1-D array of exactly arrayLength elements (an unbounded arrayLength/ArrayDimensions = [0] takes any length — reachable only on OpcUaClient); anything else is BadTypeMismatch, refused inline before dispatch.
  • The element type must match the node's DataType, or be a numeric type that converts to it exactly. A matching element type (or a Byte array, which arrives as a ByteString) is dispatched as written. A numeric array of another numeric element type is converted, checked, element by element into the node's element type, and the converted array is what the node stores and the driver receives — an Int32[] written to an Int16 array reaches the driver as short[]. The whole write is refused BadTypeMismatch inline, nothing dispatched, if any element is out of the target's range, fractional for an integer node (1.5 → Int16 refused; 2.0 → 2), NaN/±∞ for an integer node, or finite but beyond Float range for a Float node. Every non-numeric mismatch (Boolean[] → Int32, Int32[] → String, a Variant[]) is refused. This is the check the OPC UA SDK would normally make itself: it skips its own type check on any node with an OnWriteValue handler, which every writable equipment node has, so the node manager makes it instead. Without it a UInt32[] reached Galaxy's int[] encoder by CLR array covariance and was written bit-reinterpreted (4,000,000,000 → −294,967,296) with a Good result — that write is still refused (4,000,000,000 overflows Int32), while an in-range UInt32[] converts. The conversion is required, not a nicety: ScadaBridge writes the CLR list it decoded from a List attribute, and its element types are only Boolean/Int32/Float/Double/String/DateTime, so an exact-match rule left every Int16/UInt16/ Byte/UInt32/Int64/UInt64 array node (Modbus's default Int16 included) and a Double list to a Float array unwritable from it. Scalar writes keep their looser rule — any numeric to any numeric node, unconverted — because every driver's scalar encoder converts with a range check, so an out-of-range scalar fails the device write and is reverted.
  • An array value to a scalar node is refused BadTypeMismatch inline only on Modbus and S7, whose scalar encoders cannot take one (DriverArrayWriteSupport.RefusesArrayValueOnScalarTag). Every other driver keeps the pre-#610 behaviour — the write is dispatched and the driver decides. That matters for browse-committed tags: the /raw commit sets the DataType but never isArray, so an upstream array variable (OpcUaClient) or a Galaxy array attribute committed from the browser is a scalar node into which the driver publishes an array, and writing that array back still works (OpcUaClient forwards it to the upstream server, which arbitrates the shape; Galaxy encodes it). Set isArray + arrayLength on such a tag to get the array-node checks above.
  • A partial IndexRange write is BadWriteNotSupported — every array-capable driver writes the whole block, and the server does not hold the device array authoritatively enough to read-modify-write it.
  • The same realm-qualified WriteOperate / ACL gate as a scalar applies to both NodeIds, and a device-refused array write is reverted on the NodeId it was written through (the scalar write-outcome self-correction, unchanged).
  • Flipping a tag's array-write capability (a driver-type or element-type edit on an existing array tag) is a surgical in-place writability update, not a rebuild: ArrayWritable is on the classifier's raw-tag whitelist, and the mirrors repair through BackingArrayWritable. A change to the array-on-scalar refusal (ScalarRefusesArrayValue — only a driver-type edit can flip it) is deliberately NOT whitelisted and rebuilds.

⚠️ Upgrade note — array tags become client-writable without a deploy. Before #610 every array node was read-only regardless of its authored AccessLevel. On the first boot after upgrading, every array tag authored ReadWrite on Modbus, S7, OpcUaClient or Galaxy whose shape the table above accepts is materialised writable — no deployment needed, since the capability is derived at compose time (a FetchAndCache node re-composes from its cached artifact). Any client holding WriteOperate (or, with Security:Acl:Enabled, a WriteOperate grant covering the tag) can then write whole PLC arrays. Before upgrading, audit array tags authored ReadWrite that were never meant to be written and set them Read; array tags on the other drivers are unaffected.

Out of scope (named deferrals)

  • Multi-dimensional arrays (ValueRank > 1) — out of scope by decision (#610 item 3), not merely deferred. Every driver's array read decodes a flat 1-D block (TwinCAT already publishes multi-dim PLC arrays flattened), and the node manager materialises every array ValueRank = OneDimension with a single ArrayDimensions entry. Supporting rank > 1 would need per-rank dimensions in TagConfig, the plan records, the classifier whitelist, the node manager and every driver's decode/encode, and no consumer has asked for it. Author a multi-dimensional PLC array as a flat 1-D tag (row-major), or as several 1-D tags.
  • Array historization — a historized array tag materialises with the correct Historizing flag, but the historian backend treats the value as an opaque blob; per-element history is out of scope. (Continuous historization is numeric-analog only — array / non-numeric values are not recorded.)

See the individual driver docs under docs/drivers/ for per-driver implementation details.

Writable equipment tags (writable key)

An equipment tag is writable by default — the materialised OPC UA node is created CurrentReadWrite and inbound client writes are routed to the device (subject to the WriteOperate gate — a role the session's API-key principal earns from its NodeAcl grants, see security.md). Setting the optional "writable": false key in a tag's TagConfig makes the node read-only at the driver seam. The six typed tag editors (Modbus, S7, AB CIP, AB Legacy, TwinCAT, FOCAS) expose this as a Writable checkbox (checked by default); it can also be hand-authored in the raw-JSON textarea.

Condition Node
writable absent Writable (driver default) — existing configs are unchanged
writable: false Read-only
writable: true Writable

The key is honoured by Modbus, S7, AB CIP, AB Legacy, and TwinCAT. FOCAS forces read-only regardless — FOCAS PMC writes are unsupported in the wire client, so its editor shows the checkbox disabled/unchecked and any authored writable:true is ignored (and flagged at the deploy gate below). Node-level authz (WriteOperate via the API-key principal's grants, not LDAP) remains the effective write gate above this flag.

Deploy-time TagConfig validation (Deployment:TagConfigValidationMode)

The deploy gate inspects every equipment tag's TagConfig per its bound driver type and surfaces strictness warnings — a present-but-invalid enum value (e.g. a typo'd dataType, which the lenient runtime silently defaults to the wrong width with Good status), a structurally-unparseable TagConfig, or (FOCAS) an authored writable:true.

The behaviour is controlled by Deployment:TagConfigValidationMode in appsettings.json:

Mode Behaviour
Warn (default) Non-blocking: warnings are logged and appended to the deployment result message; the deploy still succeeds. Existing configs never break.
Error Blocking: warnings fold into the draft-gate reject list — a config with a typo'd enum (or unparseable TagConfig) cannot be re-deployed until fixed. Opt-in per environment once the fleet deploys warning-clean.

Running servers are untouched either way — the gate only sees re-deploys. Unmapped drivers (Galaxy, OpcUaClient) are skipped. Unknown keys are never warned on: the TagConfig blob is a shared namespace (platform intent keys coexist with per-driver address keys and forward-compat keys the typed editors preserve), so there is no single schema to validate against — parsers read known fields and ignore the rest by design.

Note (runtime strictness is deferred). In this release the runtime parsers stay lenient (a typo'd enum still silently defaults) — only the deploy gate reports it. Flipping the runtime parsers to strict (a typo'd enum ⇒ BadNodeIdUnknown instead of a wrong-width Good) is a deliberate future phase, to be taken only after fleets have run Error mode clean. Tracked in archreview/plans/STATUS.md.

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:

{"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 and clusters in one pass. After an import the whole tree reloads.

Applying changes

Edits here change the configuration only. As the page header notes, changes apply on the next deployment — run a Deploy (Deployments page) to push them into the running address space.

What a deploy actually tears down (surgical deploys, 2026-08-15)

Adding, removing or re-pointing a UNS tag reference no longer rebuilds the address space. It used to: RawTagPlan.ReferencingEquipmentPaths participates in the backing raw tag's record equality, so touching one reference flipped that tag to Changed and the classifier's blanket ChangedRawTags ⇒ Rebuild rule tore down and recreated every node on the server — killing every client's subscriptions for a one-variable edit. Now every UnsReferenceDelta is applied surgically, and so is a non-alarm raw-tag delta over the in-place-updatable attributes (Writable / IsHistorized / HistorianTagname / DataType / IsArray / ArrayLength). A full rebuild is still forced by an alarm-bearing raw-tag delta (only a rebuild re-wires native-alarm notifiers), an identity change on a raw tag (TagId / DriverInstanceId / Name / ParentNodeId / Realm), a raw container rename or removal, and any changed equipment / scripted alarm. The classifier stays default-closed — anything its whitelist does not name still rebuilds, and any surgical failure ratchets back to a rebuild.

Mirror repair. A UNS reference variable mirrors its backing raw tag, so a raw-side historian or shape edit must reach it too — UnsReferenceVariable carries the backing tag's IsHistorized / HistorianTagname / IsArray / ArrayLength, which makes the reference record unequal on such an edit and emits a coincident UnsReferenceDelta alongside the raw one. Identity-preserving repairs (the mirror keeps the same NodeId, equipment and backing RawPath) are applied in place, so the mirror's subscriptions survive an operator historizing or reshaping the tag behind it. Only an identity change — a re-point to a different raw tag, or an effective-name/display-name edit that moves the NodeId — is a remove + recreate, and it affects that one node, not the tree.

⚠️ Two consequences worth knowing. A surgically added UNS reference variable is announced (Part 3 GeneralModelChangeEvent), but an added raw container/tag is not — clients pick those up on their next browse. And a degraded non-rebuild apply (some node failed to materialise) deliberately does not record its composition as applied, so the next deploy re-diffs and repairs it.

Quality on a UNS node

A UNS reference variable mirrors its backing raw tag exactly — value, status and timestamps — so when that tag's driver stops communicating the mirror goes BadNoCommunication with its last value retained, in the same fan-out message as the raw node, and clears only when the driver re-publishes. A VirtualTag on the same equipment follows the same rule from its own inputs: it holds while any input is Bad and reports that input's sub-code. The contract, and every seam that implements it, is stated once in docs/DriverLifecycle.md §"The quality contract"; what a script sees is in docs/ScriptEditor.md.

See also