Files
lmxopcua/docs/v2/AdminUI-rebuild-plan.md
T
Joseph Doherty e08855fb9d
v2-ci / build (push) Successful in 4m52s
v2-ci / unit-tests (push) Failing after 15m58s
docs: source-verified deferment register + correct 17 drifted docs
Adds deferment.md — a source-verified inventory of new-driver status, all 17
open issues, in-code deferrals, open live gates and plan bookkeeping. Every
claim was checked against src/ and git, not against documentation.

Headline finding: three subsystems are authored, persisted and shipped but
never executed —
  * Node ACLs: IPermissionEvaluator/TriePermissionEvaluator have zero
    production consumers and OtOpcUaNodeManager never references them, yet
    ClusterAcls.razor authors NodeAcl rows and ConfigComposer.cs:51 ships them
    in every artifact. An authored deny rule has no effect.
  * IRediscoverable / IHostConnectivityProbe raise into the void (#518/#507);
    nothing ever writes a DriverHostStatus row.
  * DriverTypeRegistry is vestigial, so no factory passes a tier and every
    driver runs Tier A with the Tier-C protections dormant.

Also records the Calculation driver as picker-visible but unauthorable
(DriverConfigModal has no case) — the "registered but unauthorable" class
recurring after the Sql picker defect — and notes that no parity test guards
DriverConfigModal/DeviceModal, which is why it survived review.

Documentation corrections (source-verified):
  * ReadWriteOperations.md claimed "a denied read never hits the driver" via
    four types that do not exist in src/. Bannered + struck through; a security
    review reading that page would have concluded a per-node ACL gate exists.
  * CLAUDE.md: the Change Detection sentence was false (DriverHost consumes
    nothing); the mesh Phase 4 and Phase 5 live gates and the auto-down 1-vs-1
    gate had all PASSED; the ScriptedAlarmState table was already dropped.
  * driver-expansion tracking: Modbus RTU and SQL poll are merged, not
    pending — its command table would have made someone rebuild two shipped
    drivers in a fresh worktree.
  * drivers/README.md: dead DriverTypeRegistry paragraph, retired
    SystemPlatform namespace kind, missing Sql + Calculation rows, missing
    Modbus RTU-over-TCP transport.
  * TwinCAT.md/Galaxy.md promised an address-space rebuild that never happens.
  * Historian.md gained the #491 unproven-value-capture pointer.
  * IncrementalSync.md and AddressSpace.md bannered as wholesale v2-era (the
    rewrite is recorded as still owed, not done here); four v2 status docs
    bannered as historical; three wrong-name-for-a-live-type fixes.

Left deliberately untouched: the secrets NoOpSecretReplicator line, which
makes a security claim and needs the real behaviour identified rather than a
rename.
2026-07-27 17:26:20 -04:00

12 KiB
Raw Blame History

Admin UI rebuild plan (F15)

⚠️ Historical (audited 2026-07-27) — describes the v2 AdminUI. Several named types (AuthorizationPolicies, IdentificationFields) no longer exist, and the v3 rebuild replaced the per-cluster UNS/Equipment/Tags tabs with the global /uns page and the /raw project tree. Kept for the record.

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)