Files
lmxopcua/docs/plans/2026-06-12-galaxy-standard-driver-phase-a.md
T
Joseph Doherty 1c4052ee83 docs(galaxy): Phase A implementation plan (Galaxy standard driver)
18-task plan to make GalaxyMxGateway an Equipment-kind driver: retire the
SystemPlatform NamespaceKind split + mirror + alias/relay machinery, author
Galaxy points as ordinary equipment tags via the standard TagModal. Mostly
deletion + a single EF migration dropping the per-kind unique constraint.
Phases B (native alarms) + C (server historian) remain out of scope.
Co-located .tasks.json for resume.
2026-06-12 21:05:45 -04:00

29 KiB

Galaxy Standard Driver — Phase A Implementation Plan

For Claude: REQUIRED SUB-SKILL: Use superpowers-extended-cc:executing-plans to implement this plan task-by-task.

Goal: Make GalaxyMxGateway a valid Equipment-kind driver — retire the SystemPlatform namespace split, the SystemPlatform mirror, and the alias-tag/relay machinery — so Galaxy points are authored as ordinary equipment Tags through the standard TagModal.

Architecture: Phase A is mostly deletion + relaxing validation, not new runtime wiring. Galaxy live values already work on equipment tags (alias tags prove it via direct driver subscription by FullName). We remove the NamespaceKind.SystemPlatform enum value and every reference (validator, composer/artifact mirror, authorization, registry, namespace-create UI), delete the alias/relay AdminUI machinery, and add one EF migration to drop the per-kind unique constraint. Native Galaxy alarms (Phase B) and server-side historian (Phase C) are out of scope and get their own plans.

Tech Stack: .NET 10, EF Core (SQL Server, string-converted enum column), Akka.NET, Blazor Server (no bUnit), xUnit + Shouldly. Design doc: docs/plans/2026-06-12-galaxy-standard-driver-design.md (master 91cb9076).

Branch: feat/galaxy-standard-driver-phase-a off master 91cb9076.


Hard rules (every task)

  • Stage by path; never git add .. Never stage sql_login.txt, src/Server/ZB.MOM.WW.OtOpcUa.Host/pki/, or pending.md.
  • Never echo the gateway API key or the historian SharedSecret into a tracked file.
  • No force-push, no --no-verify.
  • Commit per task, by explicit path.

Dependency spine (read before executing)

  • Composition contract: Task 4 removes GalaxyTags from Phase7CompositionResult + the GalaxyTagPlan record. Tasks 5, 6, 7 depend on that shape change. Task 4 → 5 → 6, and 4 → 7.
  • Enum capstone: Task 9 removes the NamespaceKind.SystemPlatform / NamespaceKindCompatibility.SystemPlatform enum values. It is the capstone — every reference must already be gone, so Task 9 is blockedBy Tasks 3, 4, 5, 8. Build must be green after Task 9.
  • Migration: Task 10 (EF migration + DbContext index change) is blockedBy Task 9 (the model must be settled).
  • AdminUI spine: UnsTreeService.cs / IUnsTreeService.cs / EquipmentChildRows.cs change in lockstep (Task 12). EquipmentPage.razor (Task 13) and TagModal.razor (Task 14) depend on Task 12 and edit disjoint files (parallelizable with each other).

Task order

  1. Branch
  2. DriverTypeRegistry: GalaxyMxGateway → Equipment
  3. DraftValidator: drop Galaxy/SystemPlatform branches + rename alias validation
  4. Phase7Composer: remove Galaxy mirror producer + exception clause
  5. Phase7CompositionResult + GalaxyTagPlan: remove GalaxyTags
  6. DeploymentArtifact: remove Galaxy mirror decode + isGalaxyAlias exception
  7. Phase7Applier + OpcUaPublishActor: delete MaterialiseGalaxyTags + call site
  8. Authorization: remove ScopeKind.SystemPlatform + WalkSystemPlatform
  9. Composer↔Artifact byte-parity test (Galaxy equipment tag)
  10. Capstone: remove SystemPlatform enum values; build green
  11. EF migration: drop per-kind unique constraint + delete orphaned rows
  12. Delete alias-only AdminUI files
  13. UnsTreeService/IUnsTreeService/EquipmentChildRows: strip alias/relay
  14. EquipmentPage.razor: strip alias UI
  15. TagModal.razor: wire Galaxy picker
  16. NamespaceEdit.razor: hide SystemPlatform option
  17. Docs: Uns.md + CLAUDE.md
  18. Verify: build + test + user-driven live /run

