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.
Placeholder AlarmStateChanged rows are a DebugView snapshot-only concept emitted
by InstanceActor.BuildAlarmStatesSnapshot; they are never a real alarm transition.
Their timestamp may be DateTimeOffset.MinValue (the Protobuf Timestamp lower boundary),
which can throw when packed via Timestamp.FromDateTimeOffset.
Added early-return guard at the top of HandleAlarmStateChanged before any timestamp
pack or channel write. Updated the existing NativeBindingLinkage round-trip test to
use a real (non-placeholder) native alarm; added DropsAlarmStateChanged_WhenIsConfiguredPlaceholder
to assert placeholders are silently dropped (15/15 pass).
Replace the two flat capped tables with a Bootstrap nav-tabs layout, each
tab hosting a TreeView<DebugTreeNode> built from the live latest-per-name
dictionaries via DebugTreeBuilder. Drop the MaxRows cap, auto-scroll locks,
and Clear buttons (change-feed affordances that don't fit a current-status
tree); HandleStreamEvent now does a plain dictionary upsert. Per-tab filters
ExpandAll on change so matches stay visible. Branch nodes surface roll-up
badges (active-count for alarms, bad-quality for attributes); native binding
nodes show active-count or 'no active conditions'. All existing badge helpers
and ValueFormatter reused. Marshalling/dispose/reconnect contract preserved
(SafeInvokeAsync/_disposed/Dispose unchanged; FilteredAttributeValues kept as
the render-thread dict reader the CentralUI-021 race test exercises).
Rework DebugViewAlarmTableTests for the tabbed-tree DOM: tab presence+default,
computed alarm grouped under its Motor1 branch with the active roll-up badge,
and a native condition nested under its source-binding node with the enriched
kind/severity/Unacked/Shelved badge set.