Files
scadaproj/docs/plans/2026-07-22-overview-dashboard-impl-plan.md.tasks.json
T
Joseph Doherty 5fe7135e2c feat(overview): poller, leader aggregation and the dashboard UI
Tasks 3.4-3.6 of the overview-dashboard plan.

Polling (3.4/3.5):
- OverviewPollerService: one timer at the fastest configured cadence, each
  target polled when its own interval is due, fan-out per sweep, no retries
  (the next tick is the retry, so damping stays the only place that decides
  whether a failure is worth showing).
- OverviewSnapshotStore: atomic swap of a complete immutable graph plus a
  change event. Pages read it and never probe — the cache-first requirement.
- LeaderResolver: per-group leader from akka-cluster data, with the
  split-brain flag scoped to members that are actually answering, so an
  ordinary failover is not misread as a disagreement.

UI (3.6): ThemeShell + StatusPill/TechCard composition against the mockup;
instance cards carry the status stripe (dashed for Unreachable), the check
detail list and the raw ready/active signal line.

Two defects found by verification rather than by review:

- The active tier answers with a status code and an EMPTY body, but the client
  demanded parseable JSON on every probe — so a healthy pair rendered as
  "role unknown" instead of Active/Standby. Split into ProbeAsync (ready tier,
  body is the payload) and ProbeStatusAsync (active tier, code is the answer).
  Caught by a live smoke run against two fake health endpoints; the poller
  tests now serve an empty body so they hold the fix.

- SweepAsync published even when cancellation was already requested, leaving a
  half-probed registry as the store's final state on shutdown. It now checks
  the token itself rather than relying on the transport to throw.

Also: app.UseAntiforgery() is required despite the anonymous-by-design
pipeline — AddRazorComponents stamps antiforgery metadata unconditionally and
the endpoint middleware hard-fails without it (every page was 500). Chosen
over DisableAntiforgery() so a future form is protected by default.

147 tests, 0 warnings. Live-verified end to end against fake endpoints:
Up/Unreachable/Active/Standby, leader chip, cluster-data line, KPI counts.
2026-07-24 07:05:16 -04:00

189 lines
5.1 KiB
JSON

{
"plan": "docs/plans/2026-07-22-overview-dashboard-impl-plan.md",
"tasks": [
{
"id": "1",
"subject": "0.1 Health writer: optional per-entry `data` object",
"status": "completed",
"blockedBy": []
},
{
"id": "2",
"subject": "0.2 AkkaClusterHealthCheck: populate cluster-view data",
"status": "completed",
"blockedBy": []
},
{
"id": "3",
"subject": "0.3 Health 0.2.0: version, test, pack, publish, verify",
"status": "completed",
"blockedBy": [
"1",
"2"
]
},
{
"id": "4",
"subject": "1.1 ScadaBridge: bump Health packages to 0.2.0",
"status": "completed",
"blockedBy": [
"3"
]
},
{
"id": "5",
"subject": "1.2 ScadaBridge: site health checks (akka-cluster, localdb, active-node)",
"status": "completed",
"blockedBy": [
"4"
]
},
{
"id": "6",
"subject": "1.3 ScadaBridge: map MapZbHealth on the site pipeline",
"status": "completed",
"blockedBy": [
"5"
]
},
{
"id": "7",
"subject": "1.4 ScadaBridge: site health tests",
"status": "completed",
"blockedBy": [
"6"
]
},
{
"id": "8",
"subject": "2.1 OtOpcUa: verify `akka` check is the shared AkkaClusterHealthCheck",
"status": "completed",
"blockedBy": []
},
{
"id": "9",
"subject": "2.2 OtOpcUa: bump Health to 0.2.0",
"status": "completed",
"blockedBy": [
"3",
"8"
]
},
{
"id": "10",
"subject": "2.3 Alignment bumps: mxgw + HistorianGateway Health 0.2.0",
"status": "completed",
"blockedBy": [
"3"
]
},
{
"id": "11",
"subject": "3.1 Overview: scaffold project",
"status": "completed",
"blockedBy": [
"3"
]
},
{
"id": "12",
"subject": "3.2 Overview: registry options + validation",
"status": "completed",
"blockedBy": [
"11"
]
},
{
"id": "13",
"subject": "3.3 Overview: health client + status model",
"status": "completed",
"blockedBy": [
"11"
]
},
{
"id": "14",
"subject": "3.4 Overview: poller + snapshot store",
"status": "completed",
"blockedBy": [
"12",
"13"
]
},
{
"id": "15",
"subject": "3.5 Overview: leader aggregation",
"status": "completed",
"blockedBy": [
"13"
]
},
{
"id": "16",
"subject": "3.6 Overview: UI (ThemeShell, Overview page, InstanceCard)",
"status": "completed",
"blockedBy": [
"14",
"15"
]
},
{
"id": "17",
"subject": "3.7 Overview: self-observability",
"status": "pending",
"blockedBy": [
"14"
]
},
{
"id": "18",
"subject": "3.8 Overview: tests",
"status": "pending",
"blockedBy": [
"16",
"17"
]
},
{
"id": "19",
"subject": "3.9 Overview: config + docker",
"status": "pending",
"blockedBy": [
"17"
]
},
{
"id": "20",
"subject": "4 Live acceptance (design \u00a79)",
"status": "pending",
"blockedBy": [
"7",
"9",
"18",
"19"
]
}
],
"notes": {
"deferred": [
"Task 2.2 rig check (admin node /health/ready shows data.leader) \u2014 live docker-dev, folded into Phase 4.",
"Phase 1 DoD rig curls (site :8084 ready/active split) \u2014 live docker rig, folded into Phase 4."
],
"branches": {
"scadaproj": "feat/health-0.2.0-cluster-data (80668a0) -> feat/overview-dashboard, stacked",
"ScadaBridge": "feat/site-node-health (commit 47850c0f)",
"OtOpcUa": "feat/health-0.2.0-bump (commit 2e515c34, made in an isolated git worktree \u2014 feat/mesh-phase6 working tree untouched)",
"HistorianGateway": "chore/health-0.2.0-bump (commit 51f0c2b)",
"MxAccessGateway": "chore/health-0.2.0-bump (commit 2d54ace)"
},
"front_loaded": [
"Validator / health-client / status-derivation tests from task 3.8 were written with tasks 3.2-3.3 so each batch is verified; 3.8 covers the remainder (poller, leader, store, bUnit, boot).",
"Poller / snapshot-store / leader-resolver / bUnit InstanceCard + Overview page tests were written with tasks 3.4-3.6; task 3.8 now covers only the WebApplicationFactory boot tests."
],
"deviations": [
"Task 3.6 said 'no antiforgery'. AddRazorComponents stamps antiforgery metadata on every component endpoint unconditionally and the endpoint middleware hard-fails without a matching middleware, so every page returned 500. Resolved with app.UseAntiforgery() (inert here, no forms) rather than MapRazorComponents(...).DisableAntiforgery(), which would silently expose the first form anyone adds. No authentication was added.",
"Bootstrap IS vendored (copied from HistorianGateway, v5.3.3). Not optional: ThemeShell and TechButton are built on Bootstrap utility classes, so the shared kit does not render without it."
]
}
}