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.");
}
}
}