# Current status — Galaxy standard-driver effort

_Updated 2026-06-13._

## Where we are

**Phase A (Galaxy = standard Equipment-kind driver) is DONE, merged to master (`c3c56172`), pushed, and live-verified.**

- `GalaxyMxGateway` is now an ordinary **Equipment-kind** driver. Retired: the `SystemPlatform` `NamespaceKind` + its mirror (`Phase7Applier.MaterialiseGalaxyTags`, the composer/artifact `GalaxyTags` contract + `GalaxyTagPlan`), the byte-parity `|| DriverType=="GalaxyMxGateway"` exception clauses, the authz `NodeHierarchyKind.SystemPlatform` + `PermissionTrie.WalkSystemPlatform`, and the **entire alias-tag/relay AdminUI machinery**.
- A Galaxy point is now a plain equipment `Tag{EquipmentId set, DriverInstanceId=GalaxyMxGateway, TagConfig={"FullName":"tag.attr"}}`, authored through the standard `TagModal` + the "Browse Galaxy" picker.
- Forward-only data-only migration `CleanupSystemPlatformNamespaces` deletes orphaned SystemPlatform config + `NodeAcl` grants and releases `ExternalIdReservation`s. Kept `UX_Namespace_Cluster_Kind` (Simulated kind still reserved).
- Gate: 1009 unit tests green + full docker-dev live `/run` (migration applied clean, server boots on migrated DB, alias UI gone, namespace picker drops SystemPlatform, Galaxy driver created **under Equipment**, Galaxy tag authored via the new picker).

Design: `docs/plans/2026-06-12-galaxy-standard-driver-design.md` (3-phase). Phase A plan: `docs/plans/2026-06-12-galaxy-standard-driver-phase-a.md` (+ `.tasks.json`).

---

## The rest of the plan (all NOT started)

### ✅ Milestone 1 — Equipment-tag live-value DELIVERY (the `FullName→NodeId` router)  — DONE, merged `c4435e4f`, pushed

Design `docs/plans/2026-06-13-equipment-tag-live-values-design.md`; plan `…-plan.md` (+ `.tasks.json`). Mirrored the proven `VirtualTagHostActor._nodeIdByVtag` pattern for DRIVER values:
- NEW `ZB.MOM.WW.OtOpcUa.Commons.OpcUa.EquipmentNodeIds` = single source of truth for the folder-scoped NodeId formula (`Variable(eq,fp,name)`); repointed `Phase7Applier` + `VirtualTagHostActor` to it (byte-identical, kills the drift risk).
- `DriverInstanceId` added to `AttributeValuePublished` (the router key — FullName is unique only WITHIN a driver instance).
- `DriverHostActor` builds a `(DriverInstanceId, FullName) → NodeId[]` map each apply (in `PushDesiredSubscriptions`, via `EquipmentNodeIds.Variable`) and resolves it in `ForwardToMux`, emitting the existing `AttributeValueUpdate` per resolved NodeId (1→many handled; no-match drops+debug-logs). NO `OpcUaPublishActor` change.
- Gate: build 0 errors; **322 tests** (incl. 3 Akka.TestKit tests driving the full router through a real deploy/apply); final integration review = READY TO MERGE (end-to-end NodeId coherence confirmed). Live `/run`: the deploy applied + the variable **materialised at the exact router NodeId** + SubscribeBulk pushed refs — the materialisation + router wiring is confirmed live.

### 🔴 Milestone 1b — Per-driver "actually PUBLISH a value" gaps  (NOT started — the router has nothing to route until these land; live value could NOT be shown in dev for this reason)

