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

12 KiB
Raw Permalink Blame History

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):
    { "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 YesHealthChecksUI: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

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.

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.