# 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](../../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)
