# Admin UI rebuild plan (F15) **Status:** UX kickoff — proposals to react to before any per-page rebuild starts. **Last updated:** 2026-05-26 on `v2-akka-fuse`. ## Why this isn't a straight port The v1 Admin UI was built around `ConfigGeneration` draft → publish: operators edited a **draft** generation, the system computed a **diff** against the last published one, and a manual **Publish** sealed it. Six full pages (`DraftEditor`, `DiffViewer`, `DiffSection`, `Generations`, plus the per-tab "viewing draft N" header) lived to make this workflow legible. v2 replaces that with **live-edit + snapshot-deploy** (decisions #14a–#14e on this branch). Edits write directly to live tables guarded by `RowVersion` concurrency; deploying is a single click that snapshots the current live state and dispatches it via Akka. Drift between "current live" and "last sealed deployment" surfaces as a one-line indicator on the [Deployments](../../src/Server/ZB.MOM.WW.OtOpcUa.AdminUI/Components/Pages/Deployments.razor) page. That collapses **six pages → zero** before we ship a line of new Razor. The remaining ~41 legacy pages map to ~30 v2 pages once redundant fleet-wide views fold into their cluster-tab equivalents. ## Inventory: 47 legacy pages → v2 disposition Source: `git show 76310b8^ -- 'src/Server/ZB.MOM.WW.OtOpcUa.Admin/**/*.razor'`. ### Site shell (5 files) — port | Legacy | v2 status | Notes | |---|---|---| | `App.razor`, `Routes.razor`, `_Imports.razor` | Port | Boilerplate; minor render-mode tweaks | | `Layout/MainLayout.razor` | ✅ Already in v2 | Done in Task 48 | | `Components/Pages/Login.razor`, `Account.razor` | Port | Auth endpoints changed (cookie+JWT hybrid, Task 26); login form posts to `/auth/login` now | ### Shared widgets (5 files) — port | Legacy | v2 status | |---|---| | `StatusBadge.razor` | ✅ Already in v2 | | `LoadingSpinner.razor` | ✅ Already in v2 | | `ToastNotification.razor` | ✅ Already in v2 | | `ClusterAuthorizeView.razor`, `RedirectToLogin.razor` | Port — adjust for v2 `IUserAuthenticator` | ### Fleet (1 file) — reshape | Legacy | v2 strategy | |---|---| | `Fleet.razor` | **Reshape.** Drop the v1 "poller reads central DB" data source. v2 reads `NodeDeploymentState` (Applied / Failed / Stale per node) + subscribes to `FleetStatusHub` for live `ServiceLevel` updates (already wired in F16) + queries `IFleetDiagnosticsClient.GetDiagnostics` (F17) for per-node driver health. Single page, similar shape to v1. | ### Cluster CRUD (3 files) — port | Legacy | v2 strategy | |---|---| | `ClustersList.razor` | Port | | `NewCluster.razor` | Port | | `ClusterDetail.razor` | **Port — drop draft/publish chrome.** No "New draft" button; no "current published" sidebar. Replace with "Last deployed" badge + a "Deploy" button (already a SignalR-aware widget on the Deployments page; this becomes a cluster-scoped variant). | ### Draft/publish workflow (4 files) — **drop entirely** | Legacy | v2 strategy | |---|---| | `DraftEditor.razor` | **Drop.** No drafts in v2. | | `DiffViewer.razor` | **Drop.** Drift indicator replaces it on Deployments page. | | `DiffSection.razor` | **Drop.** | | `Generations.razor` | **Drop — replaced by `Deployments.razor`** (already shipped in v2 ahead of F15). | ### Cluster tabs (11 files) — port as live-edit forms Each becomes a live-edit surface: load the entity, bind to a form, save with `RowVersion` concurrency check (409 on conflict → toast + reload). No "viewing draft N" header; no per-tab snapshot view. | Legacy tab | v2 strategy | |---|---| | `EquipmentTab.razor` | Port — UNS path tree picker stays | | `UnsTab.razor` | Port — same | | `NamespacesTab.razor` | Port | | `DriversTab.razor` | Port — **driver-type-specific editors are a separate question (see below)** | | `TagsTab.razor` | Port | | `AclsTab.razor` | Port — wire to v2 LDAP group → role mapping (see RoleGrants question) | | `RedundancyTab.razor` | Port — surface v2 `ServiceLevel` calc (Task 35) instead of v1 redundancy state machine | | `ScriptedAlarmsTab.razor` | Port | | `ScriptsTab.razor` | Port | | `VirtualTagsTab.razor` | Port | | `AuditTab.razor` | Port — wire to v2 `ConfigAuditLog` (post-F3 schema: `EventId`, `CorrelationId` columns) | ### Cluster-scoped editors (3 files) — port as reusable inputs | Legacy | v2 strategy | |---|---| | `IdentificationFields.razor` | Port | | `ImportEquipment.razor` | Port | | `ScriptEditor.razor` | Port | ### Cross-cluster pages (8 files) — mixed | Legacy | v2 strategy | |---|---| | `Hosts.razor` | Port — reshape to "Akka cluster members" (showing `host:port` NodeIds, roles, redundancy state) | | `Certificates.razor` | Port — F13a's `PkiStoreRoot` becomes the data source | | `Reservations.razor` | Port | | `RoleGrants.razor` | **Reshape.** v1 was cluster-scoped role grants; v2 uses LDAP group → role mapping (see Q4 below) | | `AlarmsHistorian.razor` | Port — wire to F11's `HistorianAdapterActor.GetStatus` (queue depth + drain state) | | `ScriptLog.razor` | Port — needs SignalR hub bridge (F16 deferred ScriptLogHub) | | `ScriptedAlarms.razor` (top-level) | **Possibly drop** (see Q2 below) | | `VirtualTags.razor` (top-level) | **Possibly drop** (see Q2 below) | ### Driver-typed editors (5 files) — sequencing decision needed | Legacy | v2 strategy | |---|---| | `Drivers/FocasDetail.razor` | Defer — JSON editor in `DriversTab` covers the same config initially | | `Modbus/ModbusOptionsEditor.razor` | Same | | `Modbus/ModbusAddressEditor.razor` | Same | | `Modbus/ModbusAddressPreview.razor` | Same | | `Modbus/ModbusDiagnostics.razor` | Port — separate from the config editor, this is operational telemetry | ### Account (1 file) — port | Legacy | v2 strategy | |---|---| | `Account.razor` | Port — minor reshape for JWT (token expiry UI, refresh button) | ## Summary by disposition | Disposition | Count | |---|---| | Already in v2 | 5 | | Port as-is | 22 | | Port + reshape | 7 | | **Drop (replaced by live-edit / Deployments page)** | **5** | | Drop (redundant with cluster tab) | 2 (pending Q2) | | Defer (driver-typed editors) | 4 | | **Total active rebuild** | ~30 pages | ## Open design questions These need answers before per-page sequencing starts. They drive how many phases the rebuild takes and what gets cut. ### Q1 — Driver-typed editors: ship now or defer? **Context.** v1 had typed editors for Modbus + FOCAS driver config. They sat behind a generic JSON editor for the other six driver types. The typed editors caught operator typos that the JSON editor missed (port ranges, slave-ID collisions, address-map overlaps). **Options.** - **Defer all typed editors.** Ship `DriversTab` with a JSON editor first; add typed editors per-driver as field requests come in. Saves ~1 day on F15. - **Port the existing two.** Modbus + FOCAS were already validated against field use. The other six driver types stay JSON-only. - **Ship all eight typed editors.** Most work, best UX. ~3 extra days on F15. **Recommendation:** Defer. The OPC UA dual-endpoint tests + driver engine wiring (F7-F10) are higher-leverage and need attention first. ### Q2 — Top-level `ScriptedAlarms.razor` and `VirtualTags.razor`: keep or drop? **Context.** In v1, these were fleet-wide views of every scripted alarm and virtual tag across every cluster. The cluster tabs let you edit them; the top-level pages let you find them across clusters. **Options.** - **Drop.** Fleet-wide view is rare; cluster scope covers 95% of use. - **Keep as read-only.** Cross-cluster search + drill-down to the per-cluster tab. **Recommendation:** Drop, but expose a global search on the top nav that matches cluster + alarm/tag names if operators ask. ### Q3 — ClusterDetail: 10 tabs or split routes? **Context.** v1 had 10 nav-tabs inside `ClusterDetail.razor`. Some are very heavy (Tags can be 10k rows; AuditTab streams). All 10 share render state. **Options.** - **Keep tabs.** Familiar; one URL per cluster. - **Split into routes.** `/clusters/{id}/equipment`, `/clusters/{id}/tags`, etc. Better deep-linking, better load (one tab's data per page), easier auth scoping. **Recommendation:** Split into routes. The v1 monolith was already groaning under the live-update SignalR fan-in; routes let each surface manage its own subscription lifecycle. ### Q4 — RoleGrants: cluster-scoped table or LDAP group → role map? **Context.** v1 had a per-cluster `RoleGrants` table where you mapped users to cluster-scoped roles (ClusterAdmin, ClusterOperator, etc.). v2 introduced LDAP-driven auth: LDAP group membership maps to OPC UA permissions (`ReadOnly`, `WriteOperate`, `WriteTune`, `WriteConfigure`, `AlarmAck`) fleet-wide. **Options.** - **Keep v1 model.** Cluster-scoped grants survive; LDAP just provides the username. - **Replace with fleet-wide LDAP-group → role mapping.** v2's `LdapOptions` already has a `GroupToRole` dictionary; surface that in a single fleet-level page. - **Both.** LDAP map for fleet-wide defaults; per-cluster overrides for scoping. **Recommendation:** Fleet-wide LDAP-group → role map only. Per-cluster scoping adds combinatorial complexity that v2's redundancy model doesn't need (every driver-role node runs every driver in the fleet). ### Q5 — Login UI: backed by `/auth/login` (cookie+JWT hybrid) — what about LDAP error UX? **Context.** v2's `/auth/login` does an LDAP bind. Failures come back as specific reasons (invalid creds vs. service-account misconfig vs. server unreachable). The default behavior is to lump them all into "Login failed." **Options.** - **Generic "Login failed."** Safer; doesn't leak whether the username exists. - **Specific error categories.** Helps operators diagnose deploy issues. **Recommendation:** Generic for production deployments, specific when `Authentication:Ldap:AllowInsecureLdap=true` (dev mode signal). ## Proposed sequencing (4 phases) Each phase is independently mergeable. The branch ships when Phase A is in; Phases B–D can follow as smaller PRs. ### Phase A — Shell + auth + fleet (minimum-viable Admin) ~½–1 day. Ships a working admin surface with no config editing. - Port `App.razor`, `Routes.razor`, `_Imports.razor` - Port `Login.razor` (post Q5) - Port `Account.razor` - Reshape `Fleet.razor` against v2 data sources - Port `Hosts.razor` reshape ### Phase B — Cluster CRUD + Overview/Redundancy tabs ~1 day. Adds cluster browse + readonly redundancy view. - Port `ClustersList`, `NewCluster`, `ClusterDetail` (Overview tab only) - Port `RedundancyTab` (read-only — surfaces v2 `ServiceLevel`) - Split into routes if Q3 = split ### Phase C — Config editor tabs ~2 days. The big chunk — the live-edit config surface. - `EquipmentTab`, `UnsTab`, `NamespacesTab` - `DriversTab` (JSON-only initially per Q1) - `TagsTab` - `AclsTab` post Q4 reshape - `ImportEquipment`, `IdentificationFields` ### Phase D — Logic + ops pages ~1 day. - `VirtualTagsTab`, `ScriptedAlarmsTab`, `ScriptsTab`, `ScriptEditor` - `AuditTab` against new ConfigAuditLog schema - `RoleGrants` post Q4 reshape - `Certificates` - `Reservations` - `AlarmsHistorian`, `ScriptLog` (depends on F16 ScriptLogHub deferred) ## Out of scope for F15 - Typed driver editors (Q1, deferred unless reversed) - Top-level fleet-wide ScriptedAlarms / VirtualTags pages (Q2, recommended drop) - Per-cluster RoleGrants (Q4, recommended drop) - ScriptLogHub SignalR bridge (F16 deferred — only needed for Phase D's ScriptLog page; can move to a separate F16-extension follow-up)