Files
scadaproj/overview_dashboard_options.md
Joseph Doherty 6dd7858619 docs(overview): family overview dashboard — options, design, impl plan, mockup
Options investigation (Aspire standalone/HealthChecks.UI/Gatus/Homepage/
Grafana vs custom) → decision: custom ZB.MOM.WW.Overview Blazor app in the
family look. Design: anonymous read-only single pane, appsettings registry
of all four apps' instances, cache-first poller on a configurable timer,
configurable staleness timeout, Active/Standby from /health/active, and
per-cluster Akka leader chips (needs Health 0.2.0 optional per-entry data).
Impl plan: phased + code-verified — Phase 0 Health 0.2.0 (writer data field
+ AkkaClusterHealthCheck cluster view), Phase 1 ScadaBridge site-node
MapZbHealth PR (:8084; role-scoped active check), Phase 2 OtOpcUa bump,
Phase 3 the app (:5320), Phase 4 live rig acceptance. Mockup is the visual
reference: real Theme tokens + embedded IBM Plex, all card states incl.
stale + split-brain. Ready to execute in a fresh session.
2026-07-22 09:22:13 -04:00

87 lines
12 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Central overview dashboard for the scadaproj family — options investigation
**Date:** 2026-07-22
**Goal:** a single pane showing status of every app instance (OtOpcUa, ScadaBridge, MxAccessGateway, HistorianGateway — each with multiple nodes/pairs), with deep links to each instance's own management UI, pre-configured via an appsettings registry of applications and instances. "Aspire-dashboard-like."
---
## 1. What the family already gives us (verified in code, 2026-07-22)
The reconnaissance result is unusually favorable: **the data plane for a poll-based overview already exists on all four apps, with zero changes needed in the target apps.**
- **Health endpoints are uniform and anonymous everywhere.** All four apps call the shared `MapZbHealth()` (`ZB.MOM.WW.Health`), which maps `/health/ready`, `/health/active`, and `/healthz`, all `.AllowAnonymous()` in the library itself (`ZbHealthEndpointExtensions.cs:35-64`). Even OtOpcUa's global `FallbackPolicy = RequireAuthenticatedUser` doesn't gate them (endpoint `AllowAnonymous` wins).
- **The JSON contract is canonical and stable** (`ZbHealthWriter.cs:45-81`, camelCase):
```json
{ "status": "Healthy|Degraded|Unhealthy",
"totalDurationMs": 12.3,
"entries": { "<check>": { "status": "...", "description": null, "durationMs": 1.2 } } }
```
Healthy/Degraded → HTTP 200, Unhealthy → 503.
- **`/health/active` is the active/standby signal.** ScadaBridge Central's `active` tier runs the oldest-member `active-node` check; OtOpcUa has `admin-leader`. No off-the-shelf product can express active/standby natively — this endpoint makes it a first-class dashboard column for us.
- **`/metrics` (Prometheus) is anonymous on all four** (explicitly on OtOpcUa; by absence of a fallback policy on the others). OTLP push is config-only on three apps (`<App>:Telemetry:Exporter=Otlp` + `OtlpEndpoint`); **HistorianGateway hardcodes its telemetry setup and would need a one-line change** to gain the config keys.
- **Nothing aggregates across apps today.** The closest is ScadaBridge's `/monitoring/health` (CentralUI), but it's push-based over the site transport and scoped to ScadaBridge sites in one cluster. OtOpcUa/HG/mxgw pages show only their own instance. A multi-app registry + remote HTTP poller is genuinely new work.
- **Reusable building blocks for an in-house pane:** `ZB.MOM.WW.Theme` (`ThemeShell`, `StatusPill` with Ok/Warn/Bad/Idle/Info, `TechCard`), `ZB.MOM.WW.Auth.AspNetCore` (the same hardened LDAP cookie login all four apps use), `ZB.MOM.WW.Configuration` (validate the registry at startup), `ZB.MOM.WW.Telemetry`/`Health` (the dashboard monitors itself the family way). No prebuilt tile/grid widget exists — a node-card composite would be new, but it's `TechCard` + `StatusPill` composition.
- **Known ports for the registry:** OtOpcUa AdminUI dev `:9000` (driver-only nodes fall back to `:5000`); ScadaBridge Central `:5000/:5001` (site nodes expose only `:8084` metrics — no health/UI); mxgw `:5120/:7121`; HistorianGateway dashboard `:5220` dev / single TLS ALPN port (rec. `:5222`) in production.
**Watch-out for the registry design:** ScadaBridge *site* nodes map no health endpoints at all (metrics only), so a site-pair row can't be polled the same way as a central pair — either poll `:8084/metrics` for liveness, read site status from Central's aggregator, or (later) add `MapZbHealth` to site nodes.
## 2. Off-the-shelf candidates (facts verified by fetch, 2026-07-22)
| Option | Version / status | Config-file registry | Persistence | Auth | Verdict |
|---|---|---|---|---|---|
| **Aspire dashboard (standalone)** | Aspire 13 (Nov 2025); `mcr.microsoft.com/dotnet/aspire-dashboard` | **No** — resource UI is disabled in standalone; needs a custom gRPC resource service to light up | None (in-memory, lost on restart) | BrowserToken / OIDC | Not the ops pane. MS docs: dev/short-term diagnostic tool. `AddExternalService()`/custom URLs are AppHost run-mode features only. |
| **AspNetCore.HealthChecks.UI (Xabaril)** | 9.0.0 (Dec 2024), net8.0 TFM only; upstream coasting on dependabot since Aug 2025 ("looking for maintainers"); tiny `DotNetDiag` fork exists | **Yes** — `HealthChecksUI:HealthChecks` array in appsettings; exactly the requested model | SQLite/SQL Server/InMemory | In-process — **our LDAP cookie works unmodified** | Closest functional match, but it **requires the `UIResponseWriter` payload** (issue #285) — our `ZbHealthWriter` shape is close but not identical (`durationMs` vs `duration`, no `data`/`tags`), so every app would need a second UI-format endpoint. Plus a slow-moving upstream dependency. |
| **Gatus** | v5.36.0 (May 2026), active | **Yes** — YAML; conditions incl. `[BODY]` JSONPath, so it can evaluate our health JSON **with zero app changes** (incl. degraded) | SQLite/Postgres | Built-in basic/OIDC (no LDAP cookie) | Best zero-effort comparable: registry + JSON conditions + uptime history + 30+ alert providers in one Go binary. Trade: non-.NET, non-Theme UI, outside our auth stack. |
| **Homepage (gethomepage.dev)** | v1.13.2 (Jun 2026) | Yes — YAML link tiles | n/a | None (reverse proxy) | Link portal only; `siteMonitor` is status-code-only — cannot express Degraded or active/standby. Out. |
| **Grafana + Prometheus + blackbox** | Grafana 13.0 (Apr 2026) | Yes — scrape config | Full TSDB history | **Native LDAP** | Most durable, real alerting, grows into actual ops monitoring. Highest setup (3 containers + dashboard authoring); Degraded is invisible to blackbox (HTTP 200) without a status gauge metric or `ResultStatusCodes` remap; deep links via data links are clunkier than a real portal. |
## 3. Options
### Option A — build a small in-house overview app (`ZB.MOM.WW.Overview`) — **RECOMMENDED**
A thin Blazor Server app hosted in scadaproj alongside the other shared components, composed almost entirely from the family's own libraries:
- **Registry:** `Overview:Applications[]` in appsettings — per app: name, kind, accent; per instance: display name, site/role, `healthBaseUrl`, `managementUrl`, optional `metricsUrl`, `expectActive` flag, poll interval/timeout overrides. Validated at startup via `ZB.MOM.WW.Configuration` (`AddValidatedOptions` + `ConfigPreflight`) — malformed registry fails the boot, not the first poll.
- **Poller:** one `BackgroundService` + `IHttpClientFactory`; polls each instance's `/health/ready` + `/health/active` in parallel on a fixed cadence (515 s), deserializes the canonical `ZbHealthWriter` JSON (a ~30-line typed client — worth putting in `ZB.MOM.WW.Health` as `ZbHealthReportClient` so it's contract-owned), and keeps an in-memory snapshot. Statuses per instance: **Up / Degraded / Down / Unreachable**, plus **Active/Standby** from `/health/active` — the column no off-the-shelf option can render.
- **UI:** `ThemeShell` + a new node-card grid built from `TechCard` + `StatusPill`; per-check expandable detail (the `entries` dict); every card deep-links to the instance's own management page. LDAP cookie login via `AddZbLdapAuth` — same UX as the other four apps (the dashboard reads only anonymous endpoints, but the pane itself should sit behind the family login).
- **Self-hosting:** it registers its own `MapZbHealth`/`MapZbMetrics` like any family app; a `docker/` folder mirroring HistorianGateway's runtime-only image pattern.
**Fit:** best-practice fit for this shop. The requested feature set (appsettings registry, status, deep links) maps 1:1 onto libraries that exist precisely so new apps can be assembled this way; the only genuinely new code is the poller + snapshot + one grid page — realistically **24 days** for a first useful version. It also keeps auth, theming, and ops conventions identical to the rest of the family, which none of the off-the-shelf options can do. Deliberate scope cut for v1: no history/alerting (that's Option D's job if it's ever needed — and the registry file ports straight to a Prometheus scrape config).
**Costs / risks:** you own the code (small); no built-in uptime history or webhooks in v1; the ScadaBridge-site-node gap (§1 watch-out) needs a decision in the registry design.
### Option B — AspNetCore.HealthChecks.UI embedded in a thin family host
Host the Xabaril UI inside a small ASP.NET app behind our LDAP cookie. Gets registry-from-appsettings, polling, SQLite history, and webhooks for near-zero UI code.
**Fit:** the strongest off-the-shelf functional match, and the only one where `ZB.MOM.WW.Auth` works unmodified. But it costs a change in **all four target apps** (a second health endpoint in `UIResponseWriter` format, because the UI hard-requires that payload), buys a visibly unmaintained upstream (net8.0-only TFM, dependabot-only commits since Aug 2025), has no active/standby concept, and its UI will never look like Theme. For a shop that already owns Health + Theme + Auth libs, it saves less than it costs.
### Option C — Gatus container as a zero-app-change status page
Deploy Gatus with a YAML registry; its `[BODY]` JSONPath conditions consume our existing health JSON directly (including Degraded), and it brings uptime history + alerting for free.
**Fit:** the best effort-to-value stopgap — running in an afternoon, no app changes, actively maintained. Best practice would call this the "buy" answer if the family didn't have its own UI/auth stack. Trades: separate basic/OIDC auth outside the LDAP cookie, un-Theme-able UI, and active/standby only expressible as another endpoint row rather than a paired-node concept. Reasonable as an interim pane while Option A is built, or as a permanent low-ceremony answer if the dashboard turns out to be rarely used.
### Option D — Grafana + Prometheus + blackbox exporter
**Fit:** the right long-term answer **if** the real goal grows into ops monitoring (history, alerting, SLOs) rather than a convenience pane — native LDAP auth is a genuine plus. As a *first* deliverable for "status + links" it's the highest effort and the worst at deep links/branding, and Degraded/active-standby need extra plumbing (a health-status gauge in `ZB.MOM.WW.Telemetry` would be the clean fix, and is worth doing eventually regardless). Not first; a natural phase 2 that Option A's registry feeds directly.
### Option E — Aspire dashboard standalone
**Fit:** rejected as the overview pane — in standalone mode the resource/status/links UI is disabled (it's an OTLP telemetry viewer only), nothing persists across restarts, and Microsoft positions it explicitly as a dev/short-term diagnostic tool. **Worth keeping as a free companion tool:** three of the four apps can push OTLP with config only, so an ad-hoc `aspire-dashboard` container gives structured logs + traces during debugging for zero code. (HistorianGateway needs the one-line telemetry-config change to join.)
> **DECISION (2026-07-22):** Option A chosen — custom app, family look-and-feel (Theme/Auth/Health kit). Aspire rejected for the pane. V1 design: [`docs/plans/2026-07-22-overview-dashboard-design.md`](docs/plans/2026-07-22-overview-dashboard-design.md).
## 4. Recommendation
**Option A** — build the thin `ZB.MOM.WW.Overview` app from the family's own parts, with two riders:
1. If an interim pane is wanted *this week*, stand up **Gatus (Option C)** against the existing health endpoints — zero app changes, discard later.
2. Keep **Aspire standalone (Option E)** in the toolbox as a dev-time OTLP viewer, and make HistorianGateway's telemetry config-driven (one line) so all four apps can join it.
Decisions to make before building A: how site-node ScadaBridge rows are sourced (metrics-liveness vs Central-aggregator vs adding MapZbHealth to sites), and whether v1 needs any history/alerting (recommended: no — defer to Option D if ever needed).
---
*Sources: family code (ZB.MOM.WW.Health `ZbHealthEndpointExtensions.cs`/`ZbHealthWriter.cs`, per-app Program.cs wiring); aspire.dev standalone/configuration/security docs + Aspire 13 announcement; Xabaril repo + NuGet + issue #285 + DotNetDiag fork; TwiN/gatus releases; gethomepage.dev docs; Grafana/blackbox dashboards. All web claims verified by fetch 2026-07-22.*