diff --git a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor index 536bdf1c..b3987202 100644 --- a/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor +++ b/src/ZB.MOM.WW.ScadaBridge.CentralUI/Components/Pages/Monitoring/AlarmSummary.razor @@ -291,6 +291,13 @@ try { var result = await AlarmSummaryService.GetSiteAlarmsAsync(siteId); + // Stale-site guard (arch-review R2 N4): the operator may have switched sites + // while this fan-out was in flight — drop the result rather than labeling + // site A's alarms under site B's picker. Mirrors OnLiveAlarmsChanged (:374). + if (_selectedSiteId != siteId) + { + return; + } _rows = result.Alarms; _notReporting = result.NotReportingInstances; _rollup = AlarmSummaryService.ComputeRollup(_rows); diff --git a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/AlarmSummaryRenderTests.cs b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/AlarmSummaryRenderTests.cs index 51cfefc6..5d253e0b 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/AlarmSummaryRenderTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.CentralUI.Tests/Monitoring/AlarmSummaryRenderTests.cs @@ -208,6 +208,42 @@ public class AlarmSummaryRenderTests : BunitContext Assert.True(_liveCache.DisposeCount >= 1); } + // ── arch-review R2 N4: stale-site poll guard ─────────────────────────────── + + [Fact] + public void InFlightPollForPreviousSite_DoesNotOverwriteNewSitesRows() + { + // Site 1's fan-out hangs on a TCS; site 2 answers immediately (arch-review R2 N4). + var site1Tcs = new TaskCompletionSource(); + _summary.GetSiteAlarmsAsync(1, Arg.Any()).Returns(site1Tcs.Task); + _summary.GetSiteAlarmsAsync(2, Arg.Any()).Returns(Task.FromResult( + new AlarmSummaryResult( + new List + { + new("Site2Instance", new AlarmStateChanged("Site2Instance", "S2-alarm", AlarmState.Active, 100, DateTimeOffset.UtcNow)), + }, + Array.Empty()))); + + var cut = Render(); + cut.Find("[data-test='alarm-summary-site']").Change("1"); // poll in flight, hung + cut.Find("[data-test='alarm-summary-site']").Change("2"); // site 2 renders + cut.WaitForAssertion(() => Assert.Equal("Site2Instance", FirstRowInstance(cut))); + + // The stale site-1 fan-out now completes — it must be dropped, not rendered. + site1Tcs.SetResult(new AlarmSummaryResult( + new List + { + new("StaleSite1", new AlarmStateChanged("StaleSite1", "S1-alarm", AlarmState.Active, 999, DateTimeOffset.UtcNow)), + }, + new[] { "StaleNotReporting" })); + + cut.WaitForAssertion(() => + { + Assert.Equal("Site2Instance", FirstRowInstance(cut)); + Assert.DoesNotContain("StaleNotReporting", cut.Markup); + }); + } + /// /// Controllable in-memory fake of : records /// subscribe/dispose counts and lets a test push a live snapshot (which flips