284 lines
26 KiB
Markdown
284 lines
26 KiB
Markdown
# Rewrite The Web App Into A Route-First Authenticated Shell
|
||
|
||
This ExecPlan is a living document. The sections `Progress`, `Surprises & Discoveries`, `Decision Log`, and `Outcomes & Retrospective` must be kept up to date as work proceeds.
|
||
|
||
`PLANS.md` is checked into the repository root. This document must be maintained in accordance with `PLANS.md`.
|
||
|
||
## Purpose / Big Picture
|
||
|
||
After this change, the browser URL will match the authenticated screen the user is actually using. Anonymous users who open `/` will be redirected to `/login`. Authenticated users who open `/` will be redirected to `/play`. The hamburger menu will navigate to real routes such as `/play`, `/campaigns`, and `/admin` instead of toggling large conditional branches inside one component at `/`.
|
||
|
||
This matters because the current authenticated workspace is still one large, structurally dynamic Blazor Server surface. `POSTMORTEM.md` shows that this architecture is fragile when browser extensions mutate form-related DOM during startup. The route-first rewrite reduces the amount of UI that wakes up at once, removes the dual-purpose `/` shell, and makes the authenticated shell easier to reason about, test, and evolve.
|
||
|
||
The change is complete when a human can run the app, open `/`, observe the correct redirect based on auth state, log in at `/login`, land on `/play`, navigate to `/campaigns` and `/admin` with real URLs, refresh any of those routes without being thrown back to `/`, and run the automated host and Playwright tests that prove the new behavior.
|
||
|
||
## Progress
|
||
|
||
- [x] (2026-05-04 17:52Z) Reviewed `POSTMORTEM.md`, the current app shell, workspace routing behavior, and the existing host and Playwright tests to define the rewrite around real routes instead of `sessionStorage` screen switching.
|
||
- [x] (2026-05-04 17:52Z) Updated `README.md` so it accurately describes the current architecture and the approved rewrite direction.
|
||
- [x] (2026-05-04 18:29Z) Implemented a host-level `/` redirect to `/login` or `/play`, moved the static auth document to `/login`, switched login/logout targets to `/play` and `/login`, and updated the root-path host and smoke coverage to the new contract.
|
||
- [ ] Introduce real authenticated routes for `/play`, `/campaigns`, and `/admin` while preserving current behavior.
|
||
- [ ] Remove `screen` as a `sessionStorage` routing mechanism and replace menu actions with URL navigation.
|
||
- [ ] Split the large `Workspace` render tree so play, campaign management, and admin each own a smaller subtree.
|
||
- [ ] Reduce `OnAfterRenderAsync` to the smallest practical scope and keep staged startup out of the authenticated shell root.
|
||
- [ ] Update host tests, Playwright smoke tests, and docs so the new route model is the only documented and verified behavior.
|
||
|
||
## Surprises & Discoveries
|
||
|
||
- Observation: the current browser API client is still implemented through JavaScript interop, so the authenticated UI cannot simply move all startup work into `OnInitializedAsync`.
|
||
Evidence: `RpgRoller/Components/RpgRollerApiClient.cs` calls `js.InvokeAsync("rpgRollerApi.request", ...)`, which means authenticated data fetches currently depend on an interactive render before they can run.
|
||
|
||
- Observation: the current smoke suite encodes the old dual-purpose `/` behavior and will fail as soon as `/` becomes a redirect entry point.
|
||
Evidence: `tests/e2e/smoke.spec.js` currently expects anonymous `GET /` to render static auth markup and authenticated `GET /` to render the Blazor workspace shell.
|
||
|
||
- Observation: the current host test also encodes an outdated assumption about `/`.
|
||
Evidence: `RpgRoller.Tests/Api/FrontendHostTests.cs` currently asserts that `GET /` returns HTTP 200 and a Blazor shell containing `_framework/blazor.web.js`.
|
||
|
||
- Observation: `MapRazorComponents<App>()` does not serve the static `/login` document unless a matching component route exists, even though `App.razor` itself renders the static auth markup outside the interactive router.
|
||
Evidence: the first Milestone 1 host test run returned HTTP 404 for `GET /login` until a minimal `RpgRoller/Components/Pages/LoginPage.razor` with `@page "/login"` was added.
|
||
|
||
- Observation: the repository-wide backend suite currently contains a missing-fixture failure unrelated to the route-first rewrite.
|
||
Evidence: `dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings` failed in `HostingCoverageTests.InitializeRpgRollerState_MigratesCopiedDevelopmentDatabaseAndPreservesD6Rolling` because `RpgRoller/App_Data/rpgroller.development.db` is not present in the worktree.
|
||
|
||
- Observation: the locally installed Snap Firefox build on this machine does not complete Playwright’s Firefox control handshake.
|
||
Evidence: Playwright launched `/usr/bin/firefox` with `-juggler-pipe` and stalled before page automation began, so Milestone 1 browser verification was completed with `geckodriver` plus Selenium against the same temporary app instance instead.
|
||
|
||
## Decision Log
|
||
|
||
- Decision: implement the approved route-first approach rather than continuing to add localized mitigations inside the current `/` workspace shell.
|
||
Rationale: the user approved this direction after reviewing three refactor options, and `POSTMORTEM.md` concludes that the problem is architectural rather than a single bug.
|
||
Date/Author: 2026-05-04 / Codex and user
|
||
|
||
- Decision: keep the anonymous auth page as plain HTML and JavaScript, but move it to `/login` instead of restoring it as an interactive Blazor form.
|
||
Rationale: the anonymous path was intentionally isolated from Blazor in commit `2d2ed56`, and the postmortem treats that isolation as a valid mitigation for the login surface. The rewrite should not reintroduce a form-heavy Blazor login page unless there is a compelling reason later.
|
||
Date/Author: 2026-05-04 / Codex
|
||
|
||
- Decision: make `/` a server-side redirect entry point instead of continuing to let `App.razor` choose between auth and workspace content based on request-time auth state.
|
||
Rationale: `App.razor` is currently a hidden architecture boundary. Moving auth-based entry selection to an HTTP redirect makes the boundary explicit, testable, and smaller.
|
||
Date/Author: 2026-05-04 / Codex
|
||
|
||
- Decision: use the URL path as the source of truth for the current authenticated screen.
|
||
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: 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
|
||
|
||
## Outcomes & Retrospective
|
||
|
||
At plan creation time, the repository has an updated README and a concrete implementation plan, but no code for the route-first rewrite has been started yet. The immediate risk is not uncertainty about direction; it is carrying old assumptions about `/`, `Home.razor`, and `sessionStorage`-based screen switching into the first code changes. The milestones below are written to make those assumptions explicit and retire them in an observable order.
|
||
|
||
After Milestone 1, the dual-purpose `/` entry point is gone. Anonymous requests to `/` are now redirected before Blazor renders, the static auth document lives at `/login`, and successful login lands on `/play`. The main residual risk is that the authenticated shell is still monolithic behind the new `/play` route, so later milestones still need to replace in-memory screen switching with real route ownership.
|
||
|
||
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
|
||
|
||
The current app serves both anonymous and authenticated experiences from the same root request path. In `RpgRoller/Components/App.razor`, the HTML shell checks the current request path and session cookie through `HttpContext`. If the request is for `/` and no valid session exists, it renders `RpgRoller/Components/Pages/HomeControls/StaticAuthPage.razor` as plain HTML and loads `RpgRoller/wwwroot/js/rpgroller-api.js`. That JavaScript file binds the login and registration forms and sends `fetch` requests to `/api/auth/register` and `/api/auth/login`.
|
||
|
||
If a valid session cookie exists, `App.razor` instead renders the interactive Blazor router. The only current component route for the authenticated shell is `RpgRoller/Components/Pages/Home.razor`, which maps `@page "/"` and immediately renders `Workspace`. `RpgRoller/Components/Pages/Home.razor.cs` is only a logout redirect helper; it is not a real page controller anymore.
|
||
|
||
The authenticated workspace lives in `RpgRoller/Components/Pages/Workspace.razor` and `Workspace.razor.cs`. The Razor file contains the header, play screen, campaign management screen, admin screen, toasts, and modals. The code-behind wires several coordinator classes, and `OnAfterRenderAsync` drives session initialization and staged control enablement. The currently selected screen is stored in `WorkspaceState.CurrentScreen`, and `WorkspaceSessionCoordinator.cs` persists that screen name in browser `sessionStorage` under the key `rpgroller.screen`.
|
||
|
||
In plain language, a “route-first authenticated shell” means that the browser path decides which authenticated page is being rendered. `/play` means the play page. `/campaigns` means the campaign management page. `/admin` means the admin page. The URL is not a decorative detail; it is the primary way the app chooses the screen. Menu clicks change the URL. Reloading the page preserves the same screen because the URL already says what the screen is.
|
||
|
||
In this repository, “server-side redirect” means an HTTP redirect response such as `302 Found` returned before any Blazor UI is rendered. For example, `GET /` should answer with a redirect to `/login` or `/play` based on whether the session cookie maps to a real user through `IGameService.GetUserBySession`.
|
||
|
||
The API surface is already session-cookie-based. `RpgRoller/Api/AuthEndpoints.cs` sets the session cookie on login, `RpgRoller/Api/MeEndpoints.cs` returns the authenticated user model, and the rest of the authenticated `/api` routes are behind `RequireSessionTokenFilter`. This means the routing rewrite does not need a new auth system. It needs a clearer frontend entry structure and smaller authenticated page ownership boundaries.
|
||
|
||
One constraint must be kept in mind from the start: `RpgRoller/Components/RpgRollerApiClient.cs` performs requests through JavaScript interop. That means the authenticated UI still needs an interactive render before it can make its first data request. The rewrite must therefore reduce the amount of structure that changes after interactivity begins, not pretend that interactivity can be avoided entirely with the current client stack.
|
||
|
||
## Plan of Work
|
||
|
||
Begin by separating the entry route from the anonymous auth page. Add a small host-level endpoint module, for example `RpgRoller/Api/FrontendEntryEndpoints.cs`, or an equivalent hosting extension, and map `GET /` before the Razor component host is mapped in `RpgRoller/Program.cs`. This endpoint must read the session cookie using the same cookie name defined in `RpgRoller/Api/SessionCookie.cs`, ask `IGameService` whether the cookie belongs to a real user, and return a redirect to `/play` for authenticated users or `/login` for anonymous users. This removes the dual-purpose `/` behavior.
|
||
|
||
Next, simplify `RpgRoller/Components/App.razor` so it no longer chooses between anonymous and authenticated content based on auth state. It may still choose between a static `/login` document and the interactive authenticated router based on the request path, because the anonymous page is intentionally plain HTML. The important change is that auth-state branching moves out of the component tree. `App.razor` should become a stable host for either the static login document at `/login` or the interactive authenticated route set everywhere else.
|
||
|
||
After that, introduce real component routes for the authenticated pages. Create `RpgRoller/Components/Pages/PlayPage.razor`, `CampaignsPage.razor`, and `AdminPage.razor`, each with an explicit `@page` directive. In the first implementation pass, it is acceptable to keep much of the existing state and coordinator logic by adapting `Workspace` so each route uses only the subtree it needs. The key result of this milestone is that the URL changes from `/play` to `/campaigns` to `/admin` and each route refreshes correctly.
|
||
|
||
Once the routes exist, remove `screen` as a persistence and navigation concept. Delete the `CurrentScreen` routing responsibility from `WorkspaceState.cs` and remove the `screen` `sessionStorage` reads and writes from `WorkspaceSessionCoordinator.cs`. Replace menu items in `Workspace.razor.cs` and `AppHeader.razor` wiring so they navigate through `NavigationManager.NavigateTo(...)` or plain links to `/play`, `/campaigns`, and `/admin`. Keep `sessionStorage` only for true view preferences such as mobile panel state, selected campaign when appropriate, and roll visibility if those still earn their complexity.
|
||
|
||
The next pass is the structural split. Extract the common authenticated chrome into a dedicated component such as `RpgRoller/Components/Pages/AuthenticatedShell.razor`. This shared shell should own the header, logout action, health banner, and toast stack. Then move the play-only DOM, campaign management DOM, and admin DOM out of the monolithic conditional branches in `Workspace.razor` and into page-specific route components. `PlayPage` should own SSE startup and the play-specific panels. `CampaignsPage` should own character create and edit workflows. `AdminPage` should own admin-only data loading and buttons. The goal is that each route owns a smaller and more stable subtree, rather than all authenticated screens living under one branching root.
|
||
|
||
Finally, revisit startup sequencing. Because API reads still depend on JS interop, some post-render initialization may remain necessary, but it should be limited to the page that actually needs it. Remove the pattern where the authenticated shell root performs several structural follow-up renders merely to decide which screen to show. If staged initialization remains on `/play`, it should be contained to the play page and should reveal a stable page-local loading shell rather than reshaping the entire authenticated app. Record the exact remaining `OnAfterRenderAsync` responsibilities in the code and in `README.md`.
|
||
|
||
Throughout the rewrite, keep the documentation and tests aligned. `README.md` must stop describing the rewrite as planned once the code lands, and the host and smoke tests must verify the new route-first behavior rather than preserve the old root-path assumptions.
|
||
|
||
## Milestones
|
||
|
||
### Milestone 1: Make `/` An Explicit Entry Redirect
|
||
|
||
At the end of this milestone, a browser request to `/` no longer renders either the auth page or the workspace directly. Instead, the server returns a redirect to `/login` or `/play` based on the session cookie. The anonymous auth page is reachable at `/login`, and logging in transitions the user to `/play`.
|
||
|
||
Implement this by adding the new entry endpoint mapping, updating `App.razor` to host `/login` without auth-state branching, and changing `rpgroller-api.js` so successful login goes to `/play` rather than `/`. Also update `Home.razor.cs` or its replacement logout helper so logout navigates to `/login` with the existing status message query behavior.
|
||
|
||
Proof for this milestone is simple and observable. Anonymous `GET /` returns a redirect to `/login`. Authenticated `GET /` returns a redirect to `/play`. Opening `/login` renders the current static auth markup without loading `_framework/blazor.web.js`. Logging in from `/login` lands on the play workspace.
|
||
|
||
### Milestone 2: Add Real Authenticated Routes Without Breaking Features
|
||
|
||
At the end of this milestone, `/play`, `/campaigns`, and `/admin` all exist as first-class routes, and the hamburger menu moves between them using URLs rather than `sessionStorage`. Feature behavior may still be backed by some shared workspace code, but the route model is now real.
|
||
|
||
Implement this by creating the new page components and adapting the current workspace logic so the correct route renders the correct content. During this milestone it is acceptable to keep a shared backing component or service if that reduces churn, but the URL must be the authoritative screen selection mechanism. Direct navigation to `/campaigns` should show campaign management, not the play screen followed by an in-memory switch. Direct navigation to `/admin` should either show the admin page for admins or redirect non-admin users to `/play`.
|
||
|
||
Acceptance for this milestone is that refreshing `/campaigns` leaves the user on `/campaigns`, refreshing `/play` leaves the user on `/play`, and opening `/admin` as a non-admin does not expose admin controls.
|
||
|
||
### Milestone 3: Split The Monolithic Workspace Tree
|
||
|
||
At the end of this milestone, there is no longer a single authenticated component that conditionally renders all major screens under one branch-heavy root. Shared authenticated chrome is extracted, and each route owns its own main content subtree.
|
||
|
||
Implement this by introducing a shared authenticated shell component and moving the play, campaign management, and admin markup and page-specific coordination into route-owned components. Keep shared models and helper methods where they still make sense, but stop letting the root workspace decide which major screen exists in the DOM. If common state still exists, narrow it to user identity, selected campaign context, and shared feedback only.
|
||
|
||
Acceptance for this milestone is partly structural and partly behavioral. Structurally, `Workspace.razor` should no longer contain mutually exclusive branches for play, management, and admin screens. Behaviorally, the DOM-wrap smoke test or its replacement should still pass while each route loads only the controls it needs.
|
||
|
||
### Milestone 4: Reduce Startup Churn And Finalize Docs
|
||
|
||
At the end of this milestone, the authenticated shell no longer uses `OnAfterRenderAsync` as the orchestration point for screen selection and broad structural staging. Any remaining post-render work is page-local, justified, and documented.
|
||
|
||
Implement this by moving any remaining screen-routing or shell-bootstrap logic out of `Workspace.razor.cs`, narrowing `OnAfterRenderAsync` responsibilities, and updating `README.md` to describe the completed route-first architecture rather than a planned rewrite. Also update `POSTMORTEM.md` only if a concise follow-up note is warranted; do not rewrite its historical analysis.
|
||
|
||
Acceptance for this milestone is a passing automated suite plus a manual browser run where `/`, `/login`, `/play`, `/campaigns`, and `/admin` all behave consistently with the final route model.
|
||
|
||
## Concrete Steps
|
||
|
||
Run all commands from the repository root, which is `/home/frank/Code/RpgRoller`.
|
||
|
||
Start by inspecting the current route and auth files before editing:
|
||
|
||
sed -n '1,220p' RpgRoller/Components/App.razor
|
||
sed -n '1,220p' RpgRoller/Components/Pages/Home.razor
|
||
sed -n '1,260p' RpgRoller/Components/Pages/Workspace.razor.cs
|
||
sed -n '1,260p' RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs
|
||
sed -n '1,260p' RpgRoller/wwwroot/js/rpgroller-api.js
|
||
sed -n '1,220p' RpgRoller.Tests/Api/FrontendHostTests.cs
|
||
sed -n '1,260p' tests/e2e/smoke.spec.js
|
||
|
||
When implementing Milestone 1, update the host test first so the intended redirect behavior is explicit:
|
||
|
||
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --filter FrontendHostTests
|
||
|
||
Expected direction after the edits:
|
||
|
||
RootPath_RedirectsToLogin_WhenAnonymous
|
||
RootPath_RedirectsToPlay_WhenAuthenticated
|
||
LoginPath_ServesStaticAuthMarkup
|
||
|
||
After wiring `/login` and the root redirect, run the app locally:
|
||
|
||
dotnet run --project RpgRoller/RpgRoller.csproj
|
||
|
||
Then verify in a browser:
|
||
|
||
open http://localhost:5000/
|
||
observe: anonymous request lands on /login
|
||
submit valid credentials
|
||
observe: browser lands on /play
|
||
|
||
When implementing route pages and navigation, prefer running the focused smoke suite against a temporary database:
|
||
|
||
pwsh ./scripts/run-playwright.ps1
|
||
|
||
If the app is already running and a faster inner loop is needed, run the checked-in smoke file directly:
|
||
|
||
npm run e2e:smoke
|
||
|
||
After each milestone that touches C# files, run the relevant test suite and then the full backend suite before moving on:
|
||
|
||
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings
|
||
|
||
After major frontend milestones, repeat browser verification in Chromium and Firefox. If a Firefox profile with RoboForm is available, include that manual check and record the result in `Surprises & Discoveries` or `Outcomes & Retrospective`.
|
||
|
||
## Validation and Acceptance
|
||
|
||
The final implementation is acceptable only if all of the following behaviors are true and visible.
|
||
|
||
Anonymous navigation:
|
||
|
||
`GET /` returns an HTTP redirect to `/login`. Opening `/login` shows the static auth document with the current register and login forms. The `/login` document must not load `_framework/blazor.web.js`, and it must still include the existing auth page hooks used by `rpgroller-api.js`.
|
||
|
||
Authenticated navigation:
|
||
|
||
After a successful login, the browser lands on `/play`. Opening `/` with an already valid session cookie redirects to `/play`. Refreshing `/play`, `/campaigns`, or `/admin` preserves the same route instead of rebuilding everything behind `/`.
|
||
|
||
Menu behavior:
|
||
|
||
The header menu items navigate to real routes. The active state matches the current route. Non-admin users cannot remain on `/admin`; they are redirected to `/play` or shown a deliberate authorization result defined by the implementation, but not an exposed admin UI.
|
||
|
||
Workspace stability:
|
||
|
||
The authenticated play route continues to support the existing play workflow, including campaign log rendering, character controls, and custom roll actions. The DOM-wrap smoke coverage for extension-like mutations must still pass, either through the existing test or an updated equivalent that targets `/play`.
|
||
|
||
Automated coverage:
|
||
|
||
`dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings` passes.
|
||
|
||
`pwsh ./scripts/run-playwright.ps1` passes against a temporary SQLite database.
|
||
|
||
If any previous tests are deleted or renamed because they encoded the old `/` behavior, replace them with tests that prove the new route model instead of simply removing coverage.
|
||
|
||
## Idempotence and Recovery
|
||
|
||
This rewrite should be implemented as a sequence of additive, testable steps. Each milestone must leave the app runnable and the tests meaningful. Avoid a large flag day where `/` is changed, the route pages half-exist, and the smoke suite is left broken for an extended period.
|
||
|
||
The safest recovery strategy is to keep the current workspace internals temporarily while introducing the new route model. That means it is acceptable to reuse `Workspace` behind the new page routes during Milestone 2, as long as the route behavior is correct and clearly transitional. After that, extract route-specific subtrees in Milestone 3.
|
||
|
||
When changing redirects or login targets, update the host and Playwright assertions in the same commit as the code change so the repository never has code and tests describing different route contracts.
|
||
|
||
Use a temporary SQLite database for Playwright verification, as required by the repo instructions, so browser tests do not mutate the canonical development database.
|
||
|
||
## Artifacts and Notes
|
||
|
||
Current evidence that must be retired by this rewrite:
|
||
|
||
RpgRoller.Tests/Api/FrontendHostTests.cs
|
||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||
Assert.Contains("_framework/blazor.web.js", html);
|
||
|
||
tests/e2e/smoke.spec.js
|
||
test("home page loads auth entry points", ...)
|
||
test("home document renders static auth markup without bootstrapping blazor", ...)
|
||
test("authenticated home document avoids prerendered workspace shell", ...)
|
||
|
||
Current evidence that explains the bootstrap constraint:
|
||
|
||
RpgRoller/Components/RpgRollerApiClient.cs
|
||
var response = await js.InvokeAsync<JsApiResponse>("rpgRollerApi.request", method, path, payload);
|
||
|
||
Current evidence that explains why route navigation must replace screen persistence:
|
||
|
||
RpgRoller/Components/Pages/WorkspaceSessionCoordinator.cs
|
||
private const string ScreenSessionKey = "screen";
|
||
state.CurrentScreen = NormalizeRequestedScreen(storedScreen) ?? ScreenPlay;
|
||
await js.InvokeVoidAsync("rpgRollerApi.setSessionValue", ScreenSessionKey, state.CurrentScreen);
|
||
|
||
## Interfaces and Dependencies
|
||
|
||
The implementation must continue to use the existing ASP.NET Core hosting model in `RpgRoller/Program.cs`, the minimal API auth surface in `RpgRoller/Api`, and the existing `IGameService` session lookup methods. Do not introduce a second auth mechanism.
|
||
|
||
At the end of Milestone 1, the codebase must contain a host-level entry point with behavior equivalent to:
|
||
|
||
GET /
|
||
if session cookie maps to a valid user: redirect to /play
|
||
otherwise: redirect to /login
|
||
|
||
At the end of Milestone 2, the codebase must contain route components equivalent to:
|
||
|
||
/play
|
||
/campaigns
|
||
/admin
|
||
|
||
Each of those routes must be directly navigable and refreshable.
|
||
|
||
At the end of Milestone 3, the codebase must contain a shared authenticated shell component or layout that owns common header and feedback concerns, while route pages own their feature-specific DOM. Stable names are recommended:
|
||
|
||
RpgRoller/Components/Pages/AuthenticatedShell.razor
|
||
RpgRoller/Components/Pages/PlayPage.razor
|
||
RpgRoller/Components/Pages/CampaignsPage.razor
|
||
RpgRoller/Components/Pages/AdminPage.razor
|
||
|
||
These exact filenames are recommended because they make the route split obvious to a new contributor, but equivalent names are acceptable if the same ownership boundaries are preserved and the README is updated accordingly.
|
||
|
||
## Revision Note
|
||
|
||
2026-05-04 17:52Z: Initial ExecPlan created after the route-first rewrite direction was approved and the README was overhauled. The main reason for the plan is to replace the dual-purpose `/` shell with explicit routes while keeping the repository testable at every step.
|