Foundation task of the AdminUI UI sweep. CSS/link/doc only — no behavior change
beyond restoring styles that were built but never served.
- App.razor: link the RCL's scoped-CSS bundle
(_content/ZB.MOM.WW.OtOpcUa.AdminUI/ZB.MOM.WW.OtOpcUa.AdminUI.bundle.scp.css).
It is produced on every compile and served by the Host, but nothing referenced
it, so ContextMenu.razor.css was dead at runtime and context menus rendered
in-flow with no position:fixed.
- site.css: three additions.
* .btn / .btn-sm sizing as Bootstrap CSS-VARIABLE overrides (never box
properties — .btn-group depends on Bootstrap's own .btn rules for its seams
and end-cap radii). Theme 0.3.1 ships no .btn rule, so Bootstrap's 1rem
applied against the theme's 0.9rem body. Upstreaming candidate, same as the
ignitionoee router's app.css.
* .notice-alert — red variant of the kit's amber .notice, on the --bad/
--bad-bg/--bad-border tokens. The 13 existing call sites hack this with
style="border-color:var(--alert)", and no --alert token exists anywhere, so
the tint silently no-ops today. Call-site migration is a separate task.
* .tree-twist — shared 18px expander column for the tree components, replacing
repeated inline style="width:18px". Rows are d-flex, so flex-basis is what
actually holds the column. Adoption is a separate task.
- ContextMenu.razor.css: correct the header comment — the kit is light-only
(no dark block in Theme 0.3.1, site.css, or this file). No rule changes.
- docs/Telemetry.md: record that driver-health is keyed per driver instance
across the cluster, not per Akka member (the wire record carries no per-member
identity). The fact previously lived only in Hosts.razor page copy.
- docs/Configuration.md: new audit-log subsection recording that EventId gives
cross-restart dedup, enforced by the filtered unique index
UX_ConfigAuditLog_EventId, so a retried apply inserts no duplicate row;
CorrelationId joins a row to its originating workflow. Previously only in the
historical v2 rebuild plan and ClusterAudit page copy.
The Secrets.Ui RCL's routable page was routed directly, but this host's
router is deliberately static (cookie SignInAsync needs SSR) with per-page
@rendermode opt-in - a directive the RCL page cannot carry, because the
other three family hosts render it under a globally interactive router
where a nested render mode throws. Routing straight to the RCL page
therefore rendered /admin/secrets with no circuit: it displayed, but every
@onclick was silently dead.
Fix: a host-side wrapper page (Pages/SecretsAdmin.razor) now owns
/admin/secrets, carries the standard per-page InteractiveServer render
mode, re-states the RCL page's own authorization policy (the router only
enforces [Authorize] on the routed component, which is now the wrapper),
and renders the RCL page as an ordinary child. The RCL assembly is
de-registered from both AdditionalAssemblies sites (router + endpoint) so
the route is unambiguous.
Guards: SecretsPageWiringTests pins the render mode (the #483 regression),
route parity with the RCL page, and policy parity; the page census in
PageAuthorizationGuardTests classifies the new page and admits the
secrets:manage policy. AdminUI.Tests 665/665.
Live-verified on the rebuilt docker-dev rig with Playwright: /_blazor
circuit negotiated on /admin/secrets and Add secret opens the editor
(before the fix: zero interactive markers, no circuit, dead click).
Closes#483.
Claude-Session: https://claude.ai/code/session_01BL2Vu1ESDQ9SCN4gVKkdts
Mount the shared ZB.MOM.WW.Secrets.Ui RCL page at /admin/secrets on admin nodes:
- Register secrets:manage/secrets:reveal policies additively via
Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization()) in AddAdminUI
(the admin-only composition layer that already references the RCL — avoids forcing
an RCL dependency into the core Security lib; mirrors HistorianGateway)
- Register the RCL routable assembly in BOTH the SSR endpoint (AddAdditionalAssemblies)
and the interactive Router (App.razor AdditionalAssemblies) or the route 404s
- Add a Secrets nav item; the page's own [Authorize(Policy=...)] gates access
Claim-type MATCH: AdminRole=Administrator reads the same ClaimTypes.Role as FleetAdmin,
so existing Administrators are authorized with no new role mapping. Full clean boot
verified; interactive reveal deferred to the live gate (shared UI already proven).
Port the ScadaLink CentralUI sidebar pattern into the OtOpcUa AdminUI:
- Drop the top app-bar. Brand moves into the side rail's header — same
visual rhythm as ScadaLink's NavMenu.
- New NavSection.razor: collapsible eyebrow toggle (rail-eyebrow-toggle CSS)
with a chevron + label. Mirrors ScadaLink/Components/Layout/NavSection.
- New NavSidebar.razor: interactive island carrying the three section
groups (Navigation / Scripting / Live) + session block. Marked
@rendermode InteractiveServer; MainLayout itself stays static-rendered
because layouts can't take a RenderFragment Body across an interactive
boundary.
- New wwwroot/js/nav-state.js: window.navState.get/.set persists the
expanded-section list to the otopcua_nav cookie (one-year lifetime,
SameSite=Lax). Same shape as ScadaLink's scadabridge_nav.
- New LoginLayout.razor + @layout LoginLayout on Login.razor: the login
page now renders without the side rail — clean centred card.
- MainLayout.razor: slimmed down to the d-flex shell + hamburger toggle +
<NavSidebar/> + @Body.
- Login.razor: also drops the trailing "LDAP bind against the configured
directory..." footer that the user asked to remove.
- site.css: adds .side-rail .brand styles (mirrored from ScadaLink) and
the .rail-eyebrow-toggle / .rail-eyebrow-chevron / .rail-section-body
styles for the new collapsible UI.
Auto-expand on page load: NavSidebar seeds the expanded set from the
current URL's first path segment (in OnInitialized so it works even on
the very first server render) and from the cookie (in OnAfterRenderAsync
once JS interop is available). LocationChanged hooks keep the expanded
state in sync as the user navigates between sections.
Implements Phase A of the F15 rebuild plan: minimum-viable Admin surface
with a working sign-in path and a fleet-state landing page. Decisions Q1–Q5
of docs/v2/AdminUI-rebuild-plan.md were taken as recommended.
- App.razor (moved into AdminUI library from the Host stub; vendored
Bootstrap from RCL wwwroot — no public CDN, air-gap safe)
- Routes.razor (AuthorizeRouteView enforces page-level [Authorize])
- RedirectToLogin.razor (preserves returnUrl through the auth hop)
- Login.razor (static SSR, posts to /auth/login; Q5 wording about
generic-vs-specific LDAP errors)
- Account.razor (identity + fleet roles + raw LDAP groups; Q4 — no
per-cluster grants; fleet-wide LDAP-group → role mapping only)
- Fleet.razor (per-node deployment status: reads NodeDeploymentState
+ unions with IClusterRoleInfo.MembersWithRole("driver") so freshly-
joined nodes appear as "waiting"; 10s auto-refresh)
- Hosts.razor (Akka cluster topology: members, status, roles, role-
leader; 5s auto-refresh)
Host's stub App.razor deleted; Program.cs now points at
AdminUI.Components.App via an added using.
All 104 v2 tests remain green.