Files
ScadaBridge/docs/plans/2026-07-23-otopcua-v3-raw-path-live-gate.md
T
Joseph Doherty d1ade5653b docs(plans): bookkeeping sync — reconcile stale trackers with merged code
A verified audit of all ~90 plan documents (2026-08-01) found ~30 .tasks.json
trackers and several plan headers still reporting 'pending'/'draft' for work
fully merged to main. Sync them so future audits don't re-litigate closed work:

- Flip ~380 stale task statuses to completed across March/May/June/July
  trackers (audit-log series, milestones M5-M10, playwright waves, stillpending,
  LocalDb, ClusterClient->gRPC DoD rows, and more), each verified against
  code/git evidence before flipping.
- Annotate obsolete-not-done rows: ClusterClient CLI transport (never built,
  HTTP shipped), TreeView Areas/Instances pages (replaced by Topology),
  template-tree drag-drop (dropped for M9 menu reorder), otopcua item C
  (premise superseded by #17).
- Flip stale headers: aggregated-live-alarm + kpi-rollups 'Draft not executed'
  -> Delivered 2026-07-10; otopcua cutover-scope SCOPING -> DECIDED;
  scadabridge-rename -> Implemented; LocalDb phase1/2 status strings ->
  merged 28ca04d7.
- Fix doc drift: T9/T10 'deferred' -> shipped as SMS (Teams dropped); waitfor
  sandbox follow-up shipped; followups #52/#53/#54/#162/#207 resolved; purge
  TODO closed by PendingDeploymentPurgeActor; live-gate pre-existing failures
  #28/#29/#31 fixed; auto-down boot-alone residual superseded by self-first
  seeds; supersession banners on keep-oldest SBR + ClusterClient-era designs;
  requirements-traceability 'Pending' clarified as frozen plan-generation
  status.

Deliberately left pending (genuinely open, tracked in the pending-work list):
opcua-tag-browser task 19 (live smoke), ipsen tasks 7-8 (vd03 verification),
selfform task 7 (vd03 overlay, user-held), live-gate observation 1
(external-system delete orphan bug), otopcua item A + maxDepth calibration.
2026-08-01 08:53:56 -04:00

82 lines
5.7 KiB
Markdown

# OtOpcUa v3 raw-path live-gate (Gitea #14) — PASS
**Date:** 2026-07-23 · **Scope:** the raw-subtree slice of #14 (decision: "raw gate now, flag B/C").
**Rig:** ScadaBridge docker rig (`scadabridge:latest`, rebuilt with the host-rewrite fix) against the
**live `otopcua-dev` v3 cluster** (`otopcua-host:dev`, built 2026-07-23, branch `feat/mesh-phase5`,
past the v3 merge `ec6598ce`). Related: scope doc `2026-07-23-otopcua-v3-dual-namespace-cutover-scope.md`,
phase-2 plan `2026-07-23-otopcua-v3-nsu-hardening-and-browse-ux.md`.
## Headline
ScadaBridge is confirmed **structurally v3-safe against a real dual-namespace server** — durable
`nsu=` references are emitted by browse, stored on bindings, and resolved live against the server's
`NamespaceArray` to Good-quality reads. The gate also **surfaced and fixed a real DCL robustness
bug** (couldn't dial a server advertising `0.0.0.0`). Items **B/C remain deferred** — the dev
cluster has no UNS nodes and no native alarms to gate them against.
## Environment findings
- v3 cluster publishes both namespaces: `ns=2 = https://zb.com/otopcua/raw`,
`ns=3 = https://zb.com/otopcua/uns`. **Raw lands at ns=2 — the exact index collision scope §4
item A predicted** (v2's sole namespace and v3 raw both at ns=2).
- Raw tree is **sparsely populated**: one materialized live Variable,
`nsu=https://zb.com/otopcua/raw;s=sa-modbus2/plc/SiteAOnly` (Int16, AccessLevel=1 → **read-only**).
Other drivers (calc1, opcua1, pymodbus, gate-modbus, Area1) expose folders but no live leaf tags.
- **UNS (ns=3) is empty** — `ns=3;s=OtOpcUa``BadNodeIdUnknown`; no Equipment authored.
- **No native alarms** — zero `HasCondition`/`HasEventSource` refs anywhere in the raw tree.
## The fix this gate produced
`RealOpcUaClient` took the server-advertised `EndpointDescription.EndpointUrl` verbatim for the
session channel. OtOpcUa advertises `opc.tcp://0.0.0.0:4840/OtOpcUa` (its
`OpcUaApplicationHostOptions.PublicHostname` defaults to `0.0.0.0`), and dialling `0.0.0.0` from
inside the client container hits its own loopback → connect fails. Added
`RealOpcUaClient.RewriteEndpointHostForReachability` (swaps the advertised authority for the
reachable discovery host/port, preserves scheme+path, no-op when already reachable), called in both
`ConnectAsync` and `VerifyEndpointAsync`. Mirrors `CoreClientUtils.SelectEndpoint` and OtOpcUa's own
client (`IEndpointDiscovery`). Commit `a1abbff7` on `fix/opcua-discovery-host-rewrite`; 9 unit tests
(`RealOpcUaClientEndpointRewriteTests`).
## Wiring (how the gate was run)
1. Rebuilt `scadabridge:latest` with the fix (`bash docker/deploy.sh`).
2. `docker network connect otopcua-dev_default scadabridge-site-a-a` (and `-b`) — bridged the two
docker networks so site-a can resolve `otopcua-dev-site-a-1-1`.
3. `data-connection create --site-id 1 --name OtOpcUa-v3-raw --protocol OpcUa`
`endpointUrl=opc.tcp://otopcua-dev-site-a-1-1:4840/OtOpcUa`, securityMode none (id **3044**).
4. `deploy artifacts --site-id 1` — pushed the def to site-a's LocalDb + made it live in the DCL.
5. Template **2148** (`OtOpcUaV3Probe`) + attribute `SiteA` (Int32,
`--data-source nsu=https://zb.com/otopcua/raw;s=sa-modbus2/plc/SiteAOnly`); instance **98**
(`otopcua-v3-probe-1`) on site-a; `set-bindings [["SiteA",3044]]`; `deploy instance`.
## Checks
| # | Check | Result |
|---|-------|--------|
| 1 | Connect via `ConnectAsync` (through the rewrite fix) | **PASS**`data-connection browse` returned the address space |
| 2 | Connect via `VerifyEndpointAsync` (second rewrite call site) | **PASS**`verify-endpoint``{"success":true}` |
| 3 | Browse emits durable `nsu=` throughout | **PASS** — root `nsu=…/raw;s=OtOpcUa`, drivers + `…;s=sa-modbus2/plc/SiteAOnly` |
| 4 | Both namespaces visible / discoverable | **PASS (partial)** — raw browsable; uns registered but empty (nothing to browse) |
| 5 | Durable `nsu=` binding resolves live | **PASS** — attribute stores `nsu=…`, subscription resolves it against the live `NamespaceArray` |
| 6 | Read round-trip, Good quality, live | **PASS**`SiteA` Good, values changing 59689→59806→59822→59834 across snapshots |
| 7 | Write round-trip | **N/A** — the only materialized tag is read-only (AccessLevel=1); no writable v3 tag to gate |
## Deferred (unchanged — need OtOpcUa-side authoring on the dev cluster)
- **Item A** — **STILL OPEN** — re-author legacy `ns=2` bindings to `nsu=` (data; the collision is confirmed real here).
- ~~**Item B**~~ — **CLOSED 2026-07-23**, see below.
- ~~**Item C**~~ — **CLOSED 2026-07-23**, see below.
To gate B/C, the OtOpcUa dev cluster needs UNS Equipment authored (populate ns=3, referencing raw
tags) and at least one alarm-configured tag raising a Part-9 condition that fans to raw + equipment
notifiers.
**That authoring was done the same day and B/C were gated: `docs/plans/2026-07-23-otopcua-v3-alarm-bc-live-gate.md` — PASS, no ScadaBridge code change needed.** B: one subscription per connection delivers each retained condition once, to the most-specific monitored notifier, so overlapping bindings do not double-mirror. C: the premise was obsolete — routing keys off the **subscribed node reference** (`OpcUaAlarmMapper.BuildIdentity``SourceObjectReference`), not the event `SourceName` (Gitea #17). Item **A** remains the only open #14 work.
## Rig artifacts created (non-default rig state)
- ScadaBridge site-a-a / site-a-b attached to `otopcua-dev_default` (dropped on next `docker/deploy.sh`).
- Site-a config: DataConnection **3044** `OtOpcUa-v3-raw`, template **2148**, instance **98** (deployed).
Harmless; remove with `instance delete 98` / `template delete 2148` / `data-connection delete 3044`
if a clean rig is wanted.