Files
lmxopcua/docs/plans/2026-06-16-stillpending-phase-6-adminui.md
T

145 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` (`<DriverTagPicker @bind-Visible=…><XAddressPickerBody …/></DriverTagPicker>`), 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 17
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 18
`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.