chore: add workspace crash diagnostics

This commit is contained in:
2026-05-04 22:43:57 +02:00
parent a69c6284d7
commit e60b4b5867
22 changed files with 662 additions and 112 deletions

View File

@@ -1,5 +1,6 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.AspNetCore.Components;
using Microsoft.Extensions.Logging;
using Microsoft.JSInterop;
using RpgRoller.Components.Pages.HomeControls;
using RpgRoller.Contracts;
@@ -9,8 +10,16 @@ namespace RpgRoller.Components.Pages;
[ExcludeFromCodeCoverage]
public partial class Workspace : IAsyncDisposable
{
protected override void OnInitialized()
{
Logger.LogInformation("Workspace.OnInitialized route={Route}", Route);
}
protected override void OnParametersSet()
{
Logger.LogInformation(
"Workspace.OnParametersSet route={Route} previousRoute={PreviousRoute} hasSessionInitialized={HasSessionInitialized} state=[{State}]",
Route, PreviousRoute, HasSessionInitialized, WorkspaceDiagnosticSummary.DescribeState(State));
State.IsScreenMenuOpen = false;
if (PreviousRoute.HasValue && PreviousRoute.Value != Route && HasSessionInitialized)
_ = InvokeAsync(HandleRouteChangedAsync);
@@ -18,20 +27,34 @@ public partial class Workspace : IAsyncDisposable
PreviousRoute = Route;
}
protected override Task OnAfterRenderAsync(bool firstRender)
{
RenderCount += 1;
Logger.LogInformation(
"Workspace.OnAfterRenderAsync route={Route} renderCount={RenderCount} firstRender={FirstRender} hasSessionInitialized={HasSessionInitialized} state=[{State}]",
Route, RenderCount, firstRender, HasSessionInitialized, WorkspaceDiagnosticSummary.DescribeState(State));
return Task.CompletedTask;
}
[JSInvokable]
public Task OnStateEventReceived(CampaignStateSnapshot state)
{
Logger.LogInformation("Workspace.OnStateEventReceived route={Route} snapshot=[{Snapshot}]",
Route, WorkspaceDiagnosticSummary.DescribeSnapshot(state));
return Live.OnStateEventReceivedAsync(state);
}
[JSInvokable]
public Task OnConnectionStateChanged(string state)
{
Logger.LogInformation("Workspace.OnConnectionStateChanged route={Route} state={ConnectionState}", Route, state);
return Live.OnConnectionStateChangedAsync(state);
}
public async ValueTask DisposeAsync()
{
Logger.LogInformation("Workspace.DisposeAsync route={Route} state=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
await StopStateEventsAsync();
DotNetRef?.Dispose();
}
@@ -58,12 +81,15 @@ public partial class Workspace : IAsyncDisposable
private async Task StartStateEventsCoreAsync(Guid campaignId)
{
Logger.LogInformation("Workspace.StartStateEventsCoreAsync route={Route} campaignId={CampaignId}", Route,
campaignId);
DotNetRef ??= DotNetObjectReference.Create(this);
await JS.InvokeVoidAsync("rpgRollerApi.startStateEvents", campaignId.ToString(), DotNetRef);
}
private async Task StopStateEventsCoreAsync()
{
Logger.LogInformation("Workspace.StopStateEventsCoreAsync route={Route}", Route);
try
{
await JS.InvokeVoidAsync("rpgRollerApi.stopStateEvents");
@@ -83,6 +109,8 @@ public partial class Workspace : IAsyncDisposable
private Task NavigateToRouteAsync(string route)
{
Logger.LogInformation("Workspace.NavigateToRouteAsync fromRoute={Route} toRoute={TargetRoute} state=[{State}]",
Route, route, WorkspaceDiagnosticSummary.DescribeState(State));
State.IsScreenMenuOpen = false;
Navigation.NavigateTo(route);
return InvokeAsync(StateHasChanged);
@@ -93,13 +121,27 @@ public partial class Workspace : IAsyncDisposable
if (IsPlayRoute)
return Task.CompletedTask;
Logger.LogWarning("Workspace.RedirectToPlayAsync fromRoute={Route} state=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
Navigation.NavigateTo("/play");
return Task.CompletedTask;
}
private Task RequestRefreshAsync()
{
return InvokeAsync(StateHasChanged);
return RequestRefreshAsync("Workspace");
}
private Task RequestRefreshAsync(string source)
{
Logger.LogInformation("Workspace.RequestRefreshAsync source={Source} route={Route} state=[{State}]",
source, Route, WorkspaceDiagnosticSummary.DescribeState(State));
return InvokeAsync(() =>
{
Logger.LogInformation("Workspace.StateHasChanged source={Source} route={Route} state=[{State}]",
source, Route, WorkspaceDiagnosticSummary.DescribeState(State));
StateHasChanged();
});
}
private Task InitializeRouteAsync()
@@ -110,11 +152,20 @@ public partial class Workspace : IAsyncDisposable
private async Task InitializeRouteCoreAsync()
{
if (HasSessionInitialized)
{
Logger.LogInformation(
"Workspace.InitializeRouteCoreAsync skipped route={Route} alreadyInitialized state=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
return;
}
Logger.LogInformation("Workspace.InitializeRouteCoreAsync start route={Route} stateBefore=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
State.HasInteractiveRenderStarted = true;
await Session.InitializeAsync();
HasSessionInitialized = true;
Logger.LogInformation("Workspace.InitializeRouteCoreAsync end route={Route} stateAfter=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
await RequestRefreshAsync();
}
@@ -123,16 +174,23 @@ public partial class Workspace : IAsyncDisposable
if (!HasSessionInitialized)
return;
Logger.LogInformation(
"Workspace.HandleRouteChangedAsync start route={Route} previousRoute={PreviousRoute} stateBefore=[{State}]",
Route, PreviousRoute, WorkspaceDiagnosticSummary.DescribeState(State));
if (IsAdminRoute)
{
await Live.SyncStateEventsAsync();
await EnsureAdminUsersLoadedAsync();
Logger.LogInformation("Workspace.HandleRouteChangedAsync admin end route={Route} stateAfter=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
await RequestRefreshAsync();
return;
}
await Scope.RefreshCampaignScopeAsync();
await Live.SyncStateEventsAsync();
Logger.LogInformation("Workspace.HandleRouteChangedAsync end route={Route} stateAfter=[{State}]",
Route, WorkspaceDiagnosticSummary.DescribeState(State));
await RequestRefreshAsync();
}
@@ -148,6 +206,8 @@ public partial class Workspace : IAsyncDisposable
[Inject] private WorkspaceQueryService WorkspaceQuery { get; set; } = null!;
[Inject] private NavigationManager Navigation { get; set; } = null!;
[Inject] private ILogger<Workspace> Logger { get; set; } = null!;
[Inject] private ILoggerFactory LoggerFactory { get; set; } = null!;
[Parameter] public EventCallback<string?> LoggedOut { get; set; }
[Parameter] public WorkspaceRoute Route { get; set; } = WorkspaceRoute.Play;
@@ -170,16 +230,18 @@ public partial class Workspace : IAsyncDisposable
() => IsPlayRoute, Play.EnsureSelectedCharacterActiveAsync, Play.RefreshSelectedCharacterSheetAsync,
Play.RefreshCampaignLogAsync, Play.ResetCampaignLogDetailState, Play.ResetCampaignStateTracking,
ClearAuthenticatedState,
StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
StopStateEventsAsync, message => LoggedOut.InvokeAsync(message),
LoggerFactory.CreateLogger("Workspace.CampaignScope"));
private WorkspaceLiveStateController Live => m_Live ??= new(State, Feedback, () => IsPlayRoute, () => IsAdminRoute,
StartStateEventsCoreAsync,
StopStateEventsCoreAsync, Scope.RefreshCampaignRosterAsync, Play.RefreshSelectedCharacterSheetAsync,
Play.RefreshCampaignLogAsync, () => InvokeAsync(StateHasChanged));
Play.RefreshCampaignLogAsync, () => RequestRefreshAsync("WorkspaceLiveStateController"),
LoggerFactory.CreateLogger("Workspace.LiveState"));
private WorkspacePlayCoordinator Play => m_Play ??= new(State, Feedback, () => IsPlayRoute, ApiClient,
WorkspaceQuery,
CanEditCharacter, () => InvokeAsync(StateHasChanged));
CanEditCharacter, () => RequestRefreshAsync("WorkspacePlayCoordinator"));
private WorkspaceCampaignCoordinator Campaigns => m_Campaigns ??= new(State, Feedback, JS, ApiClient,
Session.LoadKnownUsernamesAsync, Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync,
@@ -188,13 +250,15 @@ public partial class Workspace : IAsyncDisposable
private WorkspaceAdminCoordinator Admin => m_Admin ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
ClearAuthenticatedState, StopStateEventsAsync, message => LoggedOut.InvokeAsync(message));
private WorkspaceFeedbackService Feedback => m_Feedback ??= new(State, () => InvokeAsync(StateHasChanged));
private WorkspaceFeedbackService Feedback =>
m_Feedback ??= new(State, () => RequestRefreshAsync("WorkspaceFeedbackService"));
private WorkspaceSessionCoordinator Session => m_Session ??= new(State, Feedback, JS, ApiClient, WorkspaceQuery,
() => IsAdminRoute, RedirectToPlayAsync,
Scope.ReloadCampaignsAsync, Scope.ReloadCharacterCampaignOptionsAsync, Scope.RefreshCampaignScopeAsync,
Live.SyncStateEventsAsync, Live.StopStateEventsAsync, EnsureAdminUsersLoadedAsync,
Play.ResetCampaignLogDetailState, message => LoggedOut.InvokeAsync(message));
Play.ResetCampaignLogDetailState, message => LoggedOut.InvokeAsync(message),
LoggerFactory.CreateLogger("Workspace.Session"));
private IReadOnlyList<AppHeaderMenuItem> HeaderMenuItems
{
@@ -243,4 +307,5 @@ public partial class Workspace : IAsyncDisposable
private WorkspaceSessionCoordinator? m_Session;
private Task? InitializationTask { get; set; }
private WorkspaceRoute? PreviousRoute { get; set; }
}
private int RenderCount { get; set; }
}