Task 0: Create feature branch

Classification: trivial Estimated implement time: ~1 min Parallelizable with: none

Files: none (git only)

Step 1: From master at 91cb9076:

git checkout master
git rev-parse HEAD   # expect 91cb9076...
git checkout -b feat/galaxy-standard-driver-phase-a

Step 2: Confirm clean tree (ignore untracked pending.md, sql_login.txt, pki/, docs scratch files — never stage them).


Task 1: DriverTypeRegistry — GalaxyMxGateway allowed in Equipment

Classification: small Estimated implement time: ~3 min Parallelizable with: Task 11

Files:

  • Modify: src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs (the DriverTypeMetadata(...) registration — its AllowedNamespaceKinds arg)
  • Test: tests/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Tests/ (registration metadata test, if one exists; otherwise assert in an existing factory test)

Context: DriverTypeMetadata.AllowedNamespaceKinds is a NamespaceKindCompatibility flags value. Galaxy currently registers NamespaceKindCompatibility.SystemPlatform. Change it to NamespaceKindCompatibility.Equipment.

Step 1: Grep for the registration to find the exact line:

grep -n "NamespaceKindCompatibility\|DriverTypeMetadata(" src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs

Step 2: Change the AllowedNamespaceKinds argument from NamespaceKindCompatibility.SystemPlatform to NamespaceKindCompatibility.Equipment.

Step 3: Build the Galaxy driver project:

dotnet build src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy

Expected: 0 errors.

Step 4: Commit:

git add src/Drivers/ZB.MOM.WW.OtOpcUa.Driver.Galaxy/GalaxyDriverFactoryExtensions.cs
git commit -m "feat(galaxy): register GalaxyMxGateway as an Equipment-kind driver"

Task 2: DraftValidator — drop Galaxy/SystemPlatform branches; rename alias validation

Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 11

Files:

  • Modify: src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs:34,36,41-55,225-245
  • Test: tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/Validation/DraftValidatorTests.cs (or the existing validator test file — grep for DriverNamespaceKindMismatch / AliasTagMissingReference)

Context: ValidateDriverNamespaceCompatibility (225-245) currently rejects GalaxyMxGateway under Equipment and rejects non-Galaxy under SystemPlatform. After de-split, Galaxy under Equipment must be valid. ValidateAliasTagFullName (41-55) requires TagConfig.FullName on a Galaxy-bound equipment tag — keep this rule (a Galaxy equipment tag still needs a reference) but rename it from "alias" to "Galaxy tag".

Step 1 (test first): Add/extend a test: a draft with a GalaxyMxGateway driver in an Equipment namespace produces no DriverNamespaceKindMismatch error; a Galaxy equipment tag with empty TagConfig still produces the missing-reference error (now GalaxyTagMissingReference). Run it — expect FAIL (current code rejects Galaxy under Equipment).

Step 2: In ValidateDriverNamespaceCompatibility, delete the switch's Galaxy/SystemPlatform branches. With SystemPlatform gone the method has no remaining rule — delete the method and its call at line 34. (If a reviewer prefers a stub for future rules, leaving a => true no-op is acceptable, but YAGNI says delete.)

Step 3: Rename ValidateAliasTagFullNameValidateGalaxyTagFullName; change the error code AliasTagMissingReferenceGalaxyTagMissingReference and the message wording from "Alias tag" to "Galaxy tag". Keep ExtractTagConfigFullName (also used by {{equip}} validation elsewhere).

Step 4: Run the validator tests — expect PASS.

dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests --filter "FullyQualifiedName~DraftValidator"

Step 5: Commit:

git add src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Validation/DraftValidator.cs tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests/Validation/DraftValidatorTests.cs
git commit -m "feat(validation): allow GalaxyMxGateway under Equipment; rename Galaxy-tag FullName check"

