fix(central-ui): resolve CentralUI-006 — push-based deployment status via IDeploymentStatusNotifier, remove 10s polling timer

This commit is contained in:
Joseph Doherty
2026-05-17 00:02:45 -04:00
parent a55502254e
commit 34588ae10c
11 changed files with 459 additions and 36 deletions

View File

@@ -8,6 +8,7 @@
@inject IDeploymentManagerRepository DeploymentManagerRepository
@inject ITemplateEngineRepository TemplateEngineRepository
@inject ScadaLink.CentralUI.Auth.SiteScopeService SiteScope
@inject ScadaLink.DeploymentManager.IDeploymentStatusNotifier DeploymentStatusNotifier
@implements IDisposable
<div class="container-fluid mt-3">
@@ -196,47 +197,42 @@
private Dictionary<int, string> _instanceNames = new();
private bool _loading = true;
private string? _errorMessage;
private Timer? _refreshTimer;
private bool _autoRefresh = true;
private readonly HashSet<string> _expandedErrors = new();
private int _currentPage = 1;
private int _totalPages;
private const int PageSize = 25;
private static readonly TimeSpan RefreshInterval = TimeSpan.FromSeconds(10);
// CentralUI-006: deployment status updates are push-based, not polled.
// DeploymentManager raises IDeploymentStatusNotifier.StatusChanged on every
// deployment-record status write; this page subscribes to it and reloads,
// and Blazor Server pushes the re-render to the browser over its SignalR
// circuit — satisfying the design's "no polling required" requirement.
// The notifier event is raised on the DeploymentManager service thread, so
// the handler marshals onto the renderer via InvokeAsync.
protected override async Task OnInitializedAsync()
{
await LoadDataAsync();
StartTimer();
DeploymentStatusNotifier.StatusChanged += OnDeploymentStatusChanged;
}
private void StartTimer()
private void OnDeploymentStatusChanged(ScadaLink.DeploymentManager.DeploymentStatusChange change)
{
_refreshTimer?.Dispose();
_refreshTimer = new Timer(_ =>
if (!_autoRefresh) return;
_ = InvokeAsync(async () =>
{
InvokeAsync(async () =>
{
if (!_autoRefresh) return;
await LoadDataAsync();
StateHasChanged();
});
}, null, RefreshInterval, RefreshInterval);
await LoadDataAsync();
StateHasChanged();
});
}
private void ToggleAutoRefresh()
{
// When paused, incoming push notifications are ignored; "Refresh" still
// forces a manual reload. No timer is involved either way.
_autoRefresh = !_autoRefresh;
if (_autoRefresh)
{
StartTimer();
}
else
{
_refreshTimer?.Dispose();
_refreshTimer = null;
}
}
private bool IsErrorExpanded(string deploymentId) => _expandedErrors.Contains(deploymentId);
@@ -320,6 +316,8 @@
public void Dispose()
{
_refreshTimer?.Dispose();
// Unsubscribe so a status change after the circuit is gone does not
// touch a disposed component (the notifier is a process singleton).
DeploymentStatusNotifier.StatusChanged -= OnDeploymentStatusChanged;
}
}