Overhaul frontend rewrite documentation

This commit is contained in:
2026-05-04 20:04:40 +02:00
parent 8d08b857ab
commit a7f6163c4b
2 changed files with 249 additions and 138 deletions

View File

@@ -1,10 +1,12 @@
# RpgRoller
RpgRoller is an ASP.NET Core + Blazor Server app for lightweight tabletop campaign play, character sheets, and dice workflows.
RpgRoller is an ASP.NET Core and Blazor Server app for lightweight tabletop campaign play, character sheets, and dice workflows.
- `RpgRoller/`: web app, API endpoints, domain model, EF Core persistence, Blazor components, and static assets
- `RpgRoller.Tests/`: xUnit coverage for API behavior, services, hosting, payload budgets, and persistence/migration paths
- `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
Test layout:
@@ -16,41 +18,44 @@ Test layout:
Backend:
- `RpgRoller/Program.cs`: app bootstrap, JSON options, compression, API/component mapping, and optional `PathBase`
- `RpgRoller/Program.cs`: app bootstrap, JSON options, compression, API and component mapping, and optional `PathBase`
- `RpgRoller/Hosting/`: service registration, startup initialization, SQLite path resolution, and schema upgrades
- `RpgRoller/Api/`: minimal API endpoint groups, request mappings, cookie/session helpers, and result mapping
- `RpgRoller/Api/`: minimal API endpoint groups, request mappings, cookie and session helpers, and result mapping
- `RpgRoller/Services/`: gameplay and account workflows behind `IGameService`
- `RpgRoller/Services/GameService.cs`: facade over composed domain services
- `RpgRoller/Services/GameAuthService.cs`: registration, login, logout, session lookup, and `GetMe`
- `RpgRoller/Services/GameCampaignService.cs`: campaign creation, listing, roster reads, campaign options, and deletion
- `RpgRoller/Services/GameCharacterService.cs`: character creation, updates, activation, deletion, transfer, and owner-scoped reads
- `RpgRoller/Services/GameSkillService.cs`: skill-group CRUD, skill CRUD, sheet shaping, and ruleset validation
- `RpgRoller/Services/GameRollService.cs`: skill/custom rolls, compact log pages, roll detail, and campaign state snapshots
- `RpgRoller/Services/GameRollService.cs`: skill and custom rolls, compact log pages, roll detail, and campaign state snapshots
- `RpgRoller/Services/GameUserAdministrationService.cs`: username reads, admin user listing, role updates, and account deletion
- `RpgRoller/Services/GameStateStore.cs`, `GameStateCloneFactory.cs`, and `GamePersistenceService.cs`: in-memory runtime state, campaign-state version tracking, and SQLite load/save boundaries
- `RpgRoller/Services/GameAuthorization.cs`, `GameContextResolver.cs`, and `GameDtoMapper.cs`: shared authorization, session/campaign resolution, and backend read-model mapping
- `RpgRoller/Services/GameStateStore.cs`, `GameStateCloneFactory.cs`, and `GamePersistenceService.cs`: in-memory runtime state, campaign-state version tracking, and SQLite load and save boundaries
- `RpgRoller/Services/GameAuthorization.cs`, `GameContextResolver.cs`, and `GameDtoMapper.cs`: shared authorization, session and campaign resolution, and backend read-model mapping
- `RpgRoller/Services/RollEngine.cs`, `StandardRollEngine.cs`, `D6RollEngine.cs`, `RolemasterRollEngine.cs`, `RollBreakdownFormatter.cs`, and `CampaignLogSummaryBuilder.cs`: ruleset-specific dice execution, breakdown formatting, and compact campaign-log summaries
- `RpgRoller/Services/SkillDefinitionValidator.cs`, `RoleSerializer.cs`, `RollVisibilityParser.cs`, and `CustomRollOptionsResolver.cs`: shared rules and parsing helpers
Frontend:
- `RpgRoller/Components/`: Blazor app shell, routes, layout, page components, and query/client helpers
- `RpgRoller/Components/Pages/Home.razor`: gateway that switches between loading, auth, and authenticated workspace views, and force-reloads after login so the authenticated play workspace is built from the fresh session cookie
- `RpgRoller/Components/Pages/Home.razor.cs`: gateway/session orchestration for `Home`
- `RpgRoller/Components/Pages/Workspace.razor`: authenticated workspace UI
- `RpgRoller/Components/Pages/Workspace.razor.cs`: workspace composition root, coordinator wiring, lifecycle, and JS-invokable entry points
- `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/Routes.razor`: Blazor router and layout hookup
- `RpgRoller/Components/Layout/MainLayout.razor`: default layout
- `RpgRoller/Components/Pages/Home.razor`: current root route component for `/`; it only renders `Workspace`
- `RpgRoller/Components/Pages/Home.razor.cs`: logout navigation helper that force-loads `/` and carries auth status query text
- `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/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/log, admin, live update, and toast concerns used by `Workspace`
- `RpgRoller/Components/Pages/HomeControls/`: workspace and auth child components, forms, header, panels, and modal controls
- `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 when `/` is requested without a valid session
- `RpgRoller/Components/Pages/HomeControls/`: workspace child components, forms, header, panels, and modal controls
- `RpgRoller/Components/RpgRollerApiClient.cs`: browser API client for write actions
- `RpgRoller/Components/WorkspaceQueryService.cs`: server-side read model access for workspace data
- `RpgRoller/wwwroot/js/rpgroller-api.js`: browser interop for session storage, SSE wiring, and DOM helpers
- `RpgRoller/Components/WorkspaceQueryService.cs`: browser-facing read client for workspace data
- `RpgRoller/wwwroot/js/rpgroller-api.js`: browser interop for auth forms, session storage, SSE wiring, and DOM helpers
- `RpgRoller/wwwroot/styles.css`: app styling and responsive layout
Current repo note:
- `TASKS.md` records the completed decomposition work and the final execution notes for this refactor.
- This README describes the code as it exists today. It does not treat blueprint items in `TASKS.md` as finished unless they are already present in the repo.
- `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.
## Runtime and Persistence
@@ -66,10 +71,10 @@ Current repo note:
- Supported campaign rulesets: D6 System, D&D 5e, and Rolemaster
- Account registration, login, session-based auth, and role-aware authorization
- Admin tools for user listing, role updates, account deletion, and direct SQLite database download
- Campaign creation, roster reads, participant-scoped visibility, and owner/admin deletion
- Character creation, activation, owner transfer, campaign reassignment or unlinking, and owner/admin deletion
- Campaign creation, roster reads, participant-scoped visibility, and owner and admin deletion
- Character creation, activation, owner transfer, campaign reassignment or unlinking, and owner and admin deletion
- Skill groups with reusable defaults plus skill and skill-group create, edit, reassign, and delete flows
- Owner-scoped play workspace that lists only the current user's characters while preserving GM/admin management capabilities
- Owner-scoped play workspace that lists only the current user's characters while preserving GM and admin management capabilities
- Campaign log paging, lazy-loaded roll detail, compact summaries, and live state refresh through SSE
- Custom roll submission from the play screen without creating a persisted skill
- Instant skill filtering in the character panel
@@ -87,6 +92,37 @@ Rolemaster support:
- Open-ended high chaining and low-end subtraction with ordered die metadata in roll detail
- Compact log badges and summaries for open-ended, retry, and fumble-related events, including `Retry +5` and `Retry +10`
## 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`.
Today, `/` is dual-purpose:
- when the request has no valid session cookie, `RpgRoller/Components/App.razor` renders `StaticAuthPage.razor` as plain HTML and `RpgRoller/wwwroot/js/rpgroller-api.js` handles login and registration through `fetch`
- when the request has a valid session cookie, `App.razor` renders the interactive Blazor app and `Home.razor` loads the authenticated `Workspace`
Inside the authenticated app, the hamburger menu does not navigate to different URLs. Instead, `WorkspaceSessionCoordinator.cs` stores a `screen` preference in `sessionStorage`, and `Workspace.razor` conditionally swaps between play, campaign management, and admin screens inside one large component tree.
This architecture works functionally but remains structurally fragile because:
- the root shell still branches on request-time `HttpContext`
- 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 route URL does not represent the authenticated screen the user is actually viewing
## Approved Rewrite Direction
The approved remediation direction is a route-first authenticated shell:
- `/` becomes an auth-aware entry point that redirects to `/login` or `/play`
- `/login` hosts the anonymous auth experience
- `/play`, `/campaigns`, and `/admin` become real authenticated routes
- the hamburger menu becomes route navigation instead of in-memory screen switching
- 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.
## Local Development
Prerequisites:
@@ -110,6 +146,7 @@ Run locally:
dotnet run --project RpgRoller/RpgRoller.csproj
```
2. Open `http://localhost:5000` or the URL printed in the console.
3. Expect the current app to show either the static auth page at `/` or the authenticated workspace at `/`, depending on whether a valid session cookie already exists.
Playwright helpers:
@@ -145,14 +182,14 @@ SQLite migration rule:
## Frontend Runtime
- The UI runs as Blazor Server with interactive components.
- 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 `/`.
- Static assets are linked through Blazor's `@Assets[...]` pipeline for fingerprinted cache-busting URLs.
- Workspace reads are resolved server-side through `WorkspaceQueryService`; browser interop stays limited to browser-only concerns.
- Workspace reads are resolved through API requests in `WorkspaceQueryService`; browser interop stays focused on auth forms, session storage, SSE wiring, and small DOM helpers.
- 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.
- Newly appended local rolls auto-expand in the play workspace and reuse the roll response as the initial detail payload.
- Custom roll submission uses the selected character context; D6 uses baseline wild-die/fumble behavior, while D&D 5e and Rolemaster use the submitted expression directly.
- Custom roll submission uses the selected character context; D6 uses baseline wild-die and fumble behavior, while D&D 5e and Rolemaster use the submitted expression directly.
- API JSON contracts use the source-generated `RpgRollerJsonSerializerContext`.
- HTTP JSON responses are gzip-compressed when the client advertises support.
- The OpenAPI contract source lives at `openapi/RpgRoller.json`.

