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

124 lines
9.2 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.
# 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 05 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 78.
## 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.