Task 3: Phase7Composer — remove the Galaxy mirror producer + exception clause

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none (composition contract; gates Tasks 4-8)

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs:338-360,365-374
  • Test: tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests/ (existing composer tests — grep GalaxyTag, MaterialiseGalaxy, equipmentTags)

Context: Lines 338-360 build galaxyTags (the SystemPlatform mirror, t.EquipmentId is null + ns.Kind == SystemPlatform). Lines 365-374 build equipmentTags with the Galaxy exception clause (ns.Kind == NamespaceKind.Equipment || di.DriverType == "GalaxyMxGateway"). After de-split a Galaxy driver lives in an Equipment namespace, so the exception is dead.

Step 1: Delete the galaxyTags producer (338-360) and stop returning it in the Phase7CompositionResult it constructs (the GalaxyTags: argument — coordinated with Task 4; if Task 4 hasn't run yet, temporarily pass []).

Step 2: In the equipmentTags predicate, remove || di.DriverType == "GalaxyMxGateway" so it reads && ns.Kind == NamespaceKind.Equipment. Update the now-stale comment.

Step 3: Update composer tests: a Galaxy equipment tag (EquipmentId set, GalaxyMxGateway, Equipment-kind ns) appears in EquipmentTags; assert no GalaxyTags are produced. Run:

dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests --filter "FullyQualifiedName~Phase7Composer"

Step 4: Commit (composer + its tests).


Task 4: Phase7CompositionResult — remove GalaxyTags + GalaxyTagPlan

Classification: standard Estimated implement time: ~3 min Parallelizable with: none

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs (the Phase7CompositionResult record GalaxyTags member + the GalaxyTagPlan record def, ~lines 21, 91-97)

Context: Once no producer emits Galaxy mirror plans, remove the contract.

Step 1: Remove the GalaxyTags property from Phase7CompositionResult and delete the GalaxyTagPlan record. Fix the composer's result construction (drop the GalaxyTags: arg).

Step 2: Build the OpcUaServer project; fix any remaining references in this project only (Task 5/6 handle the others). Expect references in DeploymentArtifact + Phase7Applier to break — those are Tasks 5 and 6.

dotnet build src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer

Step 3: Commit.

blockedBy: Task 3.


Task 5: DeploymentArtifact — remove Galaxy mirror decode + isGalaxyAlias exception

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none (byte-parity partner of composer)

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.Runtime/Drivers/DeploymentArtifact.cs:366-437,375-391,447-535,712-727

Context: BuildGalaxyTagPlans (366-437) mirrors the composer's mirror producer; the systemPlatformNamespaces decode (375-391) reads Kind==1/"SystemPlatform"; BuildEquipmentTagPlans (447-535) carries the driverToType map + isGalaxyAlias exception (513-514). All retire. Must stay byte-parity with the composer (Task 3): equipment-tag plans must be identical for the same input.

Step 1: Delete BuildGalaxyTagPlans and its call site; remove the GalaxyTags from the artifact's decoded composition (matching Task 4's contract).

Step 2: Delete the systemPlatformNamespaces decode block (375-391) and any use of it.

Step 3: In BuildEquipmentTagPlans, remove the driverToType map and the isGalaxyAlias exception; the equipment-tag gate becomes if (!equipmentNamespaces.Contains(nsId)) continue;. Keep ExtractTagFullName (712-727) — Galaxy equipment tags still carry FullName.

Step 4: Build Runtime + OpcUaServer:

dotnet build src/Server/ZB.MOM.WW.OtOpcUa.Runtime

Expect 0 errors. Run existing DeploymentArtifact tests.

Step 5: Commit.

blockedBy: Task 4.


Task 6: Phase7Applier + OpcUaPublishActor — delete MaterialiseGalaxyTags + call site

Classification: standard Estimated implement time: ~3 min Parallelizable with: Task 7

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Applier.cs:152-179 (delete MaterialiseGalaxyTags)
  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.Runtime/OpcUa/OpcUaPublishActor.cs:~244-248 (delete the _applier.MaterialiseGalaxyTags(composition); call + its comment)

Context: MaterialiseEquipmentTags (202-239) stays and already materializes Galaxy equipment tags. Only the mirror pass retires.