302
TASKS.md
View File

@@ -1,4 +1,4 @@
# Rolemaster skill roll situational modifier modal
# 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.
@@ -6,193 +6,267 @@ This ExecPlan is a living document. The sections `Progress`, `Surprises & Discov
## Purpose / Big Picture
After this change, clicking the dice button for any Rolemaster skill on the play screen will no longer roll immediately. Instead, a modal dialog will open first and ask for a one-time situational modifier for that upcoming roll. The player can leave it blank for zero, enter a positive number such as `20` for a bonus, or enter a negative number such as `-15` for a penalty. Pressing Enter will confirm the roll, pressing Escape will cancel it, and clicking outside the modal will also cancel it.
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 `/`.
The important user-visible rule is that this temporary modifier must be applied everywhere the skill roll logic already uses the skills built-in modifier. For a skill stored as `d100!+50`, entering `20` means the first Rolemaster attempt is evaluated as `roll + 50 + 20`, not as a post-processing adjustment. That means an initial result of `8` becomes `8+50+20=78`, which falls into the existing automatic retry band and therefore triggers the retry flow. The retry attempt must also include the same `+20` situational modifier.
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-04-14 21:27:50Z) Created the initial ExecPlan in `TASKS.md`, grounded in the current workspace play flow, API contract, and Rolemaster retry implementation.
- [x] (2026-04-14 21:39:42Z) Added transient `SituationalModifier` support to the skill-roll request, API endpoint, service facade, and roll pipeline without adding persistence or schema changes.
- [x] (2026-04-14 21:51:33Z) Added a Rolemaster-only pre-roll modal on the play screen with autofocus, Escape dismissal, Enter submit, outside-click dismissal, and inline validation for signed integer input.
- [x] (2026-04-14 21:39:42Z) Updated Rolemaster roll execution and breakdown formatting so temporary modifiers are shown explicitly and feed retry-band evaluation plus retry attempts.
- [x] (2026-04-14 21:51:33Z) Added service, API, and Playwright coverage for the new behavior, updated `README.md`, and prepared the touched files for cleanup, full CI, and commit.
- [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.
- [ ] Implement a server-side entry redirect for `/` and move the anonymous auth experience to `/login`.
- [ ] 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: `TASKS.md` was empty before this plan was written, so this ExecPlan now defines the full intended work from scratch.
Evidence: `Get-Item D:\Code\RpgRoller\TASKS.md | Format-List Length` reported `Length : 0`.
- 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 situational modifier fit cleanly as transient request data. No `Skill`, `RollLogEntry`, migration, or EF model change was needed for the first implementation slice.
Evidence: the change only touched `RollSkillRequest`, the roll endpoint/service path, and Rolemaster roll formatting/execution files.
- 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 Rolemaster retry rule is already based on the fully computed first attempt total, not just the raw die result, which matches the new requirement once the temporary modifier is included in that total.
Evidence: `RpgRoller/Services/RolemasterRollEngine.cs` resolves retry bands from `firstAttempt.Total`.
- Observation: the repository already uses Blazor modal patterns with overlays and `ElementReference.FocusAsync()` for autofocus, so the new modal can follow an existing local pattern instead of inventing a second approach.
Evidence: `RpgRoller/Components/Pages/HomeControls/SkillFormModal.razor(.cs)` renders a modal and focuses the name input in `OnAfterRenderAsync`.
- Observation: compact Rolemaster retry summaries still preview the trigger die, not the fully modified first-attempt arithmetic. The authoritative arithmetic belongs in the breakdown string.
Evidence: with a situational modifier, the new service test now expects `8 | open-ended | retry +5` in the compact summary while the detailed breakdown is `8+50+20=78; retry(+5): 42+50+20=112; final=117`.
- Observation: Razor string parameters in the new modal call site need explicit `@` binding or the UI renders the property name as literal text.
Evidence: the first Playwright failure snapshot showed the dialog rendering `State.PendingRolemasterSkillRollError` instead of the actual inline validation message until the binding was corrected in `Workspace.razor`.
- 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`.
## Decision Log
- Decision: the situational modifier will be transient request data only and will not be stored on `Skill`, `RollLogEntry`, or in a migration.
Rationale: the feature is explicitly “once for the upcoming roll.” Persisting it would create stale state, require schema work, and misrepresent the feature.
Date/Author: 2026-04-14 / Codex
- 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: `RollSkillRequest` will gain an integer `SituationalModifier` field with a default of `0`, and server-side skill-roll methods will accept the same value.
Rationale: zero is the normal case, avoids null semantics through the stack, and keeps the request payload simple for both tests and UI code.
Date/Author: 2026-04-14 / Codex
- 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: non-zero situational modifiers will be accepted only for Rolemaster skill rolls. Non-Rolemaster skill rolls will continue to execute immediately without showing the modal, and the server will reject any accidental non-zero modifier sent for another ruleset.
Rationale: the user asked for this behavior only for the Rolemaster system. The server-side guard prevents future UI regressions from silently broadening the feature.
Date/Author: 2026-04-14 / 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: the modal input will be stored as raw text in UI state and parsed on confirm rather than bound directly to an `int`.
Rationale: blank must mean zero, signed values must be easy to type, and raw text avoids awkward intermediate states such as a lone `-` while the user is editing.
Date/Author: 2026-04-14 / 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: Rolemaster breakdown strings will show the base skill modifier and the one-shot situational modifier as separate visible terms instead of folding them into one combined number.
Rationale: the user needs to audit why a retry happened. `8+50+20=78` is clearer than `8+70=78`, especially when comparing the stored skill expression with the one-time adjustment.
Date/Author: 2026-04-14 / Codex
- Decision: compact log badges do not need a new “situational modifier” badge.
Rationale: the result number already changes, the detailed breakdown will show the exact temporary modifier, and adding a new badge for a one-shot value would create clutter with little value.
Date/Author: 2026-04-14 / 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
The feature is now complete end to end. Rolemaster skill rolls no longer execute immediately from the play screen; they first open a modal that accepts an optional one-shot situational modifier, focuses the input automatically, closes on Escape or backdrop click, and validates whole-number input inline. Confirmed rolls send the temporary modifier through the existing skill-roll API, Rolemaster breakdowns show the base and situational modifiers as separate terms, and automatic retry math reuses the same situational modifier on both attempts. Service, API, and Playwright coverage now prove the backend math, the Rolemaster-only guard, and the browser interaction flow.
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.
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
This repository is an ASP.NET Core and Blazor application rooted at `D:\Code\RpgRoller`. The user-facing play screen is assembled in `RpgRoller/Components/Pages/Workspace.razor`. That page wires `CharacterPanel` to the coordinator class `RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs`, which currently handles skill rolls by calling `Play.RollSkillAsync`.
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`.
The skill list and its dice buttons live in `RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor`. Each button currently emits only the `Guid` skill identifier. `RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor` and `CharacterPanel.razor.cs` forward that identifier upward through the `RollRequested` callback without opening any pre-roll UI.
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 request contract for a skill roll lives in `RpgRoller/Contracts/ApiContracts.cs` as `RollSkillRequest`. The HTTP endpoint is `POST /api/skills/{skillId}/roll` in `RpgRoller/Api/SkillEndpoints.cs`. The service contract is `IGameService.RollSkill(...)` in `RpgRoller/Services/IGameService.cs`, implemented by `RpgRoller/Services/GameService.cs`, and executed by `RpgRoller/Services/GameRollService.cs`.
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`.
Rolemaster rolling behavior is implemented in `RpgRoller/Services/RollEngine.cs`, `RolemasterRollEngine.cs`, `RolemasterRetryPolicy.cs`, and `RollBreakdownFormatter.cs`. A “situational modifier” in this plan means a temporary integer that is added to or subtracted from the stored skill expression for one roll only. The stored skill expression is still the canonical thing saved on the skill, such as `d100!+50`. The situational modifier exists only in the request that triggers one roll and in the recorded breakdown text that explains that roll afterward.
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.
UI and regression coverage already exist in the repository areas that matter here. `RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs` covers Rolemaster engine behavior. `RpgRoller.Tests/Api/RolemasterApiTests.cs` covers Rolemaster HTTP behavior. `tests/e2e/smoke.spec.js` covers the browser play flow and already contains Rolemaster smoke tests, including the automatic retry badge path. These are the places to extend rather than creating disconnected new test files.
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
Start by widening the skill-roll request path, but keep the feature transient. In `RpgRoller/Contracts/ApiContracts.cs`, change `RollSkillRequest` so it carries both `Visibility` and `SituationalModifier`, defaulting the latter to `0`. Update `RpgRoller/Contracts/RpgRollerJsonSerializerContext.cs` only if the source generator needs to reflect the changed shape. Then thread the new integer through `RpgRoller/Api/SkillEndpoints.cs`, `RpgRoller/Services/IGameService.cs`, `RpgRoller/Services/GameService.cs`, and `RpgRoller/Services/GameRollService.cs`. In `GameRollService.RollSkill`, resolve the campaign and parsed expression exactly as today, then reject a non-zero situational modifier unless the campaign ruleset is Rolemaster. Reuse the existing authorization and visibility checks unchanged. No database or domain model changes are needed for this part.
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.
After the request pipeline can accept the modifier, update the Rolemaster execution path so the temporary value participates in the actual roll math rather than being bolted on afterward. The cleanest repository-local shape is to extend `RollEngine.Roll(...)` and `RolemasterRollEngine.Roll(...)` with an optional `situationalModifier = 0` argument. Only the Rolemaster branch should consume it. In `RolemasterRollEngine`, add the situational modifier to the expression modifier for both standard Rolemaster rolls and open-ended percentile attempts. The first attempt total that feeds `RolemasterRetryPolicy.ResolveAutoRetryBonus(...)` must already include both the stored skill modifier and the situational modifier. The retry attempt must use the same situational modifier again. Update `RollBreakdownFormatter` so Rolemaster text remains explicit, for example `08+50+20=78` for a normal positive path and `(05) -97 +50 +20 = -22` for a low-end path. The retry breakdown must also preserve this explicit style, for example `08+50+20=78; retry(+5): 42+50+20=112; final=117`.
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.
Then add the pre-roll modal to the workspace play flow. Keep the state and orchestration in the existing play coordinator rather than letting `CharacterPanel` call the API itself. Change the roll callback path from `Guid` to `CharacterSheetSkill` so the coordinator has the skill name and expression immediately. Update `RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor(.cs)` and `CharacterPanel.razor(.cs)` to pass the full skill object upward. In `RpgRoller/Components/Pages/WorkspaceState.cs`, add modal state for whether the prompt is open, which skill is pending, the pending raw modifier text, whether the modal is submitting, and the current validation message. In `RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs`, replace the direct-roll entry point with a Rolemaster-aware method that either opens the modal or immediately rolls with modifier `0` for other rulesets. Add companion methods to confirm and cancel the pending Rolemaster roll.
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.
Render the modal near the bottom of `RpgRoller/Components/Pages/Workspace.razor`, alongside the existing character modals, so it shares the same page-level ownership as other overlays. Create a dedicated component pair at `RpgRoller/Components/Pages/HomeControls/RolemasterSkillRollModal.razor` and `RolemasterSkillRollModal.razor.cs` instead of expanding `CharacterPanel` further. The modal should show the skill name, the stored expression, a short help line explaining that blank means zero and negative numbers are allowed, and a single signed-number input. The input should autofocus via `ElementReference.FocusAsync()`. Escape should cancel. Enter should submit through the form. Clicking the overlay outside the dialog card should cancel, while clicking inside the card must not bubble out. Use the existing `.modal-overlay` and `.modal-card` styling patterns first; only add CSS in `RpgRoller/wwwroot/styles.css` if the modal needs a small amount of spacing or width tuning.
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.
Validation belongs in both UI and server code. The modal should trim whitespace and treat an empty field as `0`. If parsing fails, keep the modal open and show an inline error such as “Enter a whole number like 20, -15, or leave blank for 0.” On the server, reject values outside the same Rolemaster modifier limits already enforced by `DiceRules`, namely `-1000` through `1000`. Reuse the existing API error path so invalid requests still surface as user-facing errors without breaking the page.
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, update documentation and tests. `README.md` must describe the new Rolemaster roll flow in current-state language, not as a changelog note. Extend service tests to prove that a situational modifier changes Rolemaster totals, triggers retry evaluation from the combined total, and applies again on the retry attempt. Extend API tests to prove the new request payload, server-side Rolemaster-only validation, and breakdown text. Extend Playwright smoke coverage so the browser proves the modal opens only on Rolemaster skill rolls, autofocus works, Enter submits, Escape and outside click dismiss, invalid text stays inline, and a positive situational bonus can be used to cause a retry-enabled result.
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
All commands below run from `D:\Code\RpgRoller`.
Run all commands from the repository root, which is `/home/frank/Code/RpgRoller`.
First, implement the request and engine changes, then run targeted tests while the work is still small.
Start by inspecting the current route and auth files before editing:
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --filter "FullyQualifiedName~ServiceRolemasterRollTests|FullyQualifiedName~RolemasterApiTests"
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
After the UI modal is in place, run the browser smoke suite directly.
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
Before closing the iteration, format every touched file using the repository rule. Replace the placeholder list with the exact touched file paths separated by semicolons.
If the app is already running and a faster inner loop is needed, run the checked-in smoke file directly:
jb cleanupcode --build=False RpgRoller/Contracts/ApiContracts.cs;RpgRoller/Api/SkillEndpoints.cs;RpgRoller/Services/IGameService.cs;RpgRoller/Services/GameService.cs;RpgRoller/Services/GameRollService.cs;RpgRoller/Services/RollEngine.cs;RpgRoller/Services/RolemasterRollEngine.cs;RpgRoller/Services/RollBreakdownFormatter.cs;RpgRoller/Components/Pages/Workspace.razor;RpgRoller/Components/Pages/WorkspaceState.cs;RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs;RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor;RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs;RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor;RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs;RpgRoller/Components/Pages/HomeControls/RolemasterSkillRollModal.razor;RpgRoller/Components/Pages/HomeControls/RolemasterSkillRollModal.razor.cs;RpgRoller/wwwroot/styles.css;RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs;RpgRoller.Tests/Api/RolemasterApiTests.cs;tests/e2e/smoke.spec.js;README.md
npm run e2e:smoke
Run the repository-wide local CI script as the final proof.
After each milestone that touches C# files, run the relevant test suite and then the full backend suite before moving on:
pwsh ./scripts/ci-local.ps1
dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj --collect:"XPlat Code Coverage" --settings RpgRoller.Tests/coverlet.runsettings
Then create one brief commit for the iteration.
git add TASKS.md README.md RpgRoller/Contracts/ApiContracts.cs RpgRoller/Api/SkillEndpoints.cs RpgRoller/Services/IGameService.cs RpgRoller/Services/GameService.cs RpgRoller/Services/GameRollService.cs RpgRoller/Services/RollEngine.cs RpgRoller/Services/RolemasterRollEngine.cs RpgRoller/Services/RollBreakdownFormatter.cs RpgRoller/Components/Pages/Workspace.razor RpgRoller/Components/Pages/WorkspaceState.cs RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor RpgRoller/Components/Pages/HomeControls/CharacterPanel.razor.cs RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs RpgRoller/Components/Pages/HomeControls/RolemasterSkillRollModal.razor RpgRoller/Components/Pages/HomeControls/RolemasterSkillRollModal.razor.cs RpgRoller/wwwroot/styles.css RpgRoller.Tests/Services/ServiceRolemasterRollTests.cs RpgRoller.Tests/Api/RolemasterApiTests.cs tests/e2e/smoke.spec.js
git commit -m "Add Rolemaster situational roll modifier prompt"
Expected proof points during implementation are:
A Rolemaster skill such as d100!+50 opens the modal instead of rolling immediately.
Leaving the field blank and pressing Enter records a normal roll.
Typing 20 and pressing Enter records a breakdown that visibly contains +20.
If the first attempt becomes 76 through 110 after adding the situational modifier, the existing retry flow still fires.
A D6 or D&D 5e skill still rolls immediately with no popup.
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
Acceptance is behavioral, not just “the code compiles.”
The final implementation is acceptable only if all of the following behaviors are true and visible.
Start the browser smoke environment with `pwsh ./scripts/run-playwright.ps1` or run the application locally and navigate to the play screen. Create or seed a Rolemaster campaign, a character, and an open-ended skill such as `Observation` with `d100!+50`, `fumbleRange: 5`, and `rolemasterAutoRetry: true`.
Anonymous navigation:
On the play screen, clicking `Roll Observation` must open a modal dialog. The modifier input must receive focus immediately. Pressing Escape must close the dialog with no roll recorded. Clicking the backdrop outside the dialog must also close it with no roll recorded. Reopening the dialog and pressing Enter with the field blank must submit a normal zero-modifier roll.
`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`.
Reopen the dialog and enter `20`. After confirming, the recorded roll detail must show the temporary modifier as a separate term in the breakdown. If the first computed attempt lands in the retry window, the breakdown must show the same `+20` in both attempts and the final total must reflect the retry bonus on top of the retry attempt. The compact log entry should still show the existing retry badge behavior, and expanding the detail should show the existing attempt markers on the dice chips.
Authenticated navigation:
Run `dotnet test RpgRoller.Tests/RpgRoller.Tests.csproj` and expect the suite to pass. The new or updated tests should fail before the feature is implemented and pass after it is complete. Run `pwsh ./scripts/ci-local.ps1` and expect the build, tests, coverage gate, and Playwright smoke test to pass end-to-end.
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 feature should be implemented additively and is safe to retry. Re-running the same code-edit steps should only replace the intended current-state logic. No migration is expected for this feature because the modifier is transient. If a draft implementation accidentally introduces persistence for the modifier, remove that persistence before considering the work complete.
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.
If the UI modal gets into a bad state during development, the safe recovery path is to clear only the workspace prompt state in `WorkspaceState` and retry the interaction. If Playwright fails because the temporary application process is still running, stop the lingering `dotnet` process once, rerun the script, and confirm the health endpoint responds before rechecking the smoke suite.
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
The most important visible transcript to preserve during implementation is the breakdown text for a retry-causing situational bonus. A representative successful result should look like this shape, with different random numbers allowed:
Current evidence that must be retired by this rewrite:
08+50+20=78; retry(+5): 42+50+20=112; final=117
RpgRoller.Tests/Api/FrontendHostTests.cs
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
Assert.Contains("_framework/blazor.web.js", html);
The corresponding browser-level proof should include a log row that shows the final result, retains the existing retry badge, and expands into detail whose dice titles still include attempt markers such as:
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", ...)
Roll 8, step 1, attempt 1, Rolemaster open-ended initial
Roll 42, step 1, retry attempt 2, Rolemaster open-ended initial
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
At the end of the implementation, these repository interfaces should exist in the following shapes.
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.
In `RpgRoller/Contracts/ApiContracts.cs`, define:
At the end of Milestone 1, the codebase must contain a host-level entry point with behavior equivalent to:
public sealed record RollSkillRequest(string Visibility, int SituationalModifier = 0);
GET /
if session cookie maps to a valid user: redirect to /play
otherwise: redirect to /login
In `RpgRoller/Services/IGameService.cs`, define:
At the end of Milestone 2, the codebase must contain route components equivalent to:
ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility, int situationalModifier = 0);
/play
/campaigns
/admin
In `RpgRoller/Services/GameRollService.cs`, define a matching method:
Each of those routes must be directly navigable and refreshable.
public ServiceResult<RollResult> RollSkill(string sessionToken, Guid skillId, string visibility, int situationalModifier)
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:
In `RpgRoller/Services/RollEngine.cs`, extend the Rolemaster path with:
RpgRoller/Components/Pages/AuthenticatedShell.razor
RpgRoller/Components/Pages/PlayPage.razor
RpgRoller/Components/Pages/CampaignsPage.razor
RpgRoller/Components/Pages/AdminPage.razor
public (int Total, string Breakdown, IReadOnlyList<RollDieResult> Dice) Roll(
RulesetKind ruleset,
DiceExpression expression,
int wildDice,
bool allowFumble,
int? fumbleRange,
bool rolemasterAutoRetry = false,
int situationalModifier = 0)
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.
In `RpgRoller/Components/Pages/HomeControls/SkillGroupBlock.razor.cs` and `CharacterPanel.razor.cs`, change the roll callback shape to carry the full skill:
## Revision Note
[Parameter]
public EventCallback<CharacterSheetSkill> RollSkillRequested { get; set; }
[Parameter]
public EventCallback<CharacterSheetSkill> RollRequested { get; set; }
Create a new modal component at `RpgRoller/Components/Pages/HomeControls/RolemasterSkillRollModal.razor` and `.razor.cs` that accepts at least the visibility flag, skill label, expression label, raw modifier text, submit state, and confirm/cancel callbacks. The component should use only existing Blazor and repository dependencies; no third-party modal library is required or desired.
Plan revision note (2026-04-14 / Codex): created the initial ExecPlan for the new Rolemaster one-shot situational modifier modal because `TASKS.md` was empty and the feature needs a self-contained implementation guide before coding begins.
Plan revision note (2026-04-14 / Codex): updated the living plan after the first backend slice landed so progress, discoveries, and retrospective match the new transient request path, explicit breakdown formatting, and added service/API coverage.
Plan revision note (2026-04-14 / Codex): updated the living plan again after the UI slice completed so the document now reflects the shipped modal behavior, the Playwright coverage, and the final end-to-end outcome.
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.