e08855fb9d
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.
271 lines
12 KiB
Markdown
271 lines
12 KiB
Markdown
# 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)
|