fix: use per-page blazor startup
This commit is contained in:
12
README.md
12
README.md
@@ -36,7 +36,7 @@ Backend:
|
||||
|
||||
Frontend:
|
||||
|
||||
- `RpgRoller/Components/App.razor`: HTML shell that serves the static `/login` auth document or the interactive route set based on request path
|
||||
- `RpgRoller/Components/App.razor`: HTML shell that serves the static `/login` auth document or the per-page interactive authenticated route set based on request path
|
||||
- `RpgRoller/Components/Routes.razor`: Blazor router and layout hookup
|
||||
- `RpgRoller/Components/Layout/MainLayout.razor`: default layout
|
||||
- `RpgRoller/Components/Pages/LoginPage.razor`: route marker for the static `/login` auth document
|
||||
@@ -108,6 +108,13 @@ The frontend now uses a route-first authenticated shell that keeps the anonymous
|
||||
|
||||
Inside the authenticated app, `/play`, `/campaigns`, and `/admin` are real Blazor routes, and the hamburger menu navigates between those URLs. `Workspace.razor` is now a shared shell only. Each authenticated route owns its own main content subtree through a route-specific component.
|
||||
|
||||
Authenticated interactivity is route-local instead of global:
|
||||
|
||||
- `App.razor` no longer applies `@rendermode` to `Routes` or `HeadOutlet`
|
||||
- `PlayPage.razor`, `CampaignsPage.razor`, and `AdminPage.razor` each opt into `InteractiveServerRenderMode(prerender: false)` directly
|
||||
- Blazor startup is manual with `Blazor.start({ ssr: { disableDomPreservation: true } })` so the app can disable enhanced SSR DOM preservation during interactive attach
|
||||
- Header route changes now use full document navigation so moving between authenticated routes remounts the target per-page interactive root instead of trying to reuse the previous page circuit
|
||||
|
||||
Interactive bootstrap is now route-local:
|
||||
|
||||
- `WorkspaceRouteView.razor` performs the first-render JS-dependent session initialization for the authenticated route that mounted
|
||||
@@ -190,12 +197,13 @@ SQLite migration rule:
|
||||
|
||||
## Frontend Runtime
|
||||
|
||||
- The UI runs as Blazor Server for authenticated routes and as plain HTML plus JavaScript for the anonymous `/login` document.
|
||||
- The UI runs as route-local Blazor Server components for authenticated routes and as plain HTML plus JavaScript for the anonymous `/login` document.
|
||||
- Static assets are linked through Blazor's `@Assets[...]` pipeline for fingerprinted cache-busting URLs.
|
||||
- Workspace reads are resolved through API requests in `WorkspaceQueryService`; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers.
|
||||
- Interactive authenticated startup begins in `WorkspaceRouteView.razor` after first render because `RpgRollerApiClient` still depends on JS interop-backed `fetch`.
|
||||
- Workspace startup diagnostics now log route initialization, route-content render phases, and browser-side workspace mutation snapshots to help isolate the remaining Firefox startup crash documented in `POSTMORTEM.md`.
|
||||
- Pre-Blazor diagnostics also watch the static `#rr-interactive-host` container before `_framework/blazor.web.js` connects, so extension-driven DOM mutations can be compared against the first failing interactive batch.
|
||||
- Authenticated routes now avoid global `Routes @rendermode` because upstream issue `dotnet/aspnetcore#58824` reports Firefox-specific failures with global interactivity and explicitly calls out per-page mode as the safer path.
|
||||
- Authenticated routes now mount through phased interactive batches: minimal shell first, then a simple header placeholder, then route skeletons, and only then the real header and control-heavy route content after route initialization succeeds.
|
||||
- Live workspace refresh compares separate roster, per-character sheet, and log versions so unrelated changes do not trigger full reloads.
|
||||
- Campaign log data is loaded in bounded slices: campaign summaries, one selected roster, one selected character sheet, and a 25-row incremental log window from `/api/campaigns/{campaignId}/log/page`.
|
||||
|
||||
@@ -54,6 +54,8 @@ public sealed class FrontendHostTests(WebApplicationFactory<Program> factory) :
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var html = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains("_framework/blazor.web.js", html);
|
||||
Assert.Contains("autostart=\"false\"", html);
|
||||
Assert.Contains("disableDomPreservation", html);
|
||||
Assert.DoesNotContain("data-auth-page", html);
|
||||
}
|
||||
}
|
||||
@@ -15,7 +15,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Color+Emoji&display=swap" rel="stylesheet">
|
||||
@if (UseInteractiveApp)
|
||||
{
|
||||
<HeadOutlet @rendermode="@(new InteractiveServerRenderMode(prerender: false))"/>
|
||||
<HeadOutlet/>
|
||||
}
|
||||
</head>
|
||||
<body>
|
||||
@@ -26,7 +26,7 @@
|
||||
else
|
||||
{
|
||||
<div id="rr-interactive-host" data-request-path="@RequestPath">
|
||||
<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))"/>
|
||||
<Routes/>
|
||||
</div>
|
||||
}
|
||||
<script src="js/rpgroller-api.js"></script>
|
||||
@@ -35,7 +35,14 @@ else
|
||||
<script>
|
||||
window.rpgRollerApi.bootstrapPreBlazorDiagnostics("@RequestPath");
|
||||
</script>
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="_framework/blazor.web.js" autostart="false"></script>
|
||||
<script>
|
||||
Blazor.start({
|
||||
ssr: {
|
||||
disableDomPreservation: true
|
||||
}
|
||||
});
|
||||
</script>
|
||||
}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/admin"
|
||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Admin" LoggedOut="OnLoggedOutAsync">
|
||||
<ChildContent Context="workspace">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/campaigns"
|
||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Campaigns" LoggedOut="OnLoggedOutAsync">
|
||||
<ChildContent Context="workspace">
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
@page "/play"
|
||||
@rendermode @(new InteractiveServerRenderMode(prerender: false))
|
||||
@inherits AuthenticatedPageBase
|
||||
<Workspace Route="WorkspaceRoute.Play" LoggedOut="OnLoggedOutAsync">
|
||||
<ChildContent Context="workspace">
|
||||
|
||||
@@ -116,8 +116,8 @@ public partial class Workspace : IAsyncDisposable
|
||||
Logger.LogInformation("Workspace.NavigateToRouteAsync fromRoute={Route} toRoute={TargetRoute} state=[{State}]",
|
||||
Route, route, WorkspaceDiagnosticSummary.DescribeState(State));
|
||||
State.IsScreenMenuOpen = false;
|
||||
Navigation.NavigateTo(route);
|
||||
return InvokeAsync(StateHasChanged);
|
||||
Navigation.NavigateTo(route, forceLoad: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
private Task RedirectToPlayAsync()
|
||||
@@ -127,7 +127,7 @@ public partial class Workspace : IAsyncDisposable
|
||||
|
||||
Logger.LogWarning("Workspace.RedirectToPlayAsync fromRoute={Route} state=[{State}]",
|
||||
Route, WorkspaceDiagnosticSummary.DescribeState(State));
|
||||
Navigation.NavigateTo("/play");
|
||||
Navigation.NavigateTo("/play", forceLoad: true);
|
||||
return Task.CompletedTask;
|
||||
}
|
||||
|
||||
|
||||
11
TASKS.md
11
TASKS.md
@@ -26,6 +26,7 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
||||
- [x] (2026-05-04) Added expanded workspace startup diagnostics across Blazor lifecycle logging, route-content render logging, and browser-side DOM mutation snapshots to narrow the remaining Firefox batch-2 crash.
|
||||
- [x] (2026-05-04) Extended the diagnostics to page-load time by wrapping the interactive host in a stable container and logging pre-Blazor body and host mutations before the first interactive batch applies.
|
||||
- [x] (2026-05-04) Reworked authenticated route startup into phased interactive batches so the first render mounts only a tiny shell, the second render mounts a simple header placeholder, the third render mounts route skeletons, and real control-heavy content appears only after route initialization completes.
|
||||
- [x] (2026-05-04 22:17Z) Removed global authenticated `Routes` interactivity, moved `InteractiveServerRenderMode(prerender: false)` onto the real authenticated pages, and switched to manual `Blazor.start({ ssr: { disableDomPreservation: true } })` startup based on the upstream Firefox guidance in `dotnet/aspnetcore#58824`.
|
||||
|
||||
## Surprises & Discoveries
|
||||
|
||||
@@ -56,6 +57,12 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
||||
- Observation: the RoboForm-triggered crash happens before any component `OnAfterRenderAsync` callback in the authenticated route tree.
|
||||
Evidence: in the failing `/play` and `/admin` reproductions, the last server-side logs were only `OnInitialized` and `OnParametersSet` entries for `Workspace` and its immediate child components; there were no `WorkspaceRouteView.OnAfterRenderAsync` or `Workspace.InitializeRouteCoreAsync` entries before the circuit terminated.
|
||||
|
||||
- Observation: the current app still matched the upstream "global interactivity" failure shape even after the route-first rewrite, because `App.razor` continued to apply `@rendermode` to the root `Routes` component.
|
||||
Evidence: `RpgRoller/Components/App.razor` still rendered `<Routes @rendermode="@(new InteractiveServerRenderMode(prerender: false))" />` until the final follow-up pass, while `dotnet/aspnetcore#58824` explicitly reports Firefox crashes for Global mode and says PerPage mode does not reproduce.
|
||||
|
||||
- Observation: once the authenticated pages moved to per-page interactivity, header route navigation needed full document reloads instead of in-circuit `NavigationManager.NavigateTo` transitions.
|
||||
Evidence: the first Selenium run after the per-page render-mode change reached `/play` in the URL but never mounted `#skill-filter-input` after `/campaigns -> /play` until `Workspace.NavigateToRouteAsync` switched to `forceLoad: true`.
|
||||
|
||||
- Observation: the locally installed Snap Firefox build on this machine is viable for Selenium through `geckodriver`, but not for Playwright protocol control.
|
||||
Evidence: Playwright stalled during the `-juggler-pipe` handshake, while a `geckodriver` plus Selenium session against `/snap/firefox/current/usr/lib/firefox/firefox` completed the same Milestone 1 verification successfully.
|
||||
|
||||
@@ -77,6 +84,10 @@ The change is complete when a human can run the app, open `/`, observe the corre
|
||||
Rationale: the current `screen` preference in `sessionStorage` causes the app state and the browser URL to disagree. Real routes remove that mismatch and make refresh, deep-linking, and testing simpler.
|
||||
Date/Author: 2026-05-04 / Codex
|
||||
|
||||
- Decision: stop using global authenticated interactivity and move the authenticated pages to per-page `InteractiveServerRenderMode(prerender: false)` with manual startup that disables SSR DOM preservation.
|
||||
Rationale: upstream issue `dotnet/aspnetcore#58824` identifies Firefox failures tied to Global interactivity and explicitly notes that PerPage mode does not share the problem. The Blazor startup guidance also documents manual `Blazor.start` configuration for SSR options such as `disableDomPreservation`.
|
||||
Date/Author: 2026-05-04 / Codex
|
||||
|
||||
- Decision: stage the rewrite in two layers: first introduce real routes while preserving existing feature behavior, then split the large workspace tree into route-owned subtrees.
|
||||
Rationale: the current workspace is dense and risk-prone. A staged rewrite keeps the app working while the route model changes, and it gives the test suite meaningful checkpoints.
|
||||
Date/Author: 2026-05-04 / Codex
|
||||
|
||||
Reference in New Issue
Block a user