feat(kpi): K5 — Host central wiring + KpiHistoryRecorder cluster singleton + appsettings (not readiness-gated)

Wire the M6 KPI History recorder into the central composition path:
- Program.cs: call services.AddKpiHistory(configuration) on the central-only
  branch alongside AddNotificationOutbox/AddAuditLog/AddSiteCallAudit.
- AkkaHostedService.cs: register KpiHistoryRecorderActor as a central,
  non-role-scoped ClusterSingletonManager + ClusterSingletonProxy + a
  PhaseClusterLeave CoordinatedShutdown graceful-stop drain (singleton name
  'kpi-history-recorder'), copied/adapted from the audit-log-purge block.
- appsettings.Central.json (Host + docker + docker-env2 central nodes): add a
  ScadaBridge:KpiHistory section (SampleInterval 00:01:00, RetentionDays 90,
  PurgeInterval 1.00:00:00, DefaultMaxSeriesPoints 200).

KPI history is observability/best-effort and MUST NOT gate readiness: the
recorder is deliberately NOT added to RequiredSingletonsHealthCheck or any
other readiness gate.
This commit is contained in:
Joseph Doherty
2026-06-17 20:20:34 -04:00
parent 601cc6f594
commit e14433cd64
7 changed files with 93 additions and 0 deletions
@@ -16,6 +16,7 @@ using ZB.MOM.WW.ScadaBridge.Host.Actors;
using ZB.MOM.WW.ScadaBridge.Host.Health;
using ZB.MOM.WW.ScadaBridge.InboundAPI;
using ZB.MOM.WW.ScadaBridge.InboundAPI.Middleware;
using ZB.MOM.WW.ScadaBridge.KpiHistory;
using ZB.MOM.WW.ScadaBridge.ManagementService;
using ZB.MOM.WW.ScadaBridge.NotificationOutbox;
using ZB.MOM.WW.ScadaBridge.NotificationService;
@@ -110,6 +111,11 @@ try
// but the call is here for symmetry with the other audit composition
// roots so future per-actor DI lands without touching Program.cs.
builder.Services.AddSiteCallAudit();
// KPI History (#26, M6) — central-only. Binds KpiHistoryOptions from
// ScadaBridge:KpiHistory and registers the validated options consumed by
// the KpiHistoryRecorderActor cluster singleton (started in
// AkkaHostedService). Observability/best-effort: NOT readiness-gated.
builder.Services.AddKpiHistory(builder.Configuration);
builder.Services.AddTemplateEngine();
builder.Services.AddDeploymentManager();
// Host is the composition root and owns config-coupled wiring: register the