OPC UA UI Design¶
Status: Partially superseded by ADR-0001 (Accepted 2026-05-11).
Asset-CRD surfaces in this design are obsolete
This design is scoped around the Asset CRD, which was removed by
ADR-0001 /
#348.
The Asset wizard, Asset detail page, and the standalone Trust List
inbox described below no longer exist as designed: discovery is now
the OPC UA wizard proposing ControlModule/Unit CRs (#347), and
server-certificate pins are set inline on
Unit.spec.serviceBinding.security (the bulk trust-list inbox is
deferred to a future rebuild). The /api/v1/asset-discovery/opcua/*
endpoints moved to /api/v1/discovery/opcua/*. The surviving
decisions (browse interaction, security-mode UX, faceplate
protocol-invisibility, method-invocation form patterns) inform the
Unit-based surfaces. See the
Asset dissolution migration guide
for the full removed-to-replacement mapping. Retained as a historical
design record.
Issue: #294
Companion: Smart Device Model RFC (joint with #293).
Scope: All UI surfaces an operator or engineer touches when working with
an OPC UA server through the Asset CRD: wizard, detail page, standalone
Explorer, method invocation, security configuration, diagnostics. This is the
companion to the Smart Device Model RFC's UI section, expanded into a full
design doc with wireframes and explicit answers to every open question
in #294.
This RFC does not redesign the data model (covered by #293), the gateway endpoints (fixed by #309), or the subscription manager (covered by #315). Where this doc and an already-filed implementation issue overlap, the implementation issue is the single source of truth for scope. This RFC records the design decisions those issues consume.
Audience and constraints¶
- Pharma plant: 21 CFR Part 11 (electronic records / signatures, audit trail) and IEC 62443 (zones / conduits, security levels) define what is defensible to an auditor. Every cert decision and every method invocation must produce a paper trail that a quality engineer can read six months later.
- Two roles: engineer (System UI: Asset wizard, OPC UA Explorer, trust-list inbox) and operator (HMI, with protocol-agnostic faceplates). Operators must never see node IDs, browse paths, security policies, or method argument tables. The protocol layer is invisible to them by design.
- Existing UI conventions: monochrome Unicode or inline SVG icons (no emoji), button classes from CLAUDE.md only, green and red reserved for healthy / unhealthy. Action buttons stay neutral.
Where it all lives in the gateway¶
The OPC UA UI splits across three System UI surfaces and the existing HMI
faceplate set. None of it is in the HMI sub-app except the faceplate
rendering, which is identical for Asset-backed and IOModule-backed
properties.
| Surface | Sub-app | Purpose | Implementation |
|---|---|---|---|
| Asset wizard | System UI | Create / edit Assets through guided steps | #310 |
| Asset detail page | System UI | Diagnostics, property table, methods, certs | #310 |
| OPC UA Explorer | System UI | Standalone ad-hoc browse / read / write | #310 |
| Trust-list inbox | System UI | Cert rotation queue and pin registry | #301 |
| Method invocation form | System UI | Call-service form, embedded in Asset detail | #298 |
| Faceplate | HMI | Operator value display, no protocol leakage | #310 |
All System UI surfaces consume the gateway endpoints fixed in
#309
under /api/v1/asset-discovery/opcua/:
POST /endpoints—GetEndpointsagainst a server, returns advertised endpoints with cert fingerprints.POST /browse— one level of address space. Children carry browse name, node ID (index + URI form), type definition,EURangeif present, and value if readable.POST /read,POST /write— ad-hoc operations for the Explorer.
This RFC does not propose new endpoints. Method invocation, cert auto-generation, and the trust-list inbox endpoints belong to their respective implementation issues.
Decisions¶
The numbered list below answers every open question raised in #294. Each is marked decided (recorded here, consumed by the linked implementation issue) or deferred (with the followup issue and one-line reason).
Address space browse¶
- Browse location — decided. Two surfaces, one tree component:
- Inline tree picker in the Asset wizard's Properties step. The operator's primary on-ramp.
- Standalone OPC UA Explorer view in the System UI sidebar. Used for commissioning and ad-hoc reads/writes. It creates no Assets.
Both consume the same POST /browse endpoint and render the same tree
component. The Explorer adds read/write affordances on leaf nodes and
a "current value" column. Implementation: #310.
-
Live vs cached browse — decided. Live, lazy, per-tab UI cache only. The gateway does not cache browse results across requests. Re-browse on every wizard open is the safe default. Server address spaces change, and a stale tree silently mis-binding properties is the exact failure mode pharma cannot accept. The UI may cache child listings within a single tab session so expanding a node twice in the same wizard pass does not re-roundtrip. A refresh control on every level forces a re-fetch.
-
Multi-namespace handling — decided. Namespaces appear as a filter chip strip above the tree, and the top tree level stays a single root. Default filter: all namespaces visible. The strip lists each namespace by index and URI (URI truncated with tooltip for full value). Operators typically work in one or two namespaces, and chip strips are a much faster filter than navigating a multi-root tree. The address-bar at the top of the tree always shows the canonical node ID in the URI form when the namespace has one (see #303 for the URI-vs-index toggle).
-
Search and filter — decided. A search box above the tree that does client-side substring filter on already-loaded children plus a server-side "search this subtree" button that calls a future
POST /searchendpoint with a depth cap. Phase 1 ships only client-side filtering. Server-side search is filed as #318, because it requires the gateway to do bounded recursive browse, which is non-trivial for servers with thousands of nodes (the Siemens S7-1500 problem the original issue calls out).
Endpoint discovery and cert pinning¶
GetEndpointsflow — decided. Always called on first connect. The wizard step's flow:- Operator types
opc.tcp://host:port. - Wizard calls
POST /endpoints. - Wizard renders a list of advertised endpoints with security policy, mode, transport, and the SHA-256 cert fingerprint.
- Operator picks one row and clicks "I trust this server".
-
The fingerprint is written into
OPCUABindingOptions.ServerCertSHA256Pinon save. -
Cert fingerprint display — decided. The fingerprint is rendered in fixed-width font, full 64 hex chars in groups of 4, on the endpoint card (not in a tooltip). It must be readable and copy-pasteable so the operator can verify it against an out-of-band channel (vendor certificate package, change-management ticket). A "copy" affordance sits next to it. Truncated fingerprints are not acceptable at first-trust, because pharma audit will not accept "the first eight characters matched". Subsequent connects show the same fingerprint in the Asset detail page. Mismatches drive a banner and pin the Asset to the
Faultstate per the state machine in smart-device-model.md. -
Insecure endpoints in pharma deployments — decided. The wizard rejects
securityPolicy: None+securityMode: Nonewith a non-bypassable error unless the cluster operator has setgateway.opcua.allowInsecure: truein the Helm values. This matches the gateway-side rejection in #309. The error names the Helm value so an engineer can locate the toggle.
Security configuration UI¶
-
securityPolicyandsecurityModeplacement — decided. Surfaced at endpoint pick time on the endpoint card itself, with no separate "Advanced" panel. TheGetEndpointsresponse gives us the menu of valid combinations the server supports. Surfacing it as a dropdown the operator could fill in independently invites misconfiguration where the chosen combo is not advertised. Pick the row, get the policy/mode bundled with the cert fingerprint. The Asset YAML records both fields, but the wizard never asks for them as free-form input. -
Auth mode selector — decided. Three-tab control labeled
Anonymous | Username/Password | Certificate. Tabs (not radios) because each option has its own form panel with several fields, and tabs make it explicit that switching changes the input set. -
Username/password handling — decided. Read-once, write-blind. The wizard persists credentials into a Kubernetes Secret in the gateway namespace via
CredentialsRef. After save, the password field renders as a placeholder dot-row, and an explicit "Replace password" affordance must be clicked to enter a new one. The audit log records the Secret rotation. It never records the value. -
Client-cert generation — decided for V1: bring-your-own, with auto-generation deferred to #301. V1 wizard ships only the BYO path: the operator pastes a PEM-encoded client cert and key, which become a Kubernetes Secret. The "Generate client cert" button appears next to the Certificate tab as
disabledwith a tooltip pointing at #301. #301 ships the per-Asset and per-Site auto-gen flow, with public-cert export so the operator can load it into the OPC UA server's trust list. -
Trust-list approval and inbox — decided for V1: in-line one-at-a-time approval at first-trust, with the standalone inbox deferred to #301. V1 handles the common case (operator creates an Asset, sees and approves one fingerprint). Cert rotation across many Assets is the inbox's job and lives in #301 along with bulk re-pin. When V1 hits a fingerprint mismatch on a pinned Asset, the Asset enters
Faultand the detail page shows the new fingerprint side-by-side with the pinned one, with explicit "Approve new fingerprint (re-pin)" / "Reject" actions. Both produce AuditRecord entries.
Channel/property parity with other protocols¶
-
Property data types beyond analog/digital — decided. The
AssetProperty.DataTypeenum from the Smart Device Model RFC is the single source of truth (Bool, Int, UInt, Double, String, ByteString, DateTime, Guid, Struct, Array, Method). The wizard's Properties step renders one row per property with the data type pre-filled from the server'sDataTypeattribute and a dropdown to override. Render rules:- Bool → digital faceplate (existing).
- Int / UInt / Double → analog faceplate (existing).
- String → read-only value strip in the faceplate, with no eng range asked.
- ByteString → hex display strip.
- DateTime / Guid → formatted read-only display strip.
- Struct / Array → row in the property list with badge "complex type — subscribed but not displayed". Faceplate decomposition is deferred to #319.
- Method → row with
()suffix and "method (deferred)" badge in V1. The invocation form lands with #298.
-
Engineering range auto-population — decided. The wizard auto-populates
EngineeringRangeandEngineeringUnitfrom the server'sEURangeandEngineeringUnitsproperties onAnalogItemTypenodes when present. The operator may edit before save. If the server does not expose these properties, the field is left empty and the Asset reconciler does not synthesize a range (manual entry only). Auto-population is a clear win for OPC UA over Modbus, where the value is never on the wire and operators always type it. -
Quality codes — decided. The faceplate's existing three-state
Good | Uncertain | Badchip stays. The OPC UA sub-code (e.g.,UncertainSubNormal,BadOutOfService) is propagated into a tooltip on the chip and into the historian's per-sample quality column for diagnostic filtering. No new colors: chips use neutral chip styling with the sub-code as label. Green and red are reserved for healthy / unhealthy elsewhere on the page, and we do not overload them on quality.
Method invocation¶
-
Method invocation form location — decided. An "Actions" tab on the Asset detail page in the System UI. Not in the HMI faceplate. Methods are an engineering-grade interaction (input-arg type validation, error surfacing, audit-required), and operators invoking ad-hoc methods is precisely what 21 CFR Part 11 considers a high-risk untracked change. Recipe-driven method invocation (the operator's primary path) lands as a recipe action in #298 and goes through the existing batch audit pipeline.
-
Async / progress for long-running methods — decided for V1: synchronous with a server-side timeout cap, and async filed in #298. OPC UA
Callis request-response in the protocol, and long-running methods are typically modelled as a method that schedules work plus a status property to poll. V1 calls synchronously with a 30-second server timeout. The form shows a spinner and a cancel button (which closes the call client-side only). True async with server-side progress reporting and resumable calls is part of #298.
Operator vs engineer separation¶
- Do operators ever see OPC UA-specific metadata? — decided. No.
HMI faceplates are protocol-agnostic. Operators see name, value,
quality, eng range, units. They do not see endpoint URLs, node IDs,
browse paths, security policies, fingerprints, or method argument
tables. All of those live exclusively in the System UI. This matches
the existing IOModule / Channel split and is what the Smart Device
Model RFC committed to. The HMI sub-app does not import any module
from
internal/gateway/static/system/.
Diagnostics surface¶
-
ServerStatusrendering — decided. Surfaced on the Asset detail page in a "Server" panel:ServerState(Running / Failed / NoConfiguration / Suspended / Shutdown / Test / CommunicationFault / Unknown).CurrentTimeand skew vs gateway clock.BuildInfo.ProductName,ProductUri,SoftwareVersion,BuildNumber,BuildDate.SecondsTillShutdownif the server signals impending shutdown.
These are read on connect and refreshed on a 30-second cadence. They populate
Asset.status.conditionsper the Smart Device Model RFC (asset-level, with no per-property copy). The panel surfaces the conditions content with friendlier labels. -
Subscription vs poll health — decided. The detail page's property table has a "Mode" column showing one of:
subscribed(icon: filled dot) — MonitoredItem established, value arriving at publishing cadence.polled(icon: hollow dot) — falling back to read-on-reconcile because the server rejected the MonitoredItem.pending(icon: dashed circle) — no value yet observed.
Hover surfaces sampling interval, publishing interval, queue size. This is consumed by the subscription manager work in #315. The UI exposes whatever the manager records into
PropertyStatus.
Wireframes¶
ASCII sketches, at design-doc fidelity. The four required surfaces follow.
Address-space browse tree (wizard step + Explorer)¶
+-------------------------------------------------------------------+
| Browse: opc.tcp://sim.example.com:48400 |
+-------------------------------------------------------------------+
| Filter: [ ns=2 (urn:opcfoun...) ] [ ns=3 ] [ All ] [Refresh] |
| Search: [____________________] (filters loaded children) |
+-------------------------------------------------------------------+
| |
| v Objects |
| > Server |
| v Granulator1 [PA-DIM Pump] |
| o AI0 Double in 45.2 degC EURange 0..120 |
| o AI1 Double in 1018.7 hPa EURange 0..2000 |
| o Setpoint Double out 50.0 EURange 0..120 |
| x Reset() Method method (deferred) |
| ? Recipe Struct complex type (deferred) |
| > MotorControl |
| |
+-------------------------------------------------------------------+
| Selected: ns=2;s=Granulator1.AI0 (URI: ns=urn:siemens:s7;s=...) |
| Picker form: [ Use as parent | Add this leaf | Use template ] |
+-------------------------------------------------------------------+
Notes:
- The [PA-DIM Pump] badge appears only when the type's
typeDefinitionId is in the companion-spec registry from
#297.
- The "Use template" button is only enabled when a companion-spec match
exists. It is hidden otherwise.
- "Method (deferred)" and "complex type (deferred)" rows are non-pickable
in V1. Clicking them shows a tooltip pointing at the relevant
followup.
- The address bar at bottom always shows both the index form and the URI
form per #303.
The toggle for "use stable URI form on save" lives in the wizard
Review step.
Endpoint picker with cert fingerprint confirmation¶
+-------------------------------------------------------------------+
| Step 4 of 7: Endpoint and security |
+-------------------------------------------------------------------+
| Server: opc.tcp://sim.example.com:48400 [Re-discover] |
| |
| Three endpoints advertised: |
| |
| ( ) None / None transport: opc.tcp |
| (rejected: gateway.opcua.allowInsecure=false) |
| |
| (o) Basic256Sha256 / SignAndEncrypt transport: opc.tcp |
| Server cert SHA-256: |
| AB12 CD34 EF56 7890 1122 3344 5566 7788 |
| 99AA BBCC DDEE FF00 1122 3344 5566 7788 [Copy] |
| Subject: CN=SimServer, O=Acme |
| NotAfter: 2027-03-01 |
| |
| ( ) Aes128Sha256RsaOaep / Sign transport: opc.tcp |
| Server cert SHA-256: |
| AB12 CD34 EF56 7890 ... [Copy] |
| |
+-------------------------------------------------------------------+
| [x] I have verified this fingerprint with the server admin. |
| This Asset will be pinned to it on save. |
| |
| [< Back] [Continue >] |
+-------------------------------------------------------------------+
Notes:
- Operator cannot continue until both an endpoint is picked and the
acknowledgement checkbox is set. Both actions are recorded in the
AuditRecord that lands when the Asset is created.
- "Re-discover" re-calls POST /endpoints. Useful if the server cert
was rotated between sessions.
- The checkbox text is fixed wording, and auditors look for it. Do not
reword without a 21 CFR Part 11 review.
Asset wizard — full step list with OPC UA fields¶
The wizard's seven steps (matching #310 exactly):
Step 1 Parent unit [drop-down of Units in current Site]
Step 2 Identity Name / AssetType / Description
Step 3 Transport OPC UA (other transports greyed)
Step 4 Endpoint + security (wireframe above)
Step 5 Auth [Anonymous | Username/Password | Certificate]
Step 6 Browse + properties (tree wireframe above)
Step 7 Review + apply Diff preview + ChangeRequest in enforced mode
Step 5 detail (auth tabs):
+-------------------------------------------------------------------+
| Step 5 of 7: Authentication |
+-------------------------------------------------------------------+
| [ Anonymous ] [ Username/Password ] [ Certificate ] |
| ----------------------------- |
| | Username/Password | |
| +---------------------------+-------------------------------- |
| Username: [_____________________________] |
| Password: [____________________________________________] |
| Stored as Secret: asset-<name>-creds (created on save) |
| |
| [< Back] [Continue >] |
+-------------------------------------------------------------------+
Certificate tab:
+-------------------------------------------------------------------+
| [ Anonymous ] [ Username/Password ] [ Certificate ] |
| ----------------------------- |
| | Certificate | |
| +-------------+--------------------- |
| Client cert (PEM): |
| +---------------------------------------------------------------+|
| | -----BEGIN CERTIFICATE----- ||
| | MIIDXTCCAkWgAwIBAgIJAKn... ||
| +---------------------------------------------------------------+|
| Client key (PEM): |
| +---------------------------------------------------------------+|
| | -----BEGIN PRIVATE KEY----- ||
| | MIIEvQIBADANBgkqhkiG9w0... ||
| +---------------------------------------------------------------+|
| |
| [ Generate client cert ] <- disabled, see #301 |
| |
| Stored as Secret: asset-<name>-cert |
+-------------------------------------------------------------------+
Notes:
- Username and certificate inputs disappear from the form on edit-after-
save. The "Replace credential" affordance reveals them again. The
underlying Secret is mutated in place (no delete-and-recreate).
- "Generate client cert" is rendered in btn-secondary with
disabled attribute and a tooltip "Auto-generation tracked in #301".
Method invocation form¶
The form lives on the Asset detail page, "Actions" tab, one card per discovered method.
+-------------------------------------------------------------------+
| Asset: granulator-1 / Actions |
+-------------------------------------------------------------------+
| |
| +-------------------------------------------------------------+ |
| | Reset() | |
| | Node: ns=2;s=Granulator1.Reset | |
| | Description: Resets the granulator to idle. | |
| | | |
| | Input arguments: | |
| | (none) | |
| | | |
| | [ Invoke ] | |
| +-------------------------------------------------------------+ |
| |
| +-------------------------------------------------------------+ |
| | Acknowledge(alarmId, comment) | |
| | Node: ns=2;s=Granulator1.Acknowledge | |
| | | |
| | Input arguments: | |
| | alarmId String [ AL-1234 ] | |
| | comment String [ Confirmed by D. Bhaskar ] | |
| | | |
| | Output arguments will appear here after invocation. | |
| | | |
| | [ Invoke ] | |
| +-------------------------------------------------------------+ |
| |
| Invocation log (most recent 20): |
| 2026-05-08 10:14:02 Reset() OK user: dhananjay |
| 2026-05-08 10:12:51 Acknowledge OK user: dhananjay |
| ... full history in Audit Trail [Open Audit Trail >] |
| |
+-------------------------------------------------------------------+
Notes for #298:
- Each invocation produces one AuditRecord. In enforced topologies
(per the GitOps Enforcement RFC),
state-mutating methods route through a ChangeRequest and require an
e-signature before Invoke becomes enabled. The threshold for "state-
mutating" is initially conservative: every method requires e-signature
unless the Asset declares protocolHints["readonly"]: "true" on the
property. Refining the threshold is part of #298.
- After Invoke, the form replaces the input panel with an output panel
showing each output argument's name, type, and value, plus the
StatusCode.
- The "Invocation log" footer is the last-N projection of the AuditRecord
stream filtered by this Asset. The full filterable view is the existing
Audit Trail page.
Reconciliation with already-filed implementation issues¶
This section records where the RFC's decisions overlap an existing issue's scope and what (if anything) needs updating on the issue. None of these reconciliations expand or contract scope of the existing issues. They only confirm shared assumptions.
#310 — wizard + detail + Explorer¶
Overlap: full. This is the main implementation issue this RFC serves. The seven-step wizard listed in #310 is the same one this RFC wireframes, and the auth tabs, endpoint picker, property picker, and detail page are all the same surfaces. No scope change. Comment posted to confirm the wireframes here match the issue's acceptance criteria.
#298 — method invocation¶
Overlap: UI form lives in this RFC's decisions (item 16), and the "Method invocation form" wireframe is the design #298 implements. The 30-second sync timeout for V1 vs full async is the same split this RFC records. No scope change. Comment posted with a link to the wireframe.
#301 — cert auto-gen + trust-list inbox¶
Overlap: BYO-cert path ships in V1 and in #310, while auto-gen and the standalone inbox are this issue. The first-trust in-line approval flow is in V1 (#310), while bulk re-pin and the rotation queue are this issue. No scope change. Comment posted clarifying which UX surfaces V1 vs this issue owns.
#297 — companion-spec import¶
Overlap: the [PA-DIM Pump] badge in the browse tree wireframe is
this issue's badge, and the "Use template" button on the picker is this
issue's button. Both are deliberately shown in the wireframe so the
shape is locked even though the data driving them lands later. No
scope change. Comment posted confirming UI affordances match.
#303 — namespace URI vs index¶
Overlap: the namespace filter chip strip and the dual-form address display in the wireframes are this issue. The "use stable URI form" toggle that lives on the Review step is this issue. No scope change. Comment posted confirming the chip-strip + dual-form rendering matches.
Followup issues¶
The decisions above defer two items that need their own implementation issues. Both are filed alongside this RFC.
-
Server-side OPC UA browse search — #318. Decision item 4 ships only client-side filter on already-loaded children. A bounded recursive browse with a depth cap is needed for large servers (Siemens namespaces with thousands of nodes). Labels:
enhancement,area: gateway,ui, priority medium. -
Struct / Array property faceplate decomposition — #319. Decision item 13 records Struct and Array as "subscribed but not displayed" with a badge. A future faceplate that decomposes a Struct into sub-property faceplates (and an Array into a vector display) is a separate piece of work. Labels:
enhancement,area: gateway,ui, priority low. -
Per-method e-signature threshold refinement. Decision item 17 defaults to "every method requires e-signature unless explicitly marked read-only". A more refined threshold (e.g., the OPC UA Method metadata classifies side-effect-free getters) is part of #298 already, so no new issue is needed.
Open questions¶
These are deliberately deferred to the followup issues filed above.
- Server-side recursive browse search shape (page size, depth cap, cancellation semantics).
- Struct decomposition: do we render sub-properties in the same faceplate or open a child faceplate per property?
- Long-running method async pattern: subscription on a status property, separate cancellation method, or both?
Acceptance for #294¶
- This RFC merged.
- Each open question listed in #294's "Open questions" section answered above with either a decision or an explicit deferral.
- ASCII wireframes for the four required surfaces (browse tree, endpoint picker, wizard auth tabs, method invocation form) included above.
- Cross-references to #310, #298, #301, #297, #303, and the new followup issues above.
The implementation lives in the linked issues. The wizard, Explorer, detail page, method form, trust-list inbox, and companion-spec template button are explicitly out of scope here.