refactor: finish route-first shell

This commit is contained in:
2026-05-04 21:58:22 +02:00
parent 9c3f7c039e
commit 73dc4a9cd4
14 changed files with 127 additions and 128 deletions

View File

@@ -6,7 +6,7 @@ RpgRoller is an ASP.NET Core and Blazor Server app for lightweight tabletop camp
- `RpgRoller.Tests/`: xUnit coverage for API behavior, services, hosting, payload budgets, and persistence and migration paths
- `RpgRoller.sln`: solution used by local development and repo scripts
- `POSTMORTEM.md`: architecture analysis of the May 2026 Firefox and RoboForm failure in the authenticated workspace
- `TASKS.md`: the current execution plan for the approved frontend routing rewrite
- `TASKS.md`: the completed execution log for the route-first authenticated shell rewrite
Test layout:
@@ -36,14 +36,17 @@ Backend:
Frontend:
- `RpgRoller/Components/App.razor`: current HTML shell and the request-time branch that decides whether `/` serves the static auth page or the interactive app
- `RpgRoller/Components/App.razor`: HTML shell that serves the static `/login` auth document or the interactive 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
- `RpgRoller/Components/Pages/PlayPage.razor`, `CampaignsPage.razor`, and `AdminPage.razor`: authenticated route entry points for the interactive workspace
- `RpgRoller/Components/Pages/AuthenticatedPageBase.cs`: shared logout-to-`/login` redirect helper for authenticated route pages
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI with play, campaign management, admin, toasts, and modals
- `RpgRoller/Components/Pages/Workspace.razor.cs`: workspace composition root, lifecycle, coordinator wiring, JS-invokable entry points, and menu item construction
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated shell with shared header, health banner, toast stack, and route-owned body slot
- `RpgRoller/Components/Pages/Workspace.razor.cs`: shell composition root, coordinator wiring, route initialization entry point, JS-invokable state-event hooks, and menu item construction
- `RpgRoller/Components/Pages/WorkspaceRouteView.razor`: route-local first-render bootstrapper that initializes the interactive workspace after the page mounts
- `RpgRoller/Components/Pages/PlayWorkspaceContent.razor`, `CampaignsWorkspaceContent.razor`, and `AdminWorkspaceContent.razor`: route-owned authenticated page subtrees
- `RpgRoller/Components/Pages/CharacterManagementModals.razor`: shared create and edit character modals used by play and campaign-management routes
- `RpgRoller/Components/Pages/WorkspaceState.cs`: workspace UI state plus pure computed and formatting projections used directly by the Razor view
- `RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs`, `WorkspaceCampaignCoordinator.cs`, `WorkspaceCampaignScopeCoordinator.cs`, `WorkspacePlayCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, `WorkspaceLiveStateController.cs`, `WorkspaceFeedbackService.cs`, and `WorkspaceToast.cs`: session bootstrap, campaign scope, play and log, admin, live update, and toast concerns used by `Workspace`
- `RpgRoller/Components/Pages/HomeControls/StaticAuthPage.razor`: plain HTML login and registration page used at `/login`
@@ -55,8 +58,8 @@ Frontend:
Current repo note:
- `POSTMORTEM.md` documents why the current authenticated workspace architecture is fragile and why the next major frontend change is a route-first rewrite of the authenticated shell.
- `TASKS.md` is the authoritative execution plan for that rewrite and must be kept current while the work proceeds.
- `POSTMORTEM.md` documents why the previous authenticated workspace architecture was fragile under Firefox plus RoboForm.
- `TASKS.md` records the route-first rewrite that addressed that architecture.
## Runtime and Persistence
@@ -95,26 +98,29 @@ Rolemaster support:
## Current Frontend Architecture
The current frontend is in an intermediate state that was created while mitigating the Firefox and RoboForm failure documented in `POSTMORTEM.md`.
The frontend now uses a route-first authenticated shell that keeps the anonymous auth document outside the interactive Blazor subtree.
Today, `/` is an auth-aware entry redirect:
`/` is an auth-aware entry redirect:
- anonymous `GET /` redirects to `/login`
- authenticated `GET /` redirects to `/play`
- `RpgRoller/Components/App.razor` still decides between the static `/login` document and the interactive route set based on the request path, not auth state
- `RpgRoller/Components/App.razor` serves the static `/login` document or the interactive route set based on the request path, not auth state
Inside the authenticated app, `/play`, `/campaigns`, and `/admin` are now real Blazor routes, and the hamburger menu navigates between those URLs. The interactive shell is still structurally transitional, because `Workspace.razor` continues to own all three major authenticated subtrees behind one component.
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.
This architecture works functionally but remains structurally fragile because:
Interactive bootstrap is now route-local:
- the HTML shell still branches on request path to keep `/login` static
- the authenticated workspace still performs staged startup in `OnAfterRenderAsync`
- the app coordinates state across Blazor component state, browser `sessionStorage`, `fetch`, and SSE during early startup
- the shared `Workspace` component still conditionally renders play, campaign management, and admin DOM instead of letting each route own its own subtree
- `WorkspaceRouteView.razor` performs the first-render JS-dependent session initialization for the authenticated route that mounted
- `Workspace.razor.cs` no longer uses `OnAfterRenderAsync` as the shell bootstrap orchestrator
- play-specific post-render behavior is limited to page-local controls such as log auto-scroll and modal autofocus inside child components
## Approved Rewrite Direction
Remaining architectural constraints are deliberate:
The approved remediation direction is a route-first authenticated shell:
- `/login` stays plain HTML plus JavaScript so the anonymous auth path avoids Blazor form ownership entirely
- authenticated reads and writes still depend on JS interop-backed `fetch`, so first interactive initialization must still happen after mount
- live updates still use SSE and route-aware synchronization, with `/play` as the only route that keeps the play log and selected character sheet live
## Route-First Authenticated Shell
- `/` becomes an auth-aware entry point that redirects to `/login` or `/play`
- `/login` hosts the anonymous auth experience
@@ -123,7 +129,7 @@ The approved remediation direction is a route-first authenticated shell:
- SSE and heavy play bootstrap stay scoped to `/play`
- the large `Workspace` component is split so each route owns a smaller, more stable subtree
This rewrite is not complete yet. Follow `TASKS.md` for the execution plan.
This rewrite is complete. See `TASKS.md` for the execution history and milestone notes.
## Local Development
@@ -184,9 +190,10 @@ SQLite migration rule:
## Frontend Runtime
- The UI currently runs as Blazor Server with interactive components for the authenticated workspace and a plain HTML plus JavaScript auth page for anonymous users at `/`.
- The UI runs as Blazor Server 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`.
- 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`.
- Log rows return compact summary data first and lazy-load full detail from `/api/rolls/{rollId}` when expanded.