Files
lmxopcua/docs/v2/AdminUI-rebuild-plan.md
Joseph Doherty 5c754ecffd
Some checks failed
v2-ci / build (push) Failing after 41s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (push) Has been skipped
docs(v2): F15 UX kickoff — AdminUI rebuild plan
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.
2026-05-26 07:38:58 -04:00

11 KiB
Raw Blame History

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 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 BD 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)