using Microsoft.JSInterop; using RpgRoller.Contracts; namespace RpgRoller.Components.Pages; public sealed class WorkspaceSessionCoordinator( WorkspaceState state, WorkspaceFeedbackService feedback, IJSRuntime js, RpgRollerApiClient apiClient, WorkspaceQueryService workspaceQuery, Func isAdminRoute, Func redirectToPlayAsync, Func reloadCampaignsAsync, Func reloadCharacterCampaignOptionsAsync, Func refreshCampaignScopeAsync, Func requestRefreshAsync, Func syncStateEventsAsync, Func stopStateEventsAsync, Func ensureAdminUsersLoadedAsync, Action resetCampaignLogDetailState, Func onLoggedOutAsync) { public async Task InitializeAsync() { var storedPanel = await js.InvokeAsync("rpgRollerApi.getSessionValue", MobilePanelSessionKey); if (string.Equals(storedPanel, "log", StringComparison.OrdinalIgnoreCase)) state.MobilePanel = "log"; var storedRollVisibility = await js.InvokeAsync("rpgRollerApi.getSessionValue", RollVisibilitySessionKey); state.RollVisibility = NormalizeRollVisibility(storedRollVisibility); Guid? preferredCampaignId = null; if (!isAdminRoute()) { var storedCampaignId = await js.InvokeAsync("rpgRollerApi.getSessionValue", CampaignSessionKey); if (Guid.TryParse(storedCampaignId, out var parsedCampaignId)) preferredCampaignId = parsedCampaignId; } await CheckHealthAsync(); var reloaded = await ReloadAuthenticatedSessionAsync(preferredCampaignId); if (!reloaded) await onLoggedOutAsync("Session expired. Please log in again."); } public async Task RetryAfterHealthIssueAsync() { await CheckHealthAsync(); if (!state.HasHealthIssue && state.User is not null) { var reloaded = await ReloadAuthenticatedSessionAsync(state.SelectedCampaignId); if (!reloaded) await onLoggedOutAsync("Session expired. Please log in again."); } } public async Task LoadKnownUsernamesAsync() { try { var usernames = await workspaceQuery.GetUsernamesAsync(); state.KnownUsernames = usernames.OrderBy(username => username, StringComparer.OrdinalIgnoreCase).ToList(); } catch (ApiRequestException ex) { state.KnownUsernames = []; feedback.SetStatus(ex.Message, true); } } public async Task LogoutAsync() { if (state.IsMutating) return; state.IsMutating = true; try { await apiClient.RequestWithoutPayloadAsync("POST", "/api/auth/logout"); } catch (ApiRequestException) { } finally { state.IsMutating = false; } ClearAuthenticatedState(); await stopStateEventsAsync(); await onLoggedOutAsync("Logged out."); } public async Task OnRollVisibilityChangedAsync(string visibility) { state.RollVisibility = NormalizeRollVisibility(visibility); await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", RollVisibilitySessionKey, state.RollVisibility); await requestRefreshAsync(); } public void ClearAuthenticatedState() { state.User = null; state.ActiveCharacterId = null; state.SelectedCampaignId = null; state.SelectedCampaign = null; state.Campaigns = []; state.CharacterCampaignOptions = []; state.SelectedCharacterSkills = []; state.SelectedCharacterSkillGroups = []; state.CampaignLog = []; state.CampaignLogCursor = null; resetCampaignLogDetailState(); state.SelectedCharacterId = null; state.LastRoll = null; state.KnownUsernames = []; state.ShowCreateCharacterModal = false; state.ShowEditCharacterModal = false; state.CanEditCharacterOwner = false; state.CreateCharacterInitialModel = new(); state.EditCharacterInitialModel = new(); state.CreateCharacterFormVersion = 0; state.EditCharacterFormVersion = 0; state.AdminUsers = []; state.HasLoadedAdminUsers = false; state.IsAdminDataLoading = false; feedback.ClearToasts(); } private async Task CheckHealthAsync() { state.HasHealthIssue = false; state.HealthIssueMessage = string.Empty; await Task.CompletedTask; } private async Task LoadRulesetsAsync() { try { state.Rulesets = (await workspaceQuery.GetRulesetsAsync()).ToList(); } catch (ApiRequestException ex) { feedback.SetStatus(ex.Message, true); } } private async Task ReloadAuthenticatedSessionAsync(Guid? preferredCampaignId) { var me = await TryGetMeAsync(); if (me is null) { ClearAuthenticatedState(); await stopStateEventsAsync(); return false; } state.User = me.User; state.ActiveCharacterId = me.ActiveCharacterId; if (!await EnsureRouteAccessAsync()) return true; if (isAdminRoute()) { await stopStateEventsAsync(); state.ConnectionState = "offline"; await ensureAdminUsersLoadedAsync(); return true; } await LoadRulesetsAsync(); await reloadCampaignsAsync(preferredCampaignId ?? me.CurrentCampaignId); await reloadCharacterCampaignOptionsAsync(); await refreshCampaignScopeAsync(); await syncStateEventsAsync(); return true; } private async Task TryGetMeAsync() { try { return await workspaceQuery.GetMeAsync(); } catch (ApiRequestException ex) when (ex.StatusCode == 401) { return null; } } private async Task EnsureRouteAccessAsync() { if (state.IsCurrentUserAdmin || !isAdminRoute()) { return true; } state.AdminUsers = []; state.HasLoadedAdminUsers = false; await redirectToPlayAsync(); return false; } private static string NormalizeRollVisibility(string? visibility) { return string.Equals(visibility, "private", StringComparison.OrdinalIgnoreCase) ? "private" : "public"; } private const string CampaignSessionKey = "campaign"; private const string MobilePanelSessionKey = "play-panel"; private const string RollVisibilitySessionKey = "roll-visibility"; }