fix(admin): stop SignalR hub-connect failure from crashing cluster pages

The Admin-003 fix gated every SignalR hub with [Authorize]/RequireAuthorization,
but the server-side HubConnection clients on ClusterDetail, AclsTab, RedundancyTab
and RoleGrants cannot forward the browser's HttpOnly auth cookie — so the hub
negotiate returns 401. Those four pages called HubConnection.StartAsync()
unguarded, so the 401 surfaced as an unhandled exception (a 500 page for the
prerendered ClusterDetail, a broken circuit for the others).

Wrap StartAsync/SendAsync in try/catch on all four, matching the established
best-effort pattern already used in Hosts.razor and ScriptLog.razor: the live
banner / live refresh degrades but the page renders. Restoring functional hub
live-updates needs a token-based hub auth scheme (cookie forwarding is not
viable across the prerender/interactive boundary) and is left as follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Joseph Doherty
2026-05-22 11:56:06 -04:00
parent bbe292a4b4
commit f2545392e0
4 changed files with 48 additions and 8 deletions

View File

@@ -232,8 +232,18 @@ else
_acls = await AclSvc.ListAsync(GenerationId, CancellationToken.None);
await InvokeAsync(StateHasChanged);
});
await _hub.StartAsync();
await _hub.SendAsync("SubscribeCluster", ClusterId);
// Best-effort: FleetStatusHub requires an authenticated caller, and the server-side
// HubConnection cannot forward the browser auth cookie — swallow connect failures so
// the tab still renders. Live ACL-change updates degrade.
try
{
await _hub.StartAsync();
await _hub.SendAsync("SubscribeCluster", ClusterId);
}
catch
{
// best-effort live updates — see comment above
}
}
public async ValueTask DisposeAsync()

View File

@@ -192,8 +192,18 @@ else
await InvokeAsync(StateHasChanged);
});
await _hub.StartAsync();
await _hub.SendAsync("SubscribeCluster", ClusterId);
// Best-effort: FleetStatusHub requires an authenticated caller, and the server-side
// HubConnection cannot forward the browser auth cookie — a connect failure must not
// crash the page. Live banner updates degrade; the page still renders.
try
{
await _hub.StartAsync();
await _hub.SendAsync("SubscribeCluster", ClusterId);
}
catch
{
// best-effort live updates — see comment above
}
}
private async Task CreateDraftAsync()

View File

@@ -143,8 +143,18 @@ else
await InvokeAsync(StateHasChanged);
});
await _hub.StartAsync();
await _hub.SendAsync("SubscribeCluster", ClusterId);
// Best-effort: FleetStatusHub requires an authenticated caller, and the server-side
// HubConnection cannot forward the browser auth cookie — swallow connect failures so
// the tab still renders. Live role-change updates degrade.
try
{
await _hub.StartAsync();
await _hub.SendAsync("SubscribeCluster", ClusterId);
}
catch
{
// best-effort live updates — see comment above
}
}
public async ValueTask DisposeAsync()

View File

@@ -180,8 +180,18 @@ else
await ReloadAsync();
await InvokeAsync(StateHasChanged);
});
await _hub.StartAsync();
await _hub.SendAsync("SubscribeFleet");
// Best-effort: FleetStatusHub requires an authenticated caller, and the server-side
// HubConnection cannot forward the browser auth cookie — swallow connect failures so
// the page still renders. Live role-grant updates degrade.
try
{
await _hub.StartAsync();
await _hub.SendAsync("SubscribeFleet");
}
catch
{
// best-effort live updates — see comment above
}
}
public async ValueTask DisposeAsync()