From f2545392e031b66776c0aa2081c7fbc0c48ca399 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Fri, 22 May 2026 11:56:06 -0400 Subject: [PATCH] fix(admin): stop SignalR hub-connect failure from crashing cluster pages MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- .../Components/Pages/Clusters/AclsTab.razor | 14 ++++++++++++-- .../Components/Pages/Clusters/ClusterDetail.razor | 14 ++++++++++++-- .../Components/Pages/Clusters/RedundancyTab.razor | 14 ++++++++++++-- .../Components/Pages/RoleGrants.razor | 14 ++++++++++++-- 4 files changed, 48 insertions(+), 8 deletions(-) diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/AclsTab.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/AclsTab.razor index a599117..5fe7c5c 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/AclsTab.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/AclsTab.razor @@ -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() diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ClusterDetail.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ClusterDetail.razor index 4be8f20..afd02d4 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ClusterDetail.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/ClusterDetail.razor @@ -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() diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/RedundancyTab.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/RedundancyTab.razor index 655113d..ad7240a 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/RedundancyTab.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/Clusters/RedundancyTab.razor @@ -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() diff --git a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/RoleGrants.razor b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/RoleGrants.razor index e24e50d..68f3f27 100644 --- a/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/RoleGrants.razor +++ b/src/Server/ZB.MOM.WW.OtOpcUa.Admin/Components/Pages/RoleGrants.razor @@ -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()