using GB5Shared.DTO.Framework.Login; using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace FrameworkSL.Hubs.SysJob { /// /// SysJobHub — server-push hub for real-time SysJob execution progress. /// /// Clients connect and are placed into a tenant-scoped group. /// SysJobExecutorQuartzJob injects IHubContext<SysJobHub, ISysJobClient> to push /// job lifecycle events (started / completed / retrying / failed) to all connected /// clients for that tenant. /// /// No client-initiated methods — this hub is server-push only. /// public class SysJobHub(ILogger _logger) : Hub { // ── Group naming ────────────────────────────────────────────────── /// Tenant-scoped group — all connected clients for a given client/tenant. internal static string TenantGroup(int clientId) => $"sysjob:client:{clientId}"; /// User-scoped group — used for targeted pushes (e.g. JobShared notifications). internal static string UserGroup(int userId) => $"sysjob:user:{userId}"; // ── Connection lifecycle ────────────────────────────────────────── public override async Task OnConnectedAsync() { var login = GetLoginDTO(); await Groups.AddToGroupAsync(Context.ConnectionId, TenantGroup(login.ClientId)); await Groups.AddToGroupAsync(Context.ConnectionId, UserGroup(login.UserId)); _logger.LogInformation("SysJobHub connected: {ConnectionId} tenant {ClientId} user {UserId}", Context.ConnectionId, login.ClientId, login.UserId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { var login = GetLoginDTO(); await Groups.RemoveFromGroupAsync(Context.ConnectionId, TenantGroup(login.ClientId)); await Groups.RemoveFromGroupAsync(Context.ConnectionId, UserGroup(login.UserId)); if (exception is not null) _logger.LogWarning(exception, "SysJobHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } // ── Helper ──────────────────────────────────────────────────────── 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."); } } }