fix: stabilize route startup render

This commit is contained in:
2026-05-04 22:27:14 +02:00
parent 12612e05fa
commit a69c6284d7
6 changed files with 47 additions and 32 deletions

View File

@@ -17,7 +17,7 @@
<div class="section-head">
<h2>User Management</h2>
</div>
@if (Workspace.State.IsAdminDataLoading)
@if (IsAdminDataLoading)
{
<p class="empty">Loading users...</p>
}
@@ -65,4 +65,6 @@
@code {
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
private bool IsAdminDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsAdminDataLoading;
}

View File

@@ -3,7 +3,7 @@
<main class="play-screen @(Workspace.State.MobilePanel == "log" ? "mobile-log" : "mobile-character")">
<CharacterPanel
IsCampaignDataLoading="Workspace.State.IsCampaignDataLoading"
IsCampaignDataLoading="@IsCampaignDataLoading"
SelectedCampaign="Workspace.State.PlaySelectedCampaign"
SelectedCharacterId="Workspace.State.PlaySelectedCharacterId"
SelectedCharacter="Workspace.State.PlaySelectedCharacter"
@@ -29,7 +29,7 @@
RollRequested="Workspace.Play.RollSkillAsync"/>
<CampaignLogPanel
IsCampaignDataLoading="Workspace.State.IsCampaignDataLoading"
IsCampaignDataLoading="@IsCampaignDataLoading"
CampaignLog="Workspace.State.PlayVisibleCampaignLog"
ExpandedRollId="Workspace.State.ExpandedCampaignLogRollId"
FreshRollId="Workspace.State.FreshCampaignLogRollId"
@@ -72,4 +72,6 @@
@code {
[Parameter, EditorRequired] public WorkspacePageContext Workspace { get; set; } = null!;
private bool IsCampaignDataLoading => !Workspace.HasSessionInitialized || Workspace.State.IsCampaignDataLoading;
}

View File

@@ -14,8 +14,6 @@
}
<div class="workspace-shell">
@if (HasSessionInitialized)
{
<AppHeader
User="State.User"
ShowCampaign="@ShowCampaignInHeader"
@@ -29,15 +27,6 @@
MenuItems="HeaderMenuItems"
ToggleMenuRequested="ToggleScreenMenu"
LogoutRequested="Session.LogoutAsync"/>
}
else
{
<main class="management-screen">
<section class="card">
<p class="empty">Loading workspace...</p>
</section>
</main>
}
@if (ChildContent is not null)
{

View File

@@ -12,6 +12,10 @@ public partial class Workspace : IAsyncDisposable
protected override void OnParametersSet()
{
State.IsScreenMenuOpen = false;
if (PreviousRoute.HasValue && PreviousRoute.Value != Route && HasSessionInitialized)
_ = InvokeAsync(HandleRouteChangedAsync);
PreviousRoute = Route;
}
[JSInvokable]
@@ -114,6 +118,24 @@ public partial class Workspace : IAsyncDisposable
await RequestRefreshAsync();
}
private async Task HandleRouteChangedAsync()
{
if (!HasSessionInitialized)
return;
if (IsAdminRoute)
{
await Live.SyncStateEventsAsync();
await EnsureAdminUsersLoadedAsync();
await RequestRefreshAsync();
return;
}
await Scope.RefreshCampaignScopeAsync();
await Live.SyncStateEventsAsync();
await RequestRefreshAsync();
}
private static bool IsStaticRenderInteropException(InvalidOperationException exception)
{
return exception.Message.Contains("statically rendered", StringComparison.OrdinalIgnoreCase);
@@ -220,4 +242,5 @@ public partial class Workspace : IAsyncDisposable
private WorkspaceCampaignScopeCoordinator? m_Scope;
private WorkspaceSessionCoordinator? m_Session;
private Task? InitializationTask { get; set; }
private WorkspaceRoute? PreviousRoute { get; set; }
}

View File

@@ -1,9 +1,6 @@
@using Microsoft.AspNetCore.Components
@if (Workspace.HasSessionInitialized)
{
@ChildContent(Workspace)
}
@code {
protected override async Task OnAfterRenderAsync(bool firstRender)

View File

@@ -92,6 +92,8 @@ After Milestone 4, authenticated startup is now triggered by a route-owned wrapp
Follow-up: the first pass at Milestone 4 removed shell-level `OnAfterRenderAsync`, but did not yet split `Session.InitializeAsync()` by route. The final follow-up fix made startup genuinely route-scoped by keeping `/admin` off play-only campaign scope and SSE startup, gating the full shell behind authenticated initialization, and adding direct `/admin` smoke coverage so this regression path stays visible.
Follow-up 2: gating the entire authenticated shell behind `HasSessionInitialized` produced another large first-batch subtree swap and broke the `/campaigns` to `/play` refresh path. The final stabilization renders a consistent route skeleton from batch 1, derives loading UI from `HasSessionInitialized` instead of mutating shared loading state in `WorkspaceRouteView`, and refreshes route-specific scope explicitly when the same `Workspace` instance changes from one authenticated route to another.
This section must be updated after each major milestone. When the implementation is complete, summarize which parts of the old workspace architecture were fully removed, which compatibility constraints remain, and whether the final startup path still depends on any multi-batch structural rendering.
## Context and Orientation