using Microsoft.AspNetCore.SignalR; using Microsoft.Extensions.Logging; namespace EntitlementSL.Hubs; // Real-time client-onboarding status push (tracker §37 Decision 2) — modeled directly on // GB5Framework's FrameworkSL/Hubs/DataSync/DataSyncHub.cs (same group-per-subject pattern, // same bare connection-lifecycle logging, no per-connection auth gate — this hub carries only // onboarding-progress state, never a credential or PII). EntitlementSL's first-ever SignalR hub. public class ProvisioningHub : Hub { private readonly ILogger _logger; public ProvisioningHub(ILogger logger) { _logger = logger; } // ── Connection lifecycle ────────────────────────────────────────────── public override async Task OnConnectedAsync() { _logger.LogInformation("ProvisioningHub connected: {ConnectionId}", Context.ConnectionId); await base.OnConnectedAsync(); } public override async Task OnDisconnectedAsync(Exception? exception) { _logger.LogInformation("ProvisioningHub disconnected: {ConnectionId}", Context.ConnectionId); if (exception is not null) _logger.LogWarning(exception, "ProvisioningHub disconnected with error: {ConnectionId}", Context.ConnectionId); await base.OnDisconnectedAsync(exception); } // ── Hub methods ─────────────────────────────────────────────────────── // Client calls this to subscribe to progress for its own onboarding job. // Group name: "provisioning:{clientId}" public async Task JoinClientGroup(int clientId) { var group = ClientGroup(clientId); await Groups.AddToGroupAsync(Context.ConnectionId, group); _logger.LogInformation("Connection {ConnectionId} joined group {Group}", Context.ConnectionId, group); } public async Task LeaveClientGroup(int clientId) { await Groups.RemoveFromGroupAsync(Context.ConnectionId, ClientGroup(clientId)); } // ── Helpers ─────────────────────────────────────────────────────────── public static string ClientGroup(int clientId) => $"provisioning:{clientId}"; }