using Microsoft.AspNetCore.SignalR; using ZB.MOM.WW.OtOpcUa.Admin.Hubs; namespace ZB.MOM.WW.OtOpcUa.Admin.Services; /// /// Thin SignalR push helper for ACL + role-grant invalidation — slice 2 of task #196. /// Lets the Admin services + razor pages invalidate connected peers' views without each /// one having to know the hub wiring. Two message kinds: NodeAclChanged (cluster-scoped) /// and RoleGrantsChanged (fleet-wide — role mappings cross cluster boundaries). /// /// /// Intentionally fire-and-forget — a failed hub send doesn't rollback the DB write that /// triggered it. Worst-case an operator sees stale data until their next poll or manual /// refresh; better than a transient hub blip blocking the authoritative write path. /// public sealed class AclChangeNotifier(IHubContext fleetHub, ILogger logger) { public async Task NotifyNodeAclChangedAsync(string clusterId, long generationId, CancellationToken ct) { try { var msg = new NodeAclChangedMessage(ClusterId: clusterId, GenerationId: generationId, ObservedAtUtc: DateTime.UtcNow); await fleetHub.Clients.Group(FleetStatusHub.GroupName(clusterId)) .SendAsync("NodeAclChanged", msg, ct).ConfigureAwait(false); } catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning(ex, "NodeAclChanged push failed for cluster {ClusterId} gen {GenerationId}", clusterId, generationId); } } public async Task NotifyRoleGrantsChangedAsync(CancellationToken ct) { try { var msg = new RoleGrantsChangedMessage(ObservedAtUtc: DateTime.UtcNow); await fleetHub.Clients.Group(FleetStatusHub.FleetGroup) .SendAsync("RoleGrantsChanged", msg, ct).ConfigureAwait(false); } catch (Exception ex) when (ex is not OperationCanceledException) { logger.LogWarning(ex, "RoleGrantsChanged push failed"); } } } public sealed record NodeAclChangedMessage(string ClusterId, long GenerationId, DateTime ObservedAtUtc); public sealed record RoleGrantsChangedMessage(DateTime ObservedAtUtc);