docs(audit): apply per-cluster judgment fixes across living docs

Resolve audit findings: correct WorkerEnvelope proto/route/metric/session
facts; rewrite auth (ZB.MOM.WW.Auth migration), dashboard (ZB.MOM.WW.Theme),
and StyleGuide (foreign-project copy-paste); document alarm subsystem, Ldap
options, and gateway alarm broker; fix client CLI flags and package paths.
This commit is contained in:
Joseph Doherty
2026-06-03 16:01:28 -04:00
parent f84e0c3474
commit e541339c07
29 changed files with 1102 additions and 432 deletions
+120 -38
View File
@@ -9,11 +9,13 @@ statistics in real time.
## Technology Choice
Decision: Blazor Server with Bootstrap CSS/JS.
Decision: Blazor Server with the shared `ZB.MOM.WW.Theme` kit layered over
Bootstrap CSS/JS.
Allowed UI stack:
- ASP.NET Core Blazor Server,
- the `ZB.MOM.WW.Theme` kit (layout chassis, status components, design tokens),
- Bootstrap CSS,
- Bootstrap JavaScript,
- small local CSS for layout and status styling,
@@ -30,7 +32,35 @@ Not allowed for v1:
Rationale: Blazor Server keeps the dashboard in the gateway process, avoids a
separate frontend build, and gives real-time UI updates through the Blazor
SignalR circuit. Bootstrap is sufficient for a basic dashboard.
SignalR circuit. The `ZB.MOM.WW.Theme` kit gives the dashboard the same chassis,
status vocabulary, and visual identity as the other ZB.MOM.WW operations UIs
without re-implementing layout and status styling per project.
## Theme Kit
The dashboard depends on the shared `ZB.MOM.WW.Theme` NuGet package
(version `0.2.0`, referenced in `ZB.MOM.WW.MxGateway.Server.csproj`). The kit is
a Razor Class Library that ships the technical-light design system: a layout
chassis, a small set of UI components, the design tokens, and the head/script
asset wiring. The dashboard takes its chrome and status presentation from the
kit and adds only its own pages and view CSS on top.
Components and assets used:
| Kit member | Role in the dashboard |
|---|---|
| `<ThemeShell>` | The application chassis — vertical side rail (brand, hamburger, responsive collapse) plus a content area. `MainLayout.razor` wraps it and supplies `Nav`, `RailFooter`, and `ChildContent` slots. |
| `<NavRailSection>` / `<NavRailItem>` | Grouped navigation items in the rail. Section expand/collapse persistence is owned by the kit (`<details>` + `ThemeScripts`); the app runs no JS interop for it. |
| `<LoginCard>` | The centered login card on `Login.razor`. Renders a native static `<form method="post" action="/login">` so the submit reaches the minimal-API endpoint rather than a Blazor event. |
| `<StatusPill State="…">` | The status chip. `StatusBadge.razor` is a thin adapter that maps domain state text to one of four `StatusState` values (`Ok`, `Warn`, `Bad`, `Idle`) and renders this pill. |
| `<ThemeHead/>` | Loaded in `App.razor`'s `<head>`; injects the kit's `theme.css` and related head assets. |
| `<ThemeScripts/>` | Loaded at the end of `App.razor`'s `<body>`; supplies the rail's interactive behavior. |
| Token system | `theme.css` defines all design tokens (`var(--card)`, `var(--ink)`, `var(--accent)`, `var(--mono)`, the state colors, etc.). The local `site.css` references these tokens and defines no hard-coded colors. |
The dependency on this kit is the reason the layout shell, navigation, status
chips, and tokens differ from a stock Bootstrap dashboard. See
[Dashboard Interface Design](./DashboardInterfaceDesign.md) for how the kit's
tokens and components shape the visual language.
## Hosting Model
@@ -67,8 +97,8 @@ Endpoint layout:
The `/galaxy` page surfaces the Galaxy Repository browse summary
(deployed object hierarchy size, last deploy timestamp, attribute totals,
template usage, and connectivity sync info). The summary is fed by
`GalaxySummaryCache`, which is refreshed off the request path by
`GalaxySummaryRefreshService` on the
`GalaxyHierarchyCache`, which is refreshed off the request path by
`GalaxyHierarchyRefreshService` on the
`MxGateway:Galaxy:DashboardRefreshIntervalSeconds` cadence so the dashboard
never blocks on SQL. See [Galaxy Repository Browse](./GalaxyRepository.md) for
the underlying gRPC service.
@@ -79,24 +109,31 @@ the underlying gRPC service.
ZB.MOM.WW.MxGateway.Server
Dashboard/
Components/
App.razor
App.razor (loads <ThemeHead/> / <ThemeScripts/>)
Routes.razor
DashboardPageBase.cs
DashboardDisplay.cs
Layout/
DashboardLayout.razor
MainLayout.razor (ThemeShell side-rail chassis)
LoginLayout.razor (minimal, no rail; hosts <LoginCard>)
Pages/
DashboardHome.razor
Login.razor
SessionsPage.razor
SessionDetailsPage.razor
WorkersPage.razor
EventsPage.razor
AlarmsPage.razor
GalaxyPage.razor
BrowsePage.razor
ApiKeysPage.razor
SettingsPage.razor
Shared/
MetricCard.razor
StatusBadge.razor
StatusBadge.razor (adapter over kit <StatusPill>)
FaultList.razor
BrowseTreeNodeView.razor
ConfirmDialog.razor
DashboardSnapshotService.cs
DashboardAuthorizationHandler.cs
DashboardAuthenticator.cs
@@ -244,10 +281,14 @@ Show:
- admin Close session / Kill worker controls (Admin role only).
The Sessions list, the Workers list, and this details page all render the same
admin controls when the signed-in principal carries the `Admin` role; viewers
admin controls when the signed-in principal carries the `Administrator` role; viewers
and the localhost-anonymous bypass see no action affordances and the server
re-checks the role on every invocation. Every destructive admin action is
gated by a confirmation dialog before it reaches `ISessionManager`.
gated by the shared `ConfirmDialog` component before it reaches
`ISessionManager`. `ConfirmDialog` is a reusable Bootstrap modal (title,
message, confirm/cancel buttons, and a busy state that disables both buttons
while the action runs); each page binds its open state and confirm/cancel
callbacks. The API keys page uses the same component.
- **Close session** routes through `ISessionManager.CloseSessionAsync`: the
worker is asked to shut down gracefully and is killed only as a fallback if
@@ -289,7 +330,8 @@ it opt-in and redacted.
### Browse page
`/browse` lets an operator explore the Galaxy tag hierarchy and watch
live values. The tree is built in-process by `DashboardBrowseTreeBuilder` from
live values. The tree is built in-process by the static
`DashboardBrowseTreeBuilder` (in `DashboardBrowseModel.cs`) from
`IGalaxyHierarchyCache.Current` — the same cache the Galaxy page reads — so a
render costs no gRPC call and no SQL round-trip. Each node shows its child
objects and, when expanded, its attributes with attribute name, data type
@@ -307,7 +349,10 @@ diagnostic session/worker views.
### Alarms page
`/alarms` lists the alarms the gateway's central alarm monitor
currently holds as Active or ActiveAcked, refreshed every three seconds. It
currently holds as Active or ActiveAcked. The page injects
`IDashboardLiveDataService` and drives a `PeriodicTimer` poll loop that calls
`QueryAlarmsAsync` every three seconds, rather than subscribing to the snapshot
hub or holding a `CurrentAlarms` reference directly. It
defaults to showing unacknowledged `Active` alarms; filters add acknowledged
alarms and narrow by area, severity range, and a reference/source/description
text search. Cleared alarms are not retained — the gateway holds no
@@ -358,7 +403,7 @@ for what each constraint means and how it is enforced on the gRPC path.
Create, Rotate, Revoke, and Delete controls render only when the signed-in
user is authorized. `DashboardApiKeyAuthorization.CanManage` requires an
authenticated principal carrying the `Admin` role claim (resolved at login
authenticated principal carrying the `Administrator` role claim (resolved at login
from the user's LDAP groups via `MxGateway:Dashboard:GroupToRole`). A
`Viewer` role can read the table but sees no action controls, and an
anonymous localhost session shows the same read-only view.
@@ -385,10 +430,11 @@ Create and Rotate return the assembled `mxgw_<keyId>_<secret>` token **once**,
in a one-time banner. It is never shown again, so the operator must copy it
immediately. This mirrors the `apikey create-key` / `rotate-key` CLI.
Every management action appends an `api_key_audit` entry
(`dashboard-create-key`, `dashboard-rotate-key`, `dashboard-revoke-key`,
`dashboard-delete-key`) with the key id and the caller's remote address.
Secrets and pepper values are never logged.
Every management action writes an entry to the canonical `audit_event` store
through `IAuditWriter` (`dashboard-create-key`, `dashboard-rotate-key`,
`dashboard-revoke-key`, `dashboard-delete-key`) with the key id, the caller's
remote address, and a correlation id. Secrets and pepper values are never
logged.
### Settings page
@@ -408,23 +454,33 @@ Do not show API key secrets or pepper values.
Dashboard authentication is LDAP-backed, distinct from the API-key model used
on the gRPC API. Users sign in with directory credentials; the gateway maps
their LDAP groups to one of two dashboard roles (`Admin` or `Viewer`) and
their LDAP groups to one of two dashboard roles (`Administrator` or `Viewer`) and
issues a cookie carrying those role claims.
Implemented behavior:
- a static `/login` HTML form posts username/password to the gateway;
- `DashboardAuthenticator` binds against `MxGateway:Ldap` (service-account bind,
user search, candidate bind) using `Novell.Directory.Ldap.NETStandard`;
- the user's `memberOf` (or short CN) is matched against
`MxGateway:Dashboard:GroupToRole`; the resolved role(s) are emitted as
`ClaimTypes.Role` claims, alongside the per-group `mxgateway:ldap_group`
claims;
- a successful login signs in the `MxGateway.Dashboard` cookie scheme
(`MxGatewayDashboard`, HttpOnly, SameSite=Strict, Secure);
- `GET /login` is served by the `[AllowAnonymous]` Blazor `Login.razor`
component (under `LoginLayout`), which renders the shared kit's `<LoginCard>`.
`LoginCard` emits a native static `<form method="post" action="/login">`
(username, password, hidden returnUrl) plus an `<AntiforgeryToken/>`. A native
form submit is not a Blazor event, so it reaches the minimal-API `POST /login`
endpoint regardless of the app's InteractiveServer render mode;
- `DashboardAuthenticator` delegates bind/search to the shared
`ZB.MOM.WW.Auth.Ldap` provider, registered by `AddZbLdapAuth(configuration,
"MxGateway:Ldap")`. The provider performs a service-account bind, user search,
then candidate bind, and fails closed;
- the user's group membership (stripped to its first RDN by the provider) is
matched against `MxGateway:Dashboard:GroupToRole`; the resolved role(s) are
emitted as `ClaimTypes.Role` claims, alongside the per-group
`mxgateway:ldap_group` claims;
- a successful login signs in the `MxGateway.Dashboard` cookie scheme. The
cookie defaults to the name `MxGatewayDashboard` (HttpOnly, SameSite=Strict,
Secure) and can be overridden via `MxGateway:Dashboard:CookieName`;
- a user with no matching group cannot sign in — the login screen returns the
generic credential-rejected message;
- antiforgery tokens guard the login and logout POSTs.
generic credential-rejected message via `/login?error=…`;
- antiforgery tokens guard the login and logout POSTs. `POST /logout` (and a
`GET /logout` convenience redirect) sign the cookie out and return to
`/login`.
Three authorization policies are registered:
@@ -443,8 +499,8 @@ Viewer role.
### Hub bearer flow
SignalR connections cannot reuse the `__Host-` cookie when the JS client
upgrades to WebSocket — the cookie's `SameSite=Strict; Path=/` keeps it from
SignalR connections cannot reuse the `MxGatewayDashboard` cookie when the JS
client upgrades to WebSocket — the cookie's `SameSite=Strict; Path=/` keeps it from
being forwarded by the browser's WebSocket layer in some edge cases. The
dashboard mints short-lived bearer tokens for the connection:
@@ -480,8 +536,10 @@ Effective configuration:
"RecentFaultLimit": 100,
"RecentSessionLimit": 200,
"ShowTagValues": false,
"CookieName": null,
"RequireHttpsCookie": true,
"GroupToRole": {
"GwAdmin": "Admin",
"GwAdmin": "Administrator",
"GwReader": "Viewer"
}
}
@@ -489,6 +547,15 @@ Effective configuration:
}
```
Two cookie keys tune the auth cookie:
- `CookieName` overrides the cookie name. Null or blank keeps the canonical
default `MxGatewayDashboard`, so a misconfiguration cannot leave the cookie
unnamed.
- `RequireHttpsCookie` (default `true`) sets the cookie `SecurePolicy` to
`Always`. Set it to `false` for dev HTTP deployments, which relaxes the policy
to `SameAsRequest`.
See [Gateway Configuration](./GatewayConfiguration.md#dashboard-options) for
the full option table and the policies/hubs that derive from these values.
@@ -504,17 +571,31 @@ the full option table and the policies/hubs that derive from these values.
## Styling
The dashboard serves Bootstrap 5.3.3 assets from
`src/ZB.MOM.WW.MxGateway.Server/wwwroot/lib/bootstrap/` and local layout/status styling
from `src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/dashboard.css`.
Styling is layered. From base to top:
1. Bootstrap 5.3.3 assets served from
`src/ZB.MOM.WW.MxGateway.Server/wwwroot/lib/bootstrap/`.
2. The `ZB.MOM.WW.Theme` kit's `theme.css` (the technical-light design system),
which owns the design tokens and the kit component styles. `App.razor` loads
it through the kit's `<ThemeHead/>` component, and pairs it with
`<ThemeScripts/>` at the end of `<body>` for the rail's interactive behavior.
3. The local view stylesheet
`src/ZB.MOM.WW.MxGateway.Server/wwwroot/css/site.css`, which wires the
dashboard's own class names and Bootstrap widgets onto the kit tokens. It
defines no hard-coded colors.
The minimal `/denied` page is rendered outside the Blazor circuit, so it loads
the kit CSS directly from the static-web-asset path
(`/_content/ZB.MOM.WW.Theme/css/theme.css` and `…/layout.css`) plus Bootstrap
and `site.css`.
Recommended visual language:
- compact tables,
- status badges,
- the kit `StatusPill` for state,
- metric cards,
- Bootstrap alerts for faults,
- restrained colors,
- restrained colors drawn from the kit tokens,
- no decorative hero sections,
- no charting dependency for v1.
@@ -530,7 +611,7 @@ Dashboard unit/component tests should cover:
- snapshot projection,
- dashboard auth authorization decisions,
- login API-key validation behavior,
- login LDAP bind and group-to-role mapping behavior,
- pages render with empty state,
- pages render with active sessions,
- pages render with faulted sessions,
@@ -557,7 +638,8 @@ Integration tests should verify:
The first dashboard slice implements:
1. Blazor Server hosting in `ZB.MOM.WW.MxGateway.Server`.
2. local Bootstrap static assets.
2. local Bootstrap static assets plus the `ZB.MOM.WW.Theme` kit layer
(chassis, tokens, status components).
3. dashboard configuration binding.
4. dashboard auth using LDAP bind + role-mapped HTTP-only cookie.
5. `DashboardSnapshotService` projecting gateway state for read views.