using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; namespace AutomationSL.Hubs; // Live log-tail hub for RunDetailComponent — built once as a persona-agnostic hub (Admin/Dev/ // PowerUser all connect to the same hub and just join the run group(s) they're viewing), per the // spec's own advice not to build three separate log viewers. public class AutomationRunHub : Hub { private readonly ILogger _logger; public AutomationRunHub(ILogger logger) => _logger = logger; public override async Task OnConnectedAsync() { var login = GetLoginDTO(); await Groups.AddToGroupAsync(Context.ConnectionId, TenantGroup(login.ClientId)); _logger.LogInformation("AutomationRunHub connected: {ConnectionId} ClientId {ClientId}", Context.ConnectionId, login.ClientId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { if (exception is not null) _logger.LogWarning(exception, "AutomationRunHub disconnected with error: {ConnectionId}", Context.ConnectionId); // Per-run groups joined via JoinRunGroup are torn down automatically by SignalR's default // in-memory group manager when the connection is removed — no manual per-run bookkeeping // needed here (unlike the tenant group, which is fixed for the connection's lifetime). await base.OnDisconnectedAsync(exception); } // Client calls this once it knows which run it's watching (e.g. opening RunDetailComponent) — // OnConnectedAsync alone can't know the RunId, unlike a fixed per-session/OU group. public async Task JoinRunGroup(long runId) => await Groups.AddToGroupAsync(Context.ConnectionId, RunGroup(runId)); public async Task LeaveRunGroup(long runId) => await Groups.RemoveFromGroupAsync(Context.ConnectionId, RunGroup(runId)); private LoginDTO GetLoginDTO() { var http = Context.GetHttpContext() ?? throw new InvalidOperationException("Hub invoked outside HTTP context."); var raw = http.Request.Headers["Login"].FirstOrDefault() ?? throw new InvalidOperationException("Login header missing."); return System.Text.Json.JsonSerializer.Deserialize(raw) ?? throw new InvalidOperationException("Login header is invalid JSON."); } public static string RunGroup(long runId) => $"automation:run:{runId}"; public static string TenantGroup(int clientId) => $"automation:client:{clientId}"; }