# 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.