The router is correct, but **no dev driver currently publishes an equipment-tag value**, so a variable still won't show data until the owning driver's publish path is wired. Three orthogonal, independent gaps (each its own follow-up):
- **OpcUaClient — MISSING FACTORY (real bug).** `src/Server/.../Host/Drivers/DriverFactoryBootstrap.cs:98-107` registers AbCip/AbLegacy/FOCAS/Galaxy/Modbus/S7/TwinCAT but **OMITS OpcUaClient** — and the `OpcUaClient` driver project has **no `…FactoryExtensions` class at all** (only `OpcUaClientDriver.cs` + `OpcUaClientDriverProbe.cs`). So OpcUaClient is **always stubbed** (`"no factory for driver type OpcUaClient … falling back to stub"`) — completely non-functional. Fix: add an `OpcUaClientDriverFactoryExtensions` (CreateInstance parsing `driverConfigJson`→`OpcUaClientDriverOptions` + `new OpcUaClientDriver(options, id, …)`, mirroring `ModbusDriverFactoryExtensions`) + register it in the bootstrap. **This is the cheapest path to a live value** (OpcUaClient is direct-ref: `TagConfig.FullName="ns=3;s=FastUInt1"` → subscribes to opc-plc → publishes → router delivers). opc-plc sim is up at `opc.tcp://10.100.0.35:50000` (`ns=3;s=FastUInt1`).
- **Protocol drivers (Modbus/S7/AbCip/…) — equipment-tag↔driver-subscription linkage.** The Modbus driver subscribes by **tag NAME from `DriverConfig.Tags`** (`_tagsByName`, keyed by `t.Name`), but an equipment tag's `TagConfig` (region/address, via `ModbusTagConfigModel`) carries **no top-level `FullName`**, so `ExtractTagFullName` falls back to the raw JSON blob → never matches a `DriverConfig.Tags` name → `SubscribeAsync` silently skips it (`if (!_tagsByName.TryGetValue(...)) continue;`). The equipment tag's register config is never connected to the driver. This is the bigger "equipment tags work for protocol drivers" gap (needs the deploy to build the driver's tag table from equipment tags, or the equipment-tag FullName to address a register directly).
- **Galaxy — needs a reachable mxaccessgw.** Direct-ref (FullName = MxAccess ref), so the router would deliver — but the gateway must be up + `ApiKeySecretRef` resolvable (dev `MX_API_KEY` unset).

> Recommended next: build the OpcUaClient factory (small, fixes a real bug, gives the first live value). Then the protocol-driver linkage (larger).

### 🟡 Phase B — Native alarms on the equipment-tag path  (NOT started)

Galaxy implements `IAlarmSource` (native MXAccess alarms), but that wiring lived **only** on the retired SystemPlatform-mirror path (`GenericDriverNodeManager` registers an alarm sink per `MarkAsAlarmCondition` variable keyed by `FullName`, routes `OnAlarmEvent.SourceNodeId` to it). After Phase A, **native Galaxy alarms have no materialization path** until this is ported.

**Work:** teach `MaterialiseEquipmentTags`/`OtOpcUaNodeManager` to register an alarm-condition sink for `IsAlarm` equipment tags (keyed by `FullName`) and subscribe the owning driver's `IAlarmSource`, routing `OnAlarmEvent.SourceNodeId == FullName` → sink. Reuse the existing Part 9 `AlarmConditionState` materialization + `alarm-commands`/ack plumbing (scripted alarms already use it). Extract `GenericDriverNodeManager`'s forwarder so both paths share it. Mirror its unsubscribe-before-rewalk for `IRediscoverable` redeploys.

### 🟢 Phase C — Server-side historian / `HistoryRead`  (NOT started)

Driver-agnostic server-side history backend over the existing Wonderware Historian reader (`Historian.Wonderware.Client` already implements `IHistoryProvider`). No mxaccessgw history RPC needed.

**Work:** add an OPC UA `HistoryRead` service override in `OtOpcUaNodeManager` (none exists today) → a router that, for an `IsHistorized` node, resolves a historian tagname (default = the tag's driver `FullName`, optional override) and calls the registered `IHistorianDataSource`. Apply `DriverAttributeInfo.IsHistorized` → UA `Historizing` + `AccessLevel.HistoryRead` at materialization (currently hardcoded `false`). Config: a server `Historian` section (Null default; Wonderware when configured), mirroring the existing `AlarmHistorian` pattern. Works for any `IsHistorized` tag, not just Galaxy.

---

## Dev rig state

docker-dev runs **locally** (Docker 29.4.0 on this Mac); login disabled in dev (`Security__Auth__DisableLogin: "true"`); central UI at `http://localhost:9200`, SQL at `localhost:14330` (`sa`/`OtOpcUa!Dev123`). Currently on the **Phase-A build** with the clean-break migration applied. Verification artifacts left in place: `MAIN-galaxy-eq` Galaxy driver + `GalaxyTestTag` under the `nw-uns` Equipment namespace. Prior 1,391-tag Galaxy config is restorable from `OtOpcUa-prePhaseA-20260612-224908.bak` (in the SQL volume `/var/opt/mssql/backup/`). Rebuild central from a branch: `docker compose -f docker-dev/docker-compose.yml up -d --build migrator central-1 central-2`.
