using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; using Newtonsoft.Json; namespace MMSL.Hubs { // Group convention: "schedulingrun:ou:{workOUId}" — OU-scoped broadcast public class SchedulingRunHub : Hub { private readonly ILogger _logger; public SchedulingRunHub(ILogger logger) { _logger = logger; } public override async Task OnConnectedAsync() { var login = GetLoginDTO(); await Groups.AddToGroupAsync(Context.ConnectionId, HubGroup(login)); _logger.LogInformation("SchedulingRunHub connected: {ConnectionId} OU {WorkOUId}", Context.ConnectionId, login.WorkOUId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var login = GetLoginDTO(); await Groups.RemoveFromGroupAsync(Context.ConnectionId, HubGroup(login)); if (exception is not null) _logger.LogWarning(exception, "SchedulingRunHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } private LoginDTO GetLoginDTO() { var httpContext = Context.GetHttpContext() ?? throw new InvalidOperationException("Hub invoked outside HTTP context."); if (!httpContext.Request.Headers.TryGetValue("Login", out var loginHeader)) throw new InvalidOperationException("Login header is missing from hub request."); return JsonConvert.DeserializeObject(loginHeader!) ?? throw new InvalidOperationException("Login header could not be deserialized."); } public static string HubGroup(LoginDTO login) => $"schedulingrun:ou:{login.WorkOUId}"; public static string HubGroup(int workOUId) => $"schedulingrun:ou:{workOUId}"; } }