feat(alarm-summary): live-cache-driven updates with poll fallback (plan #10 T5)
Wire the operator Alarm Summary page to the transient per-site live alarm cache (ISiteAlarmLiveCache, T4). Live-cache-first: on site select the page subscribes and rebuilds rows/rollup from near-real-time onChanged deltas; the 15s poll is kept untouched as the authority for NotReporting and as the safety net whenever the cache is not live (pre-seed or degraded stream). Both paths mutate shared state only via the Blazor dispatcher, so they never race, and each rebuild is an idempotent snapshot. - IAlarmSummaryService.BuildFromLiveAlarms: flattens a live AlarmStateChanged snapshot to AlarmSummaryRows with the same deterministic instance-then-name sort as GetSiteAlarmsAsync; NotReporting always empty on the live path. - AlarmSummary.razor: inject ISiteAlarmLiveCache; subscribe on select, re-subscribe on site change, unsubscribe on leave + Dispose. - Tests: service BuildFromLiveAlarms flatten/sort/empty; page subscribe, poll-fallback render, onChanged rebuild, unsubscribe on leave/change/dispose. Claude-Session: https://claude.ai/code/session_01MtdgwpEeCUn6cUA5f1LMPj
This commit is contained in:
@@ -167,4 +167,40 @@ public class AlarmSummaryServiceTests
|
||||
Assert.Equal(0, rollup.WorstSeverity);
|
||||
Assert.Equal(0, rollup.UnackedCount);
|
||||
}
|
||||
|
||||
// ── BuildFromLiveAlarms (plan #10, Task 5) ────────────────────────────────
|
||||
|
||||
[Fact]
|
||||
public void BuildFromLiveAlarms_FlattensAndSorts_SameAsPollPath_WithEmptyNotReporting()
|
||||
{
|
||||
// Intentionally out-of-order input to prove the deterministic
|
||||
// instance-then-alarm-name sort matches GetSiteAlarmsAsync.
|
||||
var alarms = new List<AlarmStateChanged>
|
||||
{
|
||||
NativeAlarm("zeta", "Zulu", AlarmState.Active, 900, active: true, acked: false),
|
||||
NativeAlarm("alpha", "Bravo", AlarmState.Active, 500, active: true, acked: true),
|
||||
NativeAlarm("alpha", "Alpha", AlarmState.Active, 700, active: true, acked: false),
|
||||
};
|
||||
|
||||
var result = CreateSut().BuildFromLiveAlarms(alarms);
|
||||
|
||||
// One row per alarm, instance taken from the alarm itself.
|
||||
Assert.Equal(3, result.Alarms.Count);
|
||||
Assert.Collection(result.Alarms,
|
||||
r => Assert.Equal(("alpha", "Alpha"), (r.InstanceUniqueName, r.Alarm.AlarmName)),
|
||||
r => Assert.Equal(("alpha", "Bravo"), (r.InstanceUniqueName, r.Alarm.AlarmName)),
|
||||
r => Assert.Equal(("zeta", "Zulu"), (r.InstanceUniqueName, r.Alarm.AlarmName)));
|
||||
|
||||
// The live path can't compute "not reporting" — always empty.
|
||||
Assert.Empty(result.NotReportingInstances);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BuildFromLiveAlarms_EmptySnapshot_YieldsNoRows()
|
||||
{
|
||||
var result = CreateSut().BuildFromLiveAlarms(Array.Empty<AlarmStateChanged>());
|
||||
|
||||
Assert.Empty(result.Alarms);
|
||||
Assert.Empty(result.NotReportingInstances);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user