diff --git a/docs/plans/2026-06-16-stillpending-phase-6-adminui-design.md b/docs/plans/2026-06-16-stillpending-phase-6-adminui-design.md new file mode 100644 index 00000000..a7a7e404 --- /dev/null +++ b/docs/plans/2026-06-16-stillpending-phase-6-adminui-design.md @@ -0,0 +1,123 @@ +# Still-Pending Phase 6 — AdminUI typed editors, pickers & UX — design + +> **Status:** approved 2026-06-16 (scope = "Everything achievable", AskUserQuestion). Parent roadmap: +> `docs/plans/2026-06-15-stillpending-backlog-design.md` (Phase 6). Branch `feat/stillpending-phase-6-adminui` +> off master `156aa900`. Phases 0–5 already shipped. **Execution is deferred to AFTER a /compact** — this doc + +> the implementation plan are the durable handoff. + +## Goal + +Close the AdminUI authoring/UX gaps in `stillpending.md` §4 so the `/uns` Tag authoring experience is complete: +typed editors for every driver, historized fields as first-class controls, native-alarm AVEVA opt-out, the +existing address pickers reachable from the Tag modal, UNS-tree deletes for the top node kinds, and +create-new-script from the inline virtual-tag panel. **NO EF migration; Razor proven only by live `/run` (no +bUnit) — the pure-C# `*TagConfigModel` / service / builder cores ARE unit-tested in +`tests/Server/ZB.MOM.WW.OtOpcUa.AdminUI.Tests/`.** + +## Grounding that reshaped the scope (the roadmap was materially stale) + +1. **`DriverAttributeInfo.IsAlarm` already exists** (`Core.Abstractions/DriverAttributeInfo.cs:55`, plus + `IsArray`/`ArrayDim`). Item 4 needs **no Commons contract change** — only the picker→modal pre-fill wiring. +2. **All 9 driver address-picker bodies + builders already exist** under + `Components/Shared/Drivers/Pickers/` (`{Modbus,S7,AbCip,AbLegacy,TwinCAT,FOCAS,Galaxy,OpcUaClient, + HistorianWonderware}AddressPickerBody.razor` + the `*AddressBuilder.cs` with unit tests under + `tests/.../AdminUI.Tests/Pickers/`). They are **wired into the per-cluster driver pages** + (`ModbusDriverPage.razor` …). Only the **Galaxy** picker is also wired into the `/uns` `TagModal.razor` + (`:115`). So the roadmap's "only OpcUaClient + Galaxy pickers shipped" is **stale**; Item 5's residue is + just wiring a "Build address" affordance into the typed editors in the Tag modal. +3. **`ctx.SetVirtualTag(...)` completions/hover are already wired** — `SetVirtualTag` is already in + `ScriptAnalysisService.TryGetTagPathLiteral`. Item 8's residue is only **create-new-script** from the + inline panel. +4. **`HistorizeToAveva` is a first-class field on *scripted* alarms** (`ScriptedAlarm` entity + + `Phase7Composer.ScriptedAlarmInfo`; the ScriptedAlarmModal toggle already ships). Item 3's residue is the + **native-alarm** side: the `TagConfig.alarm` object's AVEVA opt-out + its UI. +5. **No bUnit anywhere** — the AdminUI test project (`AdminUI.Tests`) is pure C# (model `FromJson/ToJson/ + Validate` tests, `UnsTreeService*` against in-memory EF, `ScriptAnalysis/*`, `Pickers/*AddressBuilder`). + +## In scope — the genuinely-remaining work (Item 7 deferred) + +### A. Typed editors for OpcUaClient + Historian.Wonderware (Item 1) — `standard` +- `TagConfigEditorMap` (`Uns/TagEditors/TagConfigEditorMap.cs`) maps only ModbusTcp/S7/AbCip/AbLegacy/TwinCat/ + Focas → a typed editor; OpcUaClient/Galaxy/Historian.Wonderware fall back to raw JSON. +- Add `OpcUaClientTagConfigModel` + `HistorianWonderwareTagConfigModel` (pure `FromJson`/`ToJson`/`Validate`, + preserve-unknown-keys, camelCase) + their razor editor shells under `Components/Shared/Uns/TagEditors/`, + register both in `TagConfigEditorMap` + `TagConfigValidator`. Mirror the Modbus template. +- **Unit-test the two models** (mirror `ModbusTagConfigModelTests`); editors proven by live `/run`. + +### B. `isHistorized` / `historianTagname` as first-class fields (Item 2) — `standard` +- Today they ride the raw `TagConfig` JSON (the server already reads them — `docs/Historian.md`). +- Surface a shared **"Historize this tag" checkbox + optional "Historian tagname" textbox** in the Tag modal, + driver-agnostic (applies to any equipment tag). Thread them through the typed-editor models' `FromJson`/ + `ToJson` (and the raw-JSON fallback) so a toggle round-trips byte-parity and is preserved across edits. +- **Unit-test** the field round-trip on the models. + +### C. Native-alarm `HistorizeToAveva` opt-out (Item 3) — `standard` +- The native-alarm carrier is `TagConfig.alarm` (`Phase7Composer.EquipmentTagAlarmInfo`). Add a + `historizeToAveva` (`bool?`, default-historize when absent — mirror the scripted-alarm wire contract) to the + native-alarm authoring surface + the alarm sub-model, and **confirm the server honors it for native alarms** + in `HistorianAdapterActor` (the scripted path already gates on `is not false`). NO migration (rides + `TagConfig`). Unit-test the model field; live `/run` the toggle. + +### D. Galaxy picker pre-fills alarm fields from `IsAlarm` (Item 4) — `small` +- `DriverAttributeInfo.IsAlarm` already exists. When the Galaxy address picker (wired in `TagModal.razor:115`) + returns a selection whose attribute `IsAlarm == true`, pre-fill the native-alarm sub-form (from C) with + sensible defaults so the operator can author the alarm in one pass. Wiring only; live `/run`. + +### E. Address pickers reachable from the typed editors (Item 5) — `small` +- The picker bodies + builders exist and are wired on the driver pages. Add a **"Build address"** button to each + protocol-driver typed editor (Modbus/S7/AbCip/AbLegacy/TwinCAT/Focas) in the Tag modal that opens the + matching `*AddressPickerBody` inside the shared `DriverTagPicker` shell (exactly as Galaxy is wired at + `TagModal.razor:115`), writing the built address back into the editor's address field. Reuses existing bodies + — no new picker logic. Live `/run`. + +### F. UNS-tree Delete for Cluster / Enterprise (Item 6) — `standard` +- `GlobalUns.ConfirmDeleteAsync` (`:306`) guards Enterprise/Cluster as "not yet available"; `UnsTree`'s + `RenderActions` hides Delete for those kinds. Add `IUnsTreeService.DeleteClusterAsync(clusterId, rowVersion)` + (+ `ClusterRow.RowVersion`, queried in `LoadStructureAsync`), the `UnsTree` Delete action for Cluster, and the + `GlobalUns` branch. **Enterprise** = delete all clusters under that label (Enterprise is a tree label, not a + DB entity). **Cluster cascade is the danger** (it owns driver-instances + areas→lines→equipment→tags) — match + the area/line pattern (refuse-if-children OR ordered cascade) and require RowVersion concurrency. **Service- + layer unit-tested** against in-memory EF (mirror `UnsTreeService*` tests); the tree action is live `/run`. + +### G. Create-new-script from the inline virtual-tag panel (Item 8) — `small` +- `VirtualTagModal.razor` (`:66`) shows the inline Monaco editor only for an **existing** bound script. Add a + **"New script"** branch (when `ScriptId` is empty) → `IUnsTreeService.CreateScriptAsync(...)` (new Script row, + generated id, blank source) → bind it → expand the inline editor. **Service-layer unit-tested**; UX live `/run`. + +## Out of scope / deferred + +- **Item 7 — Hosts page per-driver-instance rows: DEFERRED** (`Hosts.razor` is per-Akka-member; its own comment + says driver-instance child rows "land with F7", which is not yet implemented → blocked on runtime work, not an + AdminUI gap). Recorded as a Phase-6b/F7 follow-up. +- Phase-4b driver structural work; Phases 7–8. + +## Architecture / risk notes + +- **Independence:** A+B are coupled (same model/editor files — do as one pass). C+D are coupled (D pre-fills C's + form). E, F, G are independent. F + G are the service-layer (in-memory-EF-testable) items; A,B,C are + model-layer testable; D,E are pure Razor wiring (live-`/run` only). +- **No EF migration, no Commons contract change** (IsAlarm/IsArray already on `DriverAttributeInfo`; historized + + native-alarm-historize ride `TagConfig` JSON; `ScriptedAlarm.HistorizeToAveva` column already exists). +- The `*TagConfigModel` preserve-unknown-keys contract (per `docs/plans/2026-06-09-driver-typed-tag-editors- + design.md`) MUST hold so adding historized fields never drops a driver's existing keys. + +## Testing & verification + +- TDD red→green (xUnit + Shouldly) for every pure core: the 2 new `*TagConfigModel`s (A), the historized-field + round-trip (B), the native-alarm-historize model field (C), `DeleteClusterAsync` (F), `CreateScriptAsync` (G) + — all in `AdminUI.Tests` (model tests + `UnsTreeService` in-memory-EF tests). **No bUnit.** +- `dotnet build` clean + `dotnet test` green before merge. +- **Live `/run` (agent-driven, login disabled on the local rig):** drive `http://localhost:9200` — author an + OpcUaClient/Historian tag via the new typed editor; toggle Historize + set a historian tagname and confirm + round-trip on re-open + deploy; toggle native-alarm AVEVA opt-out; open a protocol-driver "Build address" + picker and use a built address; delete a (disposable) Cluster; create-new-script from the vtag panel. + **NOTE (Phase-5 lesson): `dotnet test` of the integration suite is network-sandboxed — run live checks against + the docker-dev rig accordingly; bring up the rig with `docker compose -f docker-dev/docker-compose.yml up -d + central-1`.** Final integration review before merge. + +## Hard constraints (carried from the parent roadmap) + +- **NO Configuration entity / EF migration.** No Commons contract change. 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`. Never echo or commit secrets. No force-push, no + `--no-verify`. Finish = merge to master + push.