From 39755d717de31ac0bf8e940bd8275c2539da00e3 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 16:11:42 -0400 Subject: [PATCH] docs(phase6): implementation plan + task persistence (10 tasks, execution deferred to post-compaction) --- ...2026-06-16-stillpending-phase-6-adminui.md | 144 ++++++++++++++++++ ...stillpending-phase-6-adminui.md.tasks.json | 20 +++ 2 files changed, 164 insertions(+) create mode 100644 docs/plans/2026-06-16-stillpending-phase-6-adminui.md create mode 100644 docs/plans/2026-06-16-stillpending-phase-6-adminui.md.tasks.json diff --git a/docs/plans/2026-06-16-stillpending-phase-6-adminui.md b/docs/plans/2026-06-16-stillpending-phase-6-adminui.md new file mode 100644 index 00000000..207ff917 --- /dev/null +++ b/docs/plans/2026-06-16-stillpending-phase-6-adminui.md @@ -0,0 +1,144 @@ +# Phase 6 — AdminUI Typed Editors, Pickers & UX — Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers-extended-cc:subagent-driven-development to implement this plan task-by-task. **Execution is post-compaction** — start by reading this plan + `docs/plans/2026-06-16-stillpending-phase-6-adminui-design.md`. Branch `feat/stillpending-phase-6-adminui` (off master `156aa900`) already exists with the design committed (`dbff29dd`). + +**Goal:** Complete the `/uns` Tag authoring UX — typed editors for every driver, historized fields as first-class controls, native-alarm AVEVA opt-out, address pickers reachable from the Tag modal, UNS-tree deletes for the top node kinds, and create-new-script from the inline vtag panel. + +**Architecture:** Blazor Server AdminUI under `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/`. Pure-C# cores (`*TagConfigModel` `FromJson`/`ToJson`/`Validate`, `UnsTreeService` methods, alarm sub-model) are unit-tested in `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/`; Razor shells are proven only by live `/run` (NO bUnit). NO EF migration, NO Commons contract change. + +**Global rules (every task):** TDD red→green for the pure cores. Stage by path — never `git add .`; never stage `sql_login.txt`, `src/Server/.../Host/pki/`, `pending.md`, `current.md`, `docker-dev/docker-compose.yml`, `stillpending.md`. No `--no-verify`, no force-push. NO EF migration. Preserve the `*TagConfigModel` **preserve-unknown-keys** contract. If `git commit` hits `.git/index.lock` (concurrent sibling), wait 2s + retry up to 5×. + +**Dependency graph:** Task1→Task2→Task5 (same TagEditors files, serial). Task3→Task4 (alarm form, serial). Task6, Task7 independent. **Concurrent start: Tasks 1, 3, 6, 7** (disjoint files). Then 2 (after 1), 4 (after 3), 5 (after 2). + +--- + +### Task 0: Feature branch *(done)* +Branch `feat/stillpending-phase-6-adminui` off `156aa900`; design committed `dbff29dd`. No action. + +--- + +### Task 1 (A): Typed editors for OpcUaClient + Historian.Wonderware + +**Classification:** standard · **Est:** ~5 min · **Parallelizable with:** Task 3, 6, 7 + +**Files:** +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/OpcUaClientTagConfigModel.cs` +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/HistorianWonderwareTagConfigModel.cs` +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/OpcUaClientTagConfigEditor.razor` +- Create: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagEditors/HistorianWonderwareTagConfigEditor.razor` +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigEditorMap.cs` (register `"OpcUaClient"` + `"Historian.Wonderware"`) +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/TagConfigValidator.cs` (register both) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/OpcUaClientTagConfigModelTests.cs`, `.../HistorianWonderwareTagConfigModelTests.cs` + +**Approach:** Read `ModbusTagConfigModel.cs` + its editor + `ModbusTagConfigModelTests.cs` as the exact template. The OpcUaClient tag-config carries the OPC UA `FullName`/NodeId + (poll) settings; the Historian tag-config carries the historian tagname/source. Inspect each driver's `TagConfig` shape (the OpcUaClient driver's equipment-tag ref + the Historian client options) for the real fields; keep **preserve-unknown-keys**. Register exact `DriverType` strings (`"OpcUaClient"`, `"Historian.Wonderware"` — confirm against `GalaxyDriverFactoryExtensions.DriverTypeName`-style constants / `DriverFactoryBootstrap`). + +**Steps:** (1) Write failing model tests (FromJson/ToJson round-trip + Validate + unknown-key preservation). (2) Run → fail. (3) Implement the 2 models + editors + register. (4) Tests green. (5) `dotnet build` the AdminUI project clean. (6) Commit `feat(adminui): typed TagConfig editors for OpcUaClient + Historian`. + +--- + +### Task 2 (B): `isHistorized` / `historianTagname` first-class fields + +**Classification:** standard · **Est:** ~5 min · **Parallelizable with:** Task 6, 7 · **blockedBy:** Task 1 + +**Files:** +- Modify: every `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Uns/TagEditors/*TagConfigModel.cs` (add `IsHistorized`/`HistorianTagname` round-trip on `FromJson`/`ToJson`, keys `isHistorized`/`historianTagname`) +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Uns/TagModal.razor` (a shared "Historize this tag" checkbox + optional "Historian tagname" textbox, driver-agnostic, above/below the typed-editor slot) +- Test: `tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/Uns/HistorizedFieldRoundTripTests.cs` + +**Approach:** The server already reads `isHistorized`/`historianTagname` from the raw `TagConfig` JSON (`docs/Historian.md`); this surfaces them as controls without changing the wire format. Decide ONE seam: either each typed model carries the two fields, OR the TagModal merges them into the JSON around the typed editor (simpler + uniform + covers the raw-JSON fallback drivers). **Prefer the TagModal-merge seam** so it works for ALL drivers incl. raw-JSON ones, and unit-test the merge helper. Confirm round-trip byte-parity (toggle → save → reopen shows the toggle; a `changed=1` deploy per the H1 work). + +**Steps:** TDD the merge/round-trip helper → implement → green → build → commit `feat(adminui): isHistorized + historianTagname as first-class Tag fields`. + +--- + +### Task 3 (C): Native-alarm `HistorizeToAveva` opt-out + +**Classification:** standard · **Est:** ~5 min · **Parallelizable with:** Task 1, 6, 7 + +**Files:** +- Modify: the native-alarm authoring surface — find it (grep `TagModal`/equipment Alarms tab for the native-alarm `alarm` object; native alarms ride `TagConfig.alarm`, NOT the `ScriptedAlarm` entity). Likely a new small alarm sub-model + a control in `TagModal.razor` (or the typed editors' alarm section). +- Verify: `src/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer/Phase7Composer.cs` `EquipmentTagAlarmInfo` — add/confirm `HistorizeToAveva` (`bool?`) parse from `TagConfig.alarm`, and that `HistorianAdapterActor` gates native-alarm durable writes on `is not false` (the scripted path already does — mirror it). +- Test: `tests/.../AdminUI.Tests/Uns/NativeAlarmHistorizeModelTests.cs` (+ a Phase7Composer parse test if the carrier changes) + +**Approach:** Add `historizeToAveva` (`bool?`, absent ⇒ historize) to the native-alarm `TagConfig.alarm` model + a "Historize to AVEVA" checkbox in the native-alarm form. Confirm the server honors it for native alarms (the scripted-alarm gate shipped in the script-log/alarm round-3 — find the `is not false` check in `HistorianAdapterActor` and ensure native transitions pass through the same gate). NO migration (rides `TagConfig`). TDD the model field; live `/run` the toggle. + +**Steps:** TDD → implement model + UI + server-honor confirmation → green → build → commit `feat(adminui): native-alarm HistorizeToAveva opt-out`. + +--- + +### Task 4 (D): Galaxy picker pre-fills alarm fields from `IsAlarm` + +**Classification:** small · **Est:** ~4 min · **Parallelizable with:** Task 5, 6, 7 · **blockedBy:** Task 3 + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Shared/Drivers/Pickers/GalaxyAddressPickerBody.razor` + its selection callback into `TagModal.razor:115` +- Test: none new (pure Razor wiring) — covered by live `/run` + +**Approach:** `DriverAttributeInfo.IsAlarm` already exists (`Core.Abstractions/DriverAttributeInfo.cs:55`). When the Galaxy picker returns a selection whose attribute `IsAlarm == true`, pre-fill the native-alarm sub-form (from Task 3) with sensible defaults (alarm type/severity) so the operator authors the alarm in one pass. Wiring only. Commit `feat(adminui): Galaxy picker pre-fills native-alarm fields from IsAlarm`. + +--- + +### Task 5 (E): "Build address" button in the protocol-driver typed editors + +**Classification:** small · **Est:** ~5 min · **Parallelizable with:** Task 6, 7 · **blockedBy:** Task 2 + +**Files:** +- Modify: the 6 typed editors `Components/Shared/Uns/TagEditors/{Modbus,S7,AbCip,AbLegacy,TwinCAT,Focas}TagConfigEditor.razor` — add a "Build address" button that opens the matching existing `Components/Shared/Drivers/Pickers/*AddressPickerBody.razor` inside the shared `DriverTagPicker` shell, writing the built address back to the editor's address field. +- Test: none new (the `*AddressBuilder` cores are already unit-tested under `AdminUI.Tests/Pickers/`) — live `/run` + +**Approach:** Reuse the EXACT pattern by which Galaxy is wired into `TagModal.razor:115` (``), but per typed editor. No new picker logic. Commit `feat(adminui): Build-address pickers in the protocol-driver Tag editors`. + +--- + +### Task 6 (F): UNS-tree Delete for Cluster / Enterprise + +**Classification:** standard · **Est:** ~6 min · **Parallelizable with:** Task 1, 3, 7 + +**Files:** +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/...` `IUnsTreeService` + `UnsTreeService` (add `DeleteClusterAsync(clusterId, rowVersion)`; add `RowVersion` to `ClusterRow` + query it in `LoadStructureAsync`) +- Modify: `Components/Shared/Uns/UnsTree.razor` (`RenderActions` → Delete action for Cluster), `Components/Pages/Uns/GlobalUns.razor` (`ConfirmDeleteAsync:306` Cluster/Enterprise branch) +- Test: `tests/.../AdminUI.Tests/Uns/UnsTreeServiceDeleteClusterTests.cs` (in-memory EF — delete empty cluster ok; refuse-or-cascade with children; RowVersion mismatch → concurrency error; Enterprise = delete all clusters under the label) + +**Approach:** Mirror the existing `DeleteAreaAsync`/`DeleteLineAsync` pattern. **Cluster cascade is the danger** (owns driver-instances + areas→…→tags) — decide refuse-if-children vs ordered cascade and TEST it; require `RowVersion` concurrency. Enterprise is a tree label (not a DB entity) ⇒ delete all clusters carrying that `Enterprise` value. Service unit-tested; tree action live `/run`. Commit `feat(adminui): UNS-tree delete for Cluster + Enterprise`. + +--- + +### Task 7 (G): Create-new-script from the inline virtual-tag panel + +**Classification:** standard · **Est:** ~5 min · **Parallelizable with:** Task 1, 3, 6 + +**Files:** +- Modify: `IUnsTreeService` + `UnsTreeService` (add `CreateScriptAsync(...)` → new `Script` row, generated id, blank source, returns the new id) +- Modify: `src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/.../VirtualTagModal.razor` (`:66` — a "New script" branch when `ScriptId` is empty → `CreateScriptAsync` → bind → expand the inline Monaco editor) +- Test: `tests/.../AdminUI.Tests/Uns/UnsTreeServiceCreateScriptTests.cs` (in-memory EF — creates a Script row with a unique id + blank source) + +**Approach:** `SetVirtualTag` completions/hover already work (no change). Add only the create-new-script flow. Service unit-tested; UX live `/run`. Commit `feat(adminui): create-new-script from the inline virtual-tag panel`. + +--- + +### Task 8: Docs + bookkeeping + +**Classification:** small · **Est:** ~4 min · **blockedBy:** Task 1–7 + +Update `docs/Uns.md` (typed editors for OpcUaClient/Historian, historized first-class fields, Build-address pickers, Cluster/Enterprise delete, create-new-script) + `docs/Historian.md` (isHistorized/historianTagname now first-class) + a native-alarm HistorizeToAveva note in `docs/ScriptedAlarms.md`/`docs/AlarmTracking.md`. Note Item 7 (Hosts page) deferred (F7-blocked). Commit `docs(phase6): AdminUI editors, pickers, deletes, new-script`. + +--- + +### Task 9: Full build + test + final integration review + +**Classification:** high-risk · **Est:** ~6 min · **blockedBy:** Task 1–8 + +`dotnet build ZB.MOM.WW.OtOpcUa.slnx` → 0 errors. `dotnet test tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests` → green (+ any touched OpcUaServer test for Task 3's composer parse). Final integration review: (a) preserve-unknown-keys holds across the historized-field + alarm additions; (b) Cluster delete cascade/concurrency is safe; (c) no EF migration / no Commons contract change leaked; (d) the native-alarm historize gate matches the scripted path. Fix findings, commit. + +--- + +### Task 10: Live `/run` acceptance (agent-driven) + +**Classification:** high-risk · **Est:** ~10 min · **blockedBy:** Task 9 + +Bring up the local rig: `docker compose -f docker-dev/docker-compose.yml up -d central-1` (+ the AdminUI service; login disabled). Drive `http://localhost:9200` via the Chrome browser tools: author an OpcUaClient + a Historian tag via the new typed editors; toggle Historize + set a historian tagname → deploy → confirm `changed=1` + round-trip on reopen; toggle a native-alarm AVEVA opt-out; open a protocol-driver "Build address" picker and use a built address; create + delete a disposable Cluster; create-new-script from the vtag panel. **Phase-5 lesson:** the Bash sandbox blocks some network — if a check can't reach a service, retry with `dangerouslyDisableSandbox: true`. Record results; honestly defer anything the local rig can't exercise. + +--- + +## Done = +Build clean + `AdminUI.Tests` green + final integration review SHIP + live `/run` acceptance pass. Then `finishing-a-development-branch` → merge to master + push (the user's standing directive). Update `pending.md` banner + the `project_stillpending_backlog` memory. diff --git a/docs/plans/2026-06-16-stillpending-phase-6-adminui.md.tasks.json b/docs/plans/2026-06-16-stillpending-phase-6-adminui.md.tasks.json new file mode 100644 index 00000000..9773ba40 --- /dev/null +++ b/docs/plans/2026-06-16-stillpending-phase-6-adminui.md.tasks.json @@ -0,0 +1,20 @@ +{ + "planPath": "docs/plans/2026-06-16-stillpending-phase-6-adminui.md", + "branch": "feat/stillpending-phase-6-adminui", + "baseMaster": "156aa900", + "designCommit": "dbff29dd", + "executionState": "PLANNED — not started (deferred to after /compact)", + "tasks": [ + {"id": 452, "subject": "P6 Task 1 (A): Typed editors for OpcUaClient + Historian", "status": "pending"}, + {"id": 453, "subject": "P6 Task 2 (B): isHistorized + historianTagname first-class fields", "status": "pending", "blockedBy": [452]}, + {"id": 454, "subject": "P6 Task 3 (C): Native-alarm HistorizeToAveva opt-out", "status": "pending"}, + {"id": 455, "subject": "P6 Task 4 (D): Galaxy picker pre-fills alarm fields from IsAlarm", "status": "pending", "blockedBy": [454]}, + {"id": 456, "subject": "P6 Task 5 (E): Build-address pickers in protocol-driver Tag editors", "status": "pending", "blockedBy": [453]}, + {"id": 457, "subject": "P6 Task 6 (F): UNS-tree delete for Cluster + Enterprise", "status": "pending"}, + {"id": 458, "subject": "P6 Task 7 (G): Create-new-script from the inline vtag panel", "status": "pending"}, + {"id": 459, "subject": "P6 Task 8: Docs + bookkeeping", "status": "pending", "blockedBy": [452, 453, 454, 455, 456, 457, 458]}, + {"id": 460, "subject": "P6 Task 9: Full build + test + final integration review", "status": "pending", "blockedBy": [452, 453, 454, 455, 456, 457, 458, 459]}, + {"id": 461, "subject": "P6 Task 10: Live /run acceptance (agent-driven)", "status": "pending", "blockedBy": [460]} + ], + "lastUpdated": "2026-06-16" +}