docs(phase6): implementation plan + task persistence (10 tasks, execution deferred to post-compaction)
This commit is contained in:
@@ -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` (`<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 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.
|
||||
@@ -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"
|
||||
}
|
||||
Reference in New Issue
Block a user