Step 1: Delete MaterialiseGalaxyTags from Phase7Applier.

Step 2: Delete the call in OpcUaPublishActor (the GalaxyTags comment + the MaterialiseGalaxyTags(composition) line).

Step 3: Build + run applier/publish tests (grep tests for MaterialiseGalaxy and delete/adjust any).

dotnet build src/Server/ZB.MOM.WW.OtOpcUa.Runtime

Step 4: Commit.

blockedBy: Task 4.


Task 7: Authorization — remove ScopeKind.SystemPlatform + WalkSystemPlatform

Classification: high-risk Estimated implement time: ~4 min Parallelizable with: Task 6

Files:

  • Modify: src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/NodeScope.cs:~57 (remove SystemPlatform from the ScopeKind enum)
  • Modify: src/Core/ZB.MOM.WW.OtOpcUa.Core/Authorization/PermissionTrieBuilder.cs (the WalkSystemPlatform call ~line 58 + method def ~82-90)
  • Test: tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests/Authorization/ (grep SystemPlatform)

Context: With no SystemPlatform namespaces possible, the SystemPlatform ACL walk is dead. Galaxy permissions now flow through the standard Equipment walk (Galaxy tags are equipment tags). This is authz — review carefully.

Step 1: Remove the WalkSystemPlatform call and method, and the ScopeKind.SystemPlatform enum value. Confirm the Equipment walk covers Galaxy equipment tags (it keys on EquipmentId/path, driver-agnostic).

Step 2: Update/remove any authz test asserting a SystemPlatform scope. Add an assertion (if a fixture exists) that a Galaxy equipment tag resolves under the Equipment permission trie.

Step 3: Build Core + run authz tests:

dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests --filter "FullyQualifiedName~Authoriz"

Step 4: Commit.

blockedBy: Task 4 (compile order; avoids churn while composition contract shifts).


Task 8: Composer ↔ Artifact byte-parity test (Galaxy equipment tag)

Classification: standard Estimated implement time: ~5 min Parallelizable with: none

Files:

  • Test: tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests/Drivers/ (extend or replace the existing alias-parity test — grep DeploymentArtifactAliasParity / Phase7ComposerAliasTag)

Context: The invariant: for the same draft input, Phase7Composer.Compose(...).EquipmentTags and DeploymentArtifact's decoded equipment-tag plans are identical when the input includes a Galaxy equipment tag (EquipmentId set, GalaxyMxGateway, Equipment-kind ns, TagConfig={"FullName":"DelmiaReceiver_001.DownloadPath"}). This replaces the old alias-exception parity test with the now-canonical case.

Step 1 (test first): Author a fixture draft: one Equipment namespace, one GalaxyMxGateway driver bound to it, one equipment tag with the Galaxy FullName. Compose via the composer; serialize to artifact; decode; assert the equipment-tag plan lists match exactly (FullName, NodeId scoping, ordering). Run — should pass once Tasks 3+5 are in; if it fails, a parity divergence is a real defect.

Step 2: Commit.

blockedBy: Tasks 3, 5.


Task 9: CAPSTONE — remove SystemPlatform enum values; build green

Classification: high-risk Estimated implement time: ~4 min Parallelizable with: none

Files:

  • Modify: src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Enums/NamespaceKind.cs:13-17 (remove SystemPlatform; keep Equipment, Simulated)
  • Modify: src/Core/ZB.MOM.WW.OtOpcUa.Core.Abstractions/DriverTypeRegistry.cs:113-114 (remove NamespaceKindCompatibility.SystemPlatform = 2)

Context: Capstone — every reference must already be gone (Tasks 1,2,3,5,7). Removing the value makes any missed reference a compile error, which is the point.

Step 1: Remove both enum members + their doc comments.

Step 2: Full-solution build:

dotnet build ZB.MOM.WW.OtOpcUa.slnx

Expected: 0 errors. Any error names a missed reference — fix it in the owning file (do not re-add the enum value).

Step 3: Full test pass on the touched suites:

dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests
dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests

Step 4: Commit.

blockedBy: Tasks 1, 2, 3, 5, 7. (Task 6 too — its call site references nothing SystemPlatform but keep it before the capstone for a clean build.)


