Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Hubs/DashboardSnapshotHub.cs
T
Joseph Doherty 6733519ae5 fix(dashboard): derive gateway status from health checks; send config once per hub connection
SEC-27: DashboardSnapshot.GatewayStatus was hardcoded Healthy. It is now the
aggregate HealthCheckService status, evaluated once per snapshot tick (2s
bound, zero-wait gate) and cached for GetSnapshot; Unknown before the first
evaluation, on failure, or without a health service.

The startup-static EffectiveGatewayConfiguration is JsonIgnore'd on the
snapshot, so /hubs/snapshot ticks carry only volatile state. The hub sends it
once per connection (ConfigurationUpdated) and serves GetConfiguration().
Snapshot broadcasts are time-bounded (1s) so one slow client cannot delay
ticks. StatusBadge maps Unhealthy to Bad.
2026-10-22 06:18:02 -04:00

59 lines
2.6 KiB
C#

using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.SignalR;
using ZB.MOM.WW.MxGateway.Server.Configuration;
namespace ZB.MOM.WW.MxGateway.Server.Dashboard.Hubs;
/// <summary>
/// SignalR hub that pushes a fresh <see cref="DashboardSnapshot"/> on every
/// snapshot refresh. New connections receive the effective configuration and the
/// current snapshot immediately via <see cref="OnConnectedAsync"/>; subsequent
/// refreshes are broadcast by <see cref="DashboardSnapshotPublisher"/> without the
/// configuration, which is startup-static and serialized only once per connection.
/// </summary>
/// <remarks>
/// Connections are counted into <see cref="DashboardSnapshotHubConnectionCounter"/>
/// so <see cref="DashboardSnapshotPublisher"/> can stop building and broadcasting
/// snapshots while nobody is watching.
/// </remarks>
[Authorize(Policy = DashboardAuthenticationDefaults.HubClientsPolicy)]
public sealed class DashboardSnapshotHub(
IDashboardSnapshotService snapshotService,
DashboardSnapshotHubConnectionCounter connectionCounter) : Hub
{
/// <summary>Method name used to push snapshot updates to clients.</summary>
public const string SnapshotMessage = "SnapshotUpdated";
/// <summary>
/// Method name of the one-per-connection effective-configuration push. Snapshot
/// ticks omit the configuration, so a client keeps the copy it got here.
/// </summary>
public const string ConfigurationMessage = "ConfigurationUpdated";
/// <summary>
/// Returns the effective gateway configuration — the same object
/// <see cref="ConfigurationMessage"/> carries — for a client that needs it again.
/// </summary>
/// <returns>The effective configuration.</returns>
public EffectiveGatewayConfiguration GetConfiguration() => snapshotService.GetSnapshot().Configuration;
/// <inheritdoc />
public override async Task OnConnectedAsync()
{
// Count the viewer before seeding it, so the publisher resumes its tick
// no later than the first snapshot this connection renders.
connectionCounter.Increment();
DashboardSnapshot snapshot = snapshotService.GetSnapshot();
await Clients.Caller.SendAsync(ConfigurationMessage, snapshot.Configuration).ConfigureAwait(false);
await Clients.Caller.SendAsync(SnapshotMessage, snapshot).ConfigureAwait(false);
await base.OnConnectedAsync().ConfigureAwait(false);
}
/// <inheritdoc />
public override async Task OnDisconnectedAsync(Exception? exception)
{
connectionCounter.Decrement();
await base.OnDisconnectedAsync(exception).ConfigureAwait(false);
}
}