using Microsoft.AspNetCore.SignalR; namespace ZB.MOM.WW.OtOpcUa.Admin.Hubs; /// /// Pushes per-node generation-apply state changes (ClusterNodeGenerationState) to /// subscribed browser clients. Clients call SubscribeCluster(clusterId) on connect to /// scope notifications; the server sends NodeStateChanged messages whenever the poller /// observes a delta. /// public sealed class FleetStatusHub : Hub { public Task SubscribeCluster(string clusterId) { if (string.IsNullOrWhiteSpace(clusterId)) return Task.CompletedTask; return Groups.AddToGroupAsync(Context.ConnectionId, GroupName(clusterId)); } public Task UnsubscribeCluster(string clusterId) { if (string.IsNullOrWhiteSpace(clusterId)) return Task.CompletedTask; return Groups.RemoveFromGroupAsync(Context.ConnectionId, GroupName(clusterId)); } /// Clients call this once to also receive fleet-wide status — used by the dashboard. public Task SubscribeFleet() => Groups.AddToGroupAsync(Context.ConnectionId, FleetGroup); public const string FleetGroup = "__fleet__"; public static string GroupName(string clusterId) => $"cluster:{clusterId}"; } public sealed record NodeStateChangedMessage( string NodeId, string ClusterId, long? CurrentGenerationId, string? LastAppliedStatus, string? LastAppliedError, DateTime? LastAppliedAt, DateTime? LastSeenAt); /// /// Pushed by FleetStatusPoller when it observes a change in a /// DriverInstanceResilienceStatus row. Closes the last Phase 6.1 Stream E.2/E.3 /// deferral — lets the Admin /hosts page upsert the matching row without the /// 10-second polling round-trip. Keyed on (DriverInstanceId, HostName); the client /// fan-outs to the matching row by matching both. /// public sealed record ResilienceStatusChangedMessage( string DriverInstanceId, string HostName, int ConsecutiveFailures, DateTime? LastCircuitBreakerOpenUtc, int CurrentBulkheadDepth, DateTime? LastRecycleUtc);