Task 10: EF migration — drop per-kind unique constraint + delete orphaned rows

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none

Files:

  • Modify: src/Core/ZB.MOM.WW.OtOpcUa.Configuration/OtOpcUaConfigDbContext.cs:207-208 (change the unique index from (ClusterId, Kind) to (ClusterId), rename UX_Namespace_Cluster_KindUX_Namespace_Cluster)
  • Create: src/Core/ZB.MOM.WW.OtOpcUa.Configuration/Migrations/<timestamp>_DropSystemPlatformNamespaceKind.cs (+ the .Designer.cs + updated OtOpcUaConfigDbContextModelSnapshot.cs, generated by the tool)

Context: Clean break. Keep the Kind column (still Equipment/Simulated, string-converted) but stop enforcing one-per-kind; enforce one namespace per cluster instead. Delete orphaned SystemPlatform rows (and dependents) so the reduced enum never reads an unmapped value. Dev rigs reseed by hand.

Step 1: Edit OtOpcUaConfigDbContext.ConfigureNamespace: change e.HasIndex(x => new { x.ClusterId, x.Kind }).IsUnique().HasDatabaseName("UX_Namespace_Cluster_Kind") to e.HasIndex(x => x.ClusterId).IsUnique().HasDatabaseName("UX_Namespace_Cluster").

Step 2: Author the migration (the design-time factory reads OTOPCUA_CONFIG_CONNECTION; migrations add does not connect, but set it to be safe):

export OTOPCUA_CONFIG_CONNECTION="Server=10.100.0.35,14330;Database=OtOpcUaConfig;Trusted_Connection=True;TrustServerCertificate=True;"
dotnet ef migrations add DropSystemPlatformNamespaceKind --project src/Core/ZB.MOM.WW.OtOpcUa.Configuration

Step 3: Hand-edit the generated Up(...) so the order is safe:

