refactor(driver-pages): address Phase 6/8 deep-review findings
v2-ci / build (push) Failing after 32s
v2-ci / unit-tests (tests/Core/ZB.MOM.WW.OtOpcUa.Cluster.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.ControlPlane.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Runtime.Tests) (push) Has been skipped
v2-ci / unit-tests (tests/Server/ZB.MOM.WW.OtOpcUa.Security.Tests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.Host.IntegrationTests) (push) Has been skipped
v2-ci / integration (tests/Server/ZB.MOM.WW.OtOpcUa.OpcUaServer.IntegrationTests) (push) Has been skipped

- Topic-name drift fix: DriverHealthChanged.TopicName and
  DriverControlTopic.Name now live on the message contracts in
  Commons. AkkaDriverHealthPublisher, DriverStatusSignalRBridge,
  DriverHostActor, and AdminOperationsActor all delegate to the
  single constant so a rename can't silently desynchronise
  publisher and subscriber.
- DriverStatusPanel._opResultClearTimer switched from
  System.Timers.Timer to System.Threading.Timer + awaited
  DisposeAsync. Prevents an in-flight 8s clear-callback from
  invoking StateHasChanged on a component whose hub has already
  been released.
- PublishHealthSnapshot deduplicates against the last published
  (state, lastSuccess, lastError, errorCount) fingerprint. The
  30s heartbeat no longer floods the SignalR layer with identical
  Healthy snapshots — newly-joined clients still warm up via the
  snapshot store on JoinDriver.
This commit is contained in:
Joseph Doherty
2026-05-28 11:52:20 -04:00
parent dcd2509548
commit 662f3f9f5c
8 changed files with 50 additions and 19 deletions
@@ -157,7 +157,7 @@
private bool _showRestartConfirm;
private string? _opResultMessage;
private bool _opResultOk;
private System.Timers.Timer? _opResultClearTimer;
private System.Threading.Timer? _opResultClearTimer;
protected override async Task OnInitializedAsync()
{
@@ -257,24 +257,23 @@
{
_opResultOk = ok;
_opResultMessage = message;
// Auto-clear the result chip after 8 s.
// Auto-clear the result chip after 8 s. System.Threading.Timer is used (not
// System.Timers.Timer) so DisposeAsync can drain any in-flight callback.
_opResultClearTimer?.Dispose();
_opResultClearTimer = new System.Timers.Timer(8_000) { AutoReset = false };
_opResultClearTimer.Elapsed += async (_, _) =>
_opResultClearTimer = new System.Threading.Timer(_ =>
{
_opResultMessage = null;
await InvokeAsync(StateHasChanged);
};
_opResultClearTimer.Start();
InvokeAsync(StateHasChanged);
}, null, TimeSpan.FromSeconds(8), Timeout.InfiniteTimeSpan);
}
public async ValueTask DisposeAsync()
{
// Drain the timer first so an in-flight callback can't invoke StateHasChanged on
// a component that's already releasing its hub. System.Threading.Timer implements
// IAsyncDisposable in .NET 6+; the async dispose awaits any in-flight callback.
// Drain BOTH timers first so an in-flight callback can't invoke StateHasChanged on
// a component whose hub has already been released. System.Threading.Timer's async
// dispose awaits any in-flight callback (.NET 6+).
if (_timer is not null) await _timer.DisposeAsync();
_opResultClearTimer?.Dispose();
if (_opResultClearTimer is not null) await _opResultClearTimer.DisposeAsync();
if (_hub is not null) await _hub.DisposeAsync();
}
@@ -15,7 +15,7 @@ namespace ZB.MOM.WW.OtOpcUa.AdminUI.Hubs;
/// </summary>
public sealed class DriverStatusSignalRBridge : ReceiveActor
{
public const string TopicName = "driver-health";
public const string TopicName = DriverHealthChanged.TopicName;
private readonly IHubContext<DriverStatusHub> _hub;
private readonly IDriverStatusSnapshotStore _store;