fix(central-ui): resolve CentralUI-007..014 — nav authz, UTC date filters, disposal guards, N+1 fix, async script analysis

This commit is contained in:
Joseph Doherty
2026-05-16 20:58:03 -04:00
parent 738e67acc5
commit 71b90ba499
21 changed files with 976 additions and 81 deletions

View File

@@ -293,6 +293,13 @@
private string? _initError;
// CentralUI-009: the stream callbacks (onEvent/onTerminated) run on an
// Akka/gRPC thread and capture `this` and `_toast`. Once the component is
// disposed, an in-flight callback must no-op rather than touch a disposed
// component (InvokeAsync would throw ObjectDisposedException) or a disposed
// ToastNotification.
private volatile bool _disposed;
protected override async Task OnInitializedAsync()
{
try
@@ -396,15 +403,18 @@
_selectedInstanceId,
onEvent: evt =>
{
// CentralUI-009: the component may have been disposed while
// this event was in flight on the Akka/gRPC thread.
if (_disposed) return;
switch (evt)
{
case AttributeValueChanged av:
UpsertWithCap(_attributeValues, av.AttributeName, av);
_ = InvokeAsync(StateHasChanged);
SafeInvokeStateHasChanged();
break;
case AlarmStateChanged al:
UpsertWithCap(_alarmStates, al.AlarmName, al);
_ = InvokeAsync(StateHasChanged);
SafeInvokeStateHasChanged();
break;
}
},
@@ -412,8 +422,11 @@
{
_connected = false;
_session = null;
_ = InvokeAsync(() =>
// CentralUI-009: skip the toast/render if already disposed.
if (_disposed) return;
_ = SafeInvokeAsync(() =>
{
if (_disposed) return;
_toast.ShowError("Debug stream terminated (site disconnected).");
StateHasChanged();
});
@@ -546,8 +559,31 @@
_ => "—"
};
/// <summary>
/// Runs <paramref name="action"/> on the render thread, guarded against the
/// component being disposed mid-flight (CentralUI-009): <c>InvokeAsync</c>
/// throws <see cref="ObjectDisposedException"/> once the circuit is gone.
/// </summary>
private async Task SafeInvokeAsync(Action action)
{
if (_disposed) return;
try
{
await InvokeAsync(action);
}
catch (ObjectDisposedException)
{
// Component disposed between the guard and the dispatch — ignore.
}
}
private void SafeInvokeStateHasChanged() => _ = SafeInvokeAsync(StateHasChanged);
public void Dispose()
{
// CentralUI-009: mark disposed first so any in-flight stream callback
// sees the flag and no-ops, then stop the stream synchronously.
_disposed = true;
if (_session != null)
{
DebugStreamService.StopStream(_session.SessionId);