// 1. delete dependents of SystemPlatform namespaces, then the namespaces themselves,
//    so the reduced NamespaceKind enum never deserialises a 'SystemPlatform' string.
migrationBuilder.Sql(@"
    DELETE FROM Tag            WHERE DriverInstanceId IN (SELECT DriverInstanceId FROM DriverInstance WHERE NamespaceId IN (SELECT NamespaceId FROM Namespace WHERE Kind = 'SystemPlatform'));
    DELETE FROM DriverInstance WHERE NamespaceId   IN (SELECT NamespaceId FROM Namespace WHERE Kind = 'SystemPlatform');
    DELETE FROM Namespace      WHERE Kind = 'SystemPlatform';");
// 2. swap the unique index.
migrationBuilder.DropIndex(name: "UX_Namespace_Cluster_Kind", table: "Namespace");
migrationBuilder.CreateIndex(name: "UX_Namespace_Cluster", table: "Namespace", column: "ClusterId", unique: true);

(Verify the actual dependent tables/FKs against the snapshot before finalizing — adjust the DELETE cascade to match real FK names. A reviewer must confirm no dependent table is missed.)

Step 4: Provide a minimal Down(...) (recreate UX_Namespace_Cluster_Kind, drop UX_Namespace_Cluster; data deletes are not reversible — note that in a comment). Build the Configuration project.

Step 5: Commit (migration files + DbContext). Do not apply the migration here — it applies at deploy/runtime against the central SQL Server.

blockedBy: Task 9.


Task 11: Delete alias-only AdminUI files

Classification: small Estimated implement time: ~2 min Parallelizable with: Tasks 1, 2

Files (delete):

  • src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/AliasTagModal.razor (+ any .razor.cs)
  • src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/RelayConversion.cs
  • src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/AliasTagEditDto.cs
  • src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/AliasTagInput.cs
  • src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/RelayAliasConvert.razor (route /uns/convert-relays)

Context: These are alias/relay-only. Project won't compile until Task 12 removes the references — that's expected; this task is the file-level delete.

Step 1: git rm each file by path. Step 2: Grep for any remaining RelayAliasConvert / convert-relays nav link (e.g. a NavMenu/NavLink) and remove it. Step 3: Commit (build will be red until Task 12 — note in the commit body).

Note for executor: because Tasks 11+12 leave AdminUI uncompilable between them, dispatch them as a bundle (one reviewer over the combined diff) rather than gating a build between them.


Task 12: UnsTreeService / IUnsTreeService / EquipmentChildRows — strip alias/relay

Classification: high-risk Estimated implement time: ~5 min Parallelizable with: none (AdminUI spine)

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs (delete: LoadAliasTagAsync 207-215, LoadGalaxyGatewaysForEquipmentAsync 778-798, CreateAliasTagAsync 907-944, UpdateAliasTagAsync 947-1001, ConvertRelayVirtualTagsToAliasesAsync 1036-1214, CheckAliasDriverGuardAsync 1595-1623, BuildAliasTagConfig ~1628, BuildAliasTag ~1637-1649, the EquipmentName record + comparer ~1652-1670; in LoadTagsForEquipmentAsync 86-118 remove the IsAlias/Source decoration 90-92 + 113-116). Keep ExtractTagConfigFullName (used by {{equip}} validation).
  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/IUnsTreeService.cs (delete members: LoadAliasTagAsync 178-182, LoadGalaxyGatewaysForEquipmentAsync 353-358, CreateAliasTagAsync 388-399, UpdateAliasTagAsync 401-413, ConvertRelayVirtualTagsToAliasesAsync 558-567)
  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/EquipmentChildRows.cs:10-12 (remove IsAlias/Source from EquipmentTagRow)
  • Delete: tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceAliasTagTests.cs, tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/UnsTreeServiceRelayConverterTests.cs

Context: These three files change in lockstep (interface + impl + row DTO). LoadTagsForEquipmentAsync returns EquipmentTagRow(TagId, Name, DriverInstanceId, DataType, AccessLevel) after the IsAlias/Source removal.

Step 1: Delete the listed methods/members and the test files. Trim LoadTagsForEquipmentAsync to the base tag load + new row shape.

Step 2: Build AdminUI together with Task 13 (this task alone leaves EquipmentPage referencing removed methods). Defer the green build to Task 13.

Step 3: Commit (service + interface + rows + deleted tests).

blockedBy: none for code, but bundle with Task 11 (see note).


Task 13: EquipmentPage.razor — strip alias UI; route Galaxy through standard TagModal

Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 14

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Uns/EquipmentPage.razor (remove: "Add alias" button ~146, "Convert relay" button ~150, alias row display ~181-186, IsAlias edit/delete routing ~188-195, convert preview panel ~204-289, AliasTagModal instance ~301-303, state vars ~426-440, handlers OpenAddAlias/OpenEditAlias/OnAliasSavedAsync/PreviewConvertRelaysAsync/ApplyConvertRelaysAsync ~526-585, the LoadGalaxyGatewaysForEquipmentAsync calls ~482/529, the Source column ~170/184)

Context: All tags (including Galaxy) use the existing "Add tag" → standard TagModal path. The tag table shows no alias badge/Source column; Edit/Delete are uniform for every row.

Step 1: Remove the alias UI, state, and handlers per the file list. Keep the Details / standard-Tag / VirtualTag / Alarm sections.

Step 2: Ensure the "Add tag" button opens the standard TagModal with the equipment's driver list (which now includes Galaxy drivers — they're Equipment-kind post-de-split).

Step 3: Build AdminUI (now compilable with Task 12):

dotnet build src/Server/ZB.MOM.WW.OtOpcUa.AdminUI

Expected: 0 errors.

Step 4: Commit.

blockedBy: Task 12.


Task 14: TagModal.razor — wire the Galaxy address picker

Classification: standard Estimated implement time: ~5 min Parallelizable with: Task 13

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor (picker dispatch: when SelectedDriverType == "GalaxyMxGateway" show GalaxyAddressPickerBody inside the existing DriverTagPicker, writing the chosen ref into TagConfig as {"FullName":"..."})
  • Possibly Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/UnsTreeService.cs LoadTagDriversForEquipmentAsync — verify it returns the Galaxy driver's DriverConfig (the picker opens a browse session with the gateway config); if the driver-list tuple lacks DriverConfig, extend it.

Context: The standard TagModal already dispatches address pickers by driver type. GalaxyMxGateway keeps the raw-JSON TagConfig editor (no TagConfigEditorMap entry needed) — the picker just fills FullName. The Galaxy picker (GalaxyAddressPickerBody) needs the selected gateway's DriverConfig to open a live browse session via IBrowserSessionService.OpenAsync("GalaxyMxGateway", configJson) (same call the old AliasTagModal made).

Step 1: Add the GalaxyMxGatewayGalaxyAddressPickerBody case to the picker dispatch, mirroring how Modbus/S7 pickers are wired and how the old AliasTagModal built the result (tag.attribute dot-join → {"FullName":...}).

Step 2: Confirm LoadTagDriversForEquipmentAsync surfaces Galaxy drivers + their config to the modal; extend the returned shape if needed.

Step 3: Build AdminUI:

dotnet build src/Server/ZB.MOM.WW.OtOpcUa.AdminUI

Step 4: Commit. (Razor behavior is proven live in Task 17 — no bUnit.)

blockedBy: Task 12.


Task 15: NamespaceEdit.razor — hide the SystemPlatform option

Classification: small Estimated implement time: ~2 min Parallelizable with: Tasks 13, 14

Files:

  • Modify: src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Clusters/NamespaceEdit.razor (the NamespaceKind selector — drop SystemPlatform; offer only Equipment, with Simulated reserved/hidden as it is today)

Context: With the enum value removed (Task 9) this likely already won't compile if it enumerates the value; this task makes the selector explicit and clean.

Step 1: Remove SystemPlatform from the kind dropdown / any Enum.GetValues binding; default new namespaces to Equipment.

Step 2: Build AdminUI; commit.

blockedBy: Task 9.


Task 16: Docs — Uns.md + CLAUDE.md

Classification: small Estimated implement time: ~4 min Parallelizable with: Tasks 13, 14, 15

Files:

  • Modify: docs/Uns.md (remove the alias-tag / relay-converter sections; note Galaxy tags are ordinary equipment tags bound to GalaxyMxGateway)
  • Modify: CLAUDE.md (rewrite the "Alias tags" paragraph in the Architecture section + the SystemPlatform/Galaxy notes — Galaxy is now a standard Equipment driver; no NamespaceKind.SystemPlatform, no alias/relay machinery)

Context: Keep docs honest with the new model. Do not echo any secret.

Step 1: Edit both docs. Cross-reference the design doc docs/plans/2026-06-12-galaxy-standard-driver-design.md.

Step 2: Commit.


Task 17: Verify — build, test, user-driven live /run

Classification: verification Estimated implement time: ~5 min (agent) + user live step Parallelizable with: none

Files: none (verification)

Step 1: Full solution build + the touched test suites:

dotnet build ZB.MOM.WW.OtOpcUa.slnx
dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Configuration.Tests
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests
dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests
dotnet test tests/Core/ZB.MOM.WW.OtOpcUa.Core.Tests

Expected: build 0 errors; all green.

Step 2 (USER drives — the agent does NOT sign in): Hand the user the live-verify checklist:

  • Reseed a docker-dev rig: a cluster with one Equipment namespace, a GalaxyMxGateway driver bound to it, one equipment tag authored via the standard TagModal + Galaxy picker ({"FullName":"<obj>.<attr>"}).
  • Confirm the alias UI is gone (no "Add alias" button, no /uns/convert-relays).
  • Confirm the Galaxy equipment tag shows a live value over OPC UA (Client.CLI read/subscribe).
  • Confirm an existing scripted alarm on that equipment still raises/acks (native Galaxy alarms are Phase B — not expected yet).

Step 3: When the user confirms green, finish via superpowers-extended-cc:finishing-a-development-branch (carry intent toward merge-to-master + push, but confirm given the user-driven live step).

blockedBy: all prior tasks.


Out of scope (future plans)

  • Phase B — native IAlarmSource alarms on the equipment-tag path (port the GenericDriverNodeManager forwarder into MaterialiseEquipmentTags).
  • Phase C — server-side HistoryRead backend over the Wonderware Historian reader; IsHistorized → UA Historizing/HistoryRead attributes; tag→tagname mapping.