47-page legacy inventory mapped to v2 disposition (5 already done, 22 port as-is, 7 reshape, 5 dropped because live-edit replaces draft/publish, 4 deferred driver-typed editors). Net ~30 active pages to rebuild. Five open design questions surfaced for review before per-page work starts: Q1 driver-typed editors (defer vs. ship), Q2 top-level fleet-wide views (drop vs. keep), Q3 ClusterDetail tabs vs. split routes, Q4 RoleGrants cluster-scoped vs. LDAP-group fleet-wide, Q5 Login error UX. Proposed 4-phase sequencing (~5 days total): shell+auth+fleet, cluster CRUD, config tabs, logic+ops. Each phase independently mergeable.
11 KiB
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
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
DriversTabwith 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
LdapOptionsalready has aGroupToRoledictionary; 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.razoragainst v2 data sources - Port
Hosts.razorreshape
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 v2ServiceLevel) - Split into routes if Q3 = split
Phase C — Config editor tabs
~2 days. The big chunk — the live-edit config surface.
EquipmentTab,UnsTab,NamespacesTabDriversTab(JSON-only initially per Q1)TagsTabAclsTabpost Q4 reshapeImportEquipment,IdentificationFields
Phase D — Logic + ops pages
~1 day.
VirtualTagsTab,ScriptedAlarmsTab,ScriptsTab,ScriptEditorAuditTabagainst new ConfigAuditLog schemaRoleGrantspost Q4 reshapeCertificatesReservationsAlarmsHistorian,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)