From a2e130abb195c9622ff5125400cc1909319e4bc6 Mon Sep 17 00:00:00 2001 From: Frank Tovar Date: Sun, 5 Apr 2026 00:53:23 +0200 Subject: [PATCH] Updated TASKS.md --- TASKS.md | 981 ++++++++++--------------------------------------------- 1 file changed, 168 insertions(+), 813 deletions(-) diff --git a/TASKS.md b/TASKS.md index 6b2a855..3153d2f 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1,907 +1,262 @@ -# Refactor Blueprint: GameService and Workspace +# Finish GameService and Workspace Decomposition -## Purpose +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. -This document is the implementation blueprint for splitting two oversized classes without changing product behavior: +`PLANS.md` is checked into the repository root. This document must be maintained in accordance with `PLANS.md`. -- `RpgRoller/Services/GameService.cs` -- `RpgRoller/Components/Pages/Workspace.razor.cs` +## Purpose / Big Picture -The goal is to reduce future churn, make responsibilities explicit, improve testability, and create stable seams for future features. +This work finishes a refactor that is already partly complete in the current tree. After the remaining changes are done, the application should behave exactly as it does now for login, campaign management, character management, rolling, live updates, and admin actions, but the code should be organized around small, obvious ownership boundaries instead of two oversized coordinators. -This is a planning artifact only. It does not authorize behavior changes. During implementation, preserve the existing public API contracts and current user-visible behavior unless a separate task explicitly requests functional changes. +The user-visible proof is intentionally boring: after starting the app, logging in, selecting a campaign, rolling skills, opening the admin screen, and letting live updates reconnect, everything should still work the same. The win is for future implementation work. A novice contributor should be able to open a focused file such as `RpgRoller/Services/GameSkillService.cs` or `RpgRoller/Components/Pages/WorkspacePlayCoordinator.cs`, make one change, and not risk unrelated behavior. -## Primary Constraints +## Progress -- Do not use partial classes as the main decomposition strategy. -- Prefer composition of small sealed classes with clear ownership. -- Keep `IGameService` stable during the refactor unless a separate change requests an API redesign. -- Preserve current endpoint contracts and `WorkspaceQueryService` behavior. -- Preserve current workspace UX and screen flow. -- Do not revert unrelated local changes already present in the repo. -- Keep changes incremental and validation-heavy. +- [x] (2026-04-04 22:46Z) Re-read `AGENTS.md` and `PLANS.md`, then converted this file from a blueprint into a self-contained ExecPlan. +- [x] (2026-04-04 22:46Z) Inspected the current backend state. `RpgRoller/Services/GameService.cs`, `GameStateStore.cs`, `GamePersistenceService.cs`, `GameAuthService.cs`, `GameCampaignService.cs`, `GameCharacterService.cs`, `GameSkillService.cs`, `GameRollService.cs`, and `GameUserAdministrationService.cs` already exist. +- [x] (2026-04-04 22:46Z) Inspected the current frontend state. `RpgRoller/Components/Pages/WorkspaceState.cs`, `WorkspaceSessionCoordinator.cs`, `WorkspaceCampaignCoordinator.cs`, `WorkspaceCampaignScopeCoordinator.cs`, `WorkspacePlayCoordinator.cs`, `WorkspaceAdminCoordinator.cs`, `WorkspaceLiveStateController.cs`, `WorkspaceFeedbackService.cs`, and `WorkspaceToast.cs` already exist. +- [x] (2026-04-04 22:46Z) Marked the large structural extractions as already done in this plan instead of treating the repository as pre-refactor. +- [ ] Complete backend shared-helper consolidation. Remaining work: move duplicated authorization, session resolution, campaign-context resolution, mapping, and campaign-state version helpers out of the domain services and into dedicated helper files or `GameStateStore` methods. +- [ ] Complete backend roll decomposition. Remaining work: extract the dice engines, breakdown formatting, and compact log summary logic out of `RpgRoller/Services/GameRollService.cs` while preserving all existing roll behavior. +- [ ] Finish thinning `RpgRoller/Services/GameService.cs`. Remaining work: remove the remaining startup and campaign-state helper methods so the class only wires collaborators and delegates public `IGameService` calls. +- [ ] Finish thinning `RpgRoller/Components/Pages/Workspace.razor.cs`. Remaining work: remove the large mirror of `WorkspaceState` properties and the excess pass-through wrappers so the file acts as a composition root plus lifecycle and JS-invokable bridge. +- [ ] Update `README.md` and this ExecPlan after the code changes land so the documentation reflects the final, not intermediate, structure. -## Desired End State +## Surprises & Discoveries -### Backend +- Observation: `GameService` is already mostly a thin facade, but it still owns startup and campaign-state rebuilding work. + Evidence: `RpgRoller/Services/GameService.cs` constructs collaborators and delegates public methods, yet still contains `LoadStateFromDatabase`, `RebuildCampaignStateLocked`, `AddCharacterStateLocked`, and `GetOrCreateCampaignStateLocked`. -- `GameService` becomes a thin façade/coordinator. -- Stateful concerns are centralized in a shared state owner instead of spread across one giant class. -- Domain workflows are split into small sealed collaborators. -- Pure logic helpers move into small focused files. -- File boundaries reflect change frequency and business ownership. +- Observation: the backend refactor stopped after file extraction, not after helper consolidation. + Evidence: `RpgRoller/Services/GameCampaignService.cs`, `GameCharacterService.cs`, `GameSkillService.cs`, `GameRollService.cs`, and `GameUserAdministrationService.cs` each still define their own `ResolveUserLocked`, campaign visibility logic, owner display mapping, or campaign-state mutation helpers. -### Frontend +- Observation: `GameRollService` is now the main backend monolith. + Evidence: `RpgRoller/Services/GameRollService.cs` still owns authorization checks, campaign-context resolution, D6 logic, Rolemaster logic, log summary formatting, event badge generation, JSON dice serialization, and campaign-state snapshot shaping in one file. -- `Workspace` becomes a thin Blazor component coordinator. -- State, orchestration, admin actions, play/log actions, and live event synchronization live in separate sealed collaborators. -- Razor bindings reference composed state/actions objects instead of one monolithic code-behind surface. -- Existing child component boundaries remain intact unless a very small binding adjustment is needed. +- Observation: the frontend refactor introduced one extra collaborator that was not named in the original blueprint, and that collaborator is worth keeping. + Evidence: `RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs` now owns selected-campaign reload, selected-character synchronization, log reset, and unauthorized-session handling. Those behaviors are cohesive and should not be pushed back into `Workspace.razor.cs`. -## Current Problems To Solve +- Observation: `Workspace.razor.cs` is structurally better than before but still too wide because it mirrors state into local aliases. + Evidence: `RpgRoller/Components/Pages/Workspace.razor.cs` contains a large block of properties such as `private UserSummary? User { get => State.User; set => State.User = value; }` and many one-line delegates such as `private Task RollSkillAsync(Guid skillId) => Play.RollSkillAsync(skillId);`. -### GameService +- Observation: `README.md` already describes the repository as partially refactored. + Evidence: the current `README.md` names the extracted service and coordinator files directly, so the final implementation must update that description in place rather than add a historical change log. -Observed concerns mixed together today: +## Decision Log -- authentication and session lifecycle -- admin user management -- campaign lifecycle -- character lifecycle and ownership transfer -- skill and skill-group lifecycle -- dice validation and roll execution -- campaign log shaping and roll-detail formatting -- DTO mapping -- in-memory state tracking -- persistence load/save/cloning +- Decision: Treat the repository as partially completed work, not as a blank implementation target. + Rationale: the current tree already contains the major backend and frontend file extractions, and an accurate ExecPlan must let a novice resume from the current state without redoing finished work. + Date/Author: 2026-04-04 / Codex -This creates the wrong coupling pattern: +- Decision: Keep `RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs` as a first-class collaborator in the plan. + Rationale: it already owns coherent campaign-scope behavior and reduces the chance that unauthorized-session handling and selection refresh logic drift back into `Workspace.razor.cs`. + Date/Author: 2026-04-04 / Codex -- changing one workflow risks merge conflicts with unrelated work -- high-level service methods depend on low-level helper implementation details -- lock/state/persistence concerns are interleaved with domain logic -- roll engine logic is too close to CRUD flows -- pure helpers are harder to discover and test in isolation +- Decision: Use the remaining work to consolidate shared backend helpers instead of creating a second round of large, nearly duplicated services. + Rationale: most of the benefit of the first extraction is already present. The missing value now is shared helper ownership, not more top-level service files. + Date/Author: 2026-04-04 / Codex -### Workspace +- Decision: Keep validation instructions in this ExecPlan even though this revision is documentation-only. + Rationale: `PLANS.md` requires executable validation guidance, but the user explicitly requested no CI or test work for this pass. The commands remain here for the implementation pass that follows later. + Date/Author: 2026-04-04 / Codex -Observed concerns mixed together today: +## Outcomes & Retrospective -- initial bootstrap -- session reload/logout -- screen selection and persistence -- campaign selection and scope refresh -- management actions -- play-screen actions -- campaign log detail caching -- admin actions -- live state event synchronization -- toast/status handling -- UI state storage and computed properties +This document rewrite did not change application behavior. It changed the planning artifact so that it now matches the code that is already in the repository. A future contributor can start from this file alone and understand which refactor steps are complete, which files already exist, and which structural seams are still missing. -This creates the wrong coupling pattern: +The remaining work is narrower than the original blueprint implied. The repository no longer needs first-wave extraction. It needs second-wave cleanup: shared helper consolidation, roll-engine breakup, final facade thinning, and Razor binding simplification. That narrower scope should keep the next implementation iterations small. -- every change lands in one file -- state mutation rules are difficult to audit -- JS interop ownership is spread across unrelated methods -- event-driven refresh logic is coupled to screen and toast logic -- the Razor file binds against too many members on one type +## Context and Orientation -## Architecture Direction +This repository is an ASP.NET Core and Blazor Server application. The backend gameplay API is centered on `RpgRoller/Services/IGameService.cs` and its concrete implementation `RpgRoller/Services/GameService.cs`. The frontend authenticated workspace is centered on `RpgRoller/Components/Pages/Workspace.razor` and `RpgRoller/Components/Pages/Workspace.razor.cs`. -## 1. Backend Composition Strategy +A "thin facade" in this plan means a class that mainly wires collaborators and forwards calls. It does not own substantial business logic. A "composition root" means the place where collaborators are constructed and connected together. In this repository, `GameService` and `Workspace.razor.cs` should be composition roots. A "campaign-state tracker" means the in-memory version counters used to decide whether a roster, character sheet, or log changed. Those counters currently live in `RpgRoller/Services/GameStateStore.cs` as `GameCampaignStateTracker`. -### 1.1 Keep a thin GameService façade +The current backend state is better than the old monolith. `RpgRoller/Services/GameService.cs` already delegates its public methods to `GameAuthService`, `GameCampaignService`, `GameCharacterService`, `GameSkillService`, `GameRollService`, and `GameUserAdministrationService`. `RpgRoller/Services/GamePersistenceService.cs` already owns SQLite loading and saving. `RpgRoller/Services/SkillDefinitionValidator.cs`, `RoleSerializer.cs`, `RollVisibilityParser.cs`, `CustomRollOptionsResolver.cs`, and `GameStateCloneFactory.cs` already exist as helper files. -`GameService` should stay as the application-facing implementation of `IGameService`, but it should stop owning every workflow directly. +The remaining backend problem is duplication. Several services still repeat the same logic for resolving the current user from a session token, checking whether someone can view or edit a campaign, building summaries, and incrementing campaign-state versions. `RpgRoller/Services/GameRollService.cs` is still a large mixed file that combines access checks, dice algorithms, log formatting, and read-model shaping. That is the main backend target. -Target shape: +The current frontend state is also better than the old monolith. `RpgRoller/Components/Pages/WorkspaceState.cs` holds most UI state and many computed projections. Session/bootstrap behavior lives in `WorkspaceSessionCoordinator.cs`. Campaign management and modal flows live in `WorkspaceCampaignCoordinator.cs`. Selected campaign scope refresh lives in `WorkspaceCampaignScopeCoordinator.cs`. Play/log behavior lives in `WorkspacePlayCoordinator.cs`. Admin behavior lives in `WorkspaceAdminCoordinator.cs`. Live event reconciliation lives in `WorkspaceLiveStateController.cs`. Toast and announcement behavior lives in `WorkspaceFeedbackService.cs`. -- constructor wires shared collaborators -- each public `IGameService` method delegates to one domain-oriented sealed service -- the façade owns no domain-heavy private logic -- only lightweight delegation and composition remain in `GameService` +The remaining frontend problem is that `RpgRoller/Components/Pages/Workspace.razor.cs` still mirrors a large amount of `WorkspaceState` into local alias properties and exposes many single-line wrapper methods only because the Razor file has not been fully retargeted to the composed surface. The next pass should delete those mirrors rather than add more wrappers. -This preserves integration points while still removing the monolith. +## Plan of Work -### 1.2 Introduce a shared state owner +### Milestone 1: Consolidate backend shared state and helper ownership -Create a single shared state owner to encapsulate the in-memory model, synchronization gate, and persistence coordination used by the composed backend services. +Begin by removing duplication that is now spread across the backend service files. Extend `RpgRoller/Services/GameStateStore.cs` so it owns the campaign-state tracker operations that are currently repeated in several places. Specifically, move `GetOrCreateCampaignStateLocked`, `AddCharacterStateLocked`, `RemoveCharacterStateLocked`, `TouchRosterLocked`, `TouchCharacterLocked`, `TouchLogLocked`, and `RebuildCampaignStateLocked` into `GameStateStore`. Once those methods exist there, delete the repeated private copies from `GameService`, `GameCharacterService`, `GameSkillService`, `GameRollService`, and `GameUserAdministrationService`. -Candidate responsibility set: - -- lock object -- dictionaries/lists currently stored in `GameService` -- lookup helpers for common state access -- campaign-state tracker storage -- database load/save orchestration hooks +In parallel, create `RpgRoller/Services/GameAuthorization.cs`, `RpgRoller/Services/GameContextResolver.cs`, and `RpgRoller/Services/GameDtoMapper.cs`. Keep them as static helper classes unless an implementation detail forces constructor injection. `GameAuthorization` should own role checks and "can view campaign", "can edit character", and "can view roll" decisions. `GameContextResolver` should own session-token-to-user lookup, campaign-context lookup, and character-to-campaign resolution. `GameDtoMapper` should own summary and response mapping such as user summaries, campaign summaries, campaign rosters, character summaries, character sheets, roll results, and campaign-state snapshots. Update each backend domain service to call these helpers instead of carrying its own copies. The end of this milestone should leave each domain service focused on one workflow family instead of common glue code. -Recommended candidate names: +### Milestone 2: Break up the roll engine and compact log formatting -- `GameStateStore` -- `GameRuntimeState` -- `GameStateCoordinator` +After the shared helpers are in place, split `RpgRoller/Services/GameRollService.cs` into a smaller orchestration file plus algorithmic helpers. Create `RpgRoller/Services/RollEngine.cs` as the dispatcher for ruleset-specific rolling, and create `StandardRollEngine.cs`, `D6RollEngine.cs`, and `RolemasterRollEngine.cs` for the actual dice logic. Move human-readable breakdown formatting into `RpgRoller/Services/RollBreakdownFormatter.cs`. Move compact log summary text, badge generation, and custom-roll expression extraction into `RpgRoller/Services/CampaignLogSummaryBuilder.cs`. -Preferred choice: `GameStateStore` +Keep `GameRollService` responsible for the public workflow: authorize the request, look up the skill or character, ask the roll engine to compute the dice, record the log entry, and return the mapped response. Do not change JSON contracts, roll totals, badge text, breakdown text, or log paging behavior. The easiest way to stay safe is to move logic in place first, preserve existing method names where possible, and only then simplify call sites. -Why: - -- "Store" matches the fact that it owns mutable in-memory state -- it avoids implying EF persistence semantics -- it gives the rest of the services a stable dependency name +### Milestone 3: Finish thinning GameService -This shared state owner must be the only place that exposes the mutable collections and the synchronization gate needed by the domain services. +Once state-version helpers and backend shared helpers are centralized, return to `RpgRoller/Services/GameService.cs`. Remove every remaining private helper there. The constructor should create the shared collaborators, call persistence bootstrap, and stop. Startup loading should happen through `GamePersistenceService.LoadStateFromDatabase()` plus `GameStateStore.RebuildCampaignStateLocked()` called from one place only. `GameService` should retain public `IGameService` methods and nothing more than lightweight delegation and ruleset enumeration. -### 1.3 Split backend workflows into domain services +If constructor readability suffers, create small private factory methods inside `GameService` only for collaborator construction. Do not reintroduce domain behavior there. The acceptance standard for this milestone is that a reader can scan the entire file quickly and understand all wiring without scrolling through business logic. -Create sealed services grouped by real business seams, not arbitrary line-count slices. +### Milestone 4: Finish thinning Workspace and clean up Razor bindings -Recommended services: +With the backend stable, finish the frontend cleanup. Keep `RpgRoller/Components/Pages/WorkspaceCampaignScopeCoordinator.cs` as the owner of selected-campaign scope refresh. In `RpgRoller/Components/Pages/Workspace.razor.cs`, delete the mirror block that copies `WorkspaceState` into local aliases such as `User`, `Campaigns`, `SelectedCampaign`, and the log-detail dictionaries. Keep `State` public to the Razor file and bind directly through `State` wherever the values are plain data or pure computed projections. -1. `GameAuthService` -2. `GameUserAdministrationService` -3. `GameCampaignService` -4. `GameCharacterService` -5. `GameSkillService` -6. `GameRollService` -7. `GamePersistenceService` - -Each service should be small enough to own one family of change, but large enough that one feature does not have to hop across many tiny orchestration layers. +To support direct binding, move the remaining pure presentation helpers into `RpgRoller/Components/Pages/WorkspaceState.cs`. That includes owner labels and skill-definition labels, because those are pure projections over the selected campaign, selected user, and skill data. Keep imperative flows in the coordinators. The file `Workspace.razor.cs` should still own dependency injection, collaborator construction, lifecycle methods, `JSInvokable` entry points, `DisposeAsync`, and any tiny wrappers needed to satisfy component callback signatures. It should stop looking like a second state bag. -### 1.4 Extract pure helper classes into separate files +Then update `RpgRoller/Components/Pages/Workspace.razor` so it uses the composed surface consistently. Data should come from `State`, management actions from `Campaigns`, play actions from `Play`, admin actions from `Admin`, and session actions from `Session`. `Scope` may remain an internal collaborator if it simplifies orchestration and keeps the markup readable. Avoid creating deep object chains; clear names matter more than ideological purity. -Pure or near-pure helpers should not stay hidden inside the coordinator or domain services. +### Milestone 5: Final documentation alignment -Recommended helper extraction: +After the code refactor is complete, update `README.md` so its code-organization section describes the final structure plainly and completely. Update this ExecPlan in place. Mark completed progress items with timestamps, add any implementation surprises, record final decisions, and write a closing retrospective. Do not add a historical mini-changelog. The documentation should describe the repository as it exists after the refactor lands. -- `SkillDefinitionValidator` -- `RollVisibilityParser` -- `CustomRollOptionsResolver` -- `RollBreakdownFormatter` -- `CampaignLogSummaryBuilder` -- `GameDtoMapper` -- `GameStateCloneFactory` -- `RoleSerializer` +## Concrete Steps -If a helper remains too broad after extraction, split again. Small helper files are desirable here because they are stable, discoverable, and easy to test. +Work from `D:\Code\RpgRoller`. -## 2. Backend Responsibility Map +Start every future implementation pass by re-reading the plan and checking the current tree: -### 2.1 GameService + Get-Content PLANS.md + Get-Content TASKS.md + git status --short + rg --files RpgRoller/Services RpgRoller/Components/Pages -`GameService` should delegate the following methods: +When beginning backend helper consolidation, inspect the current duplication before editing: -- auth/session - - `Register` - - `Login` - - `Logout` - - `GetUserBySession` - - `GetMe` -- campaigns - - `CreateCampaign` - - `GetCampaigns` - - `GetCharacterCampaignOptions` - - `GetCampaign` - - `DeleteCampaign` -- users/admin - - `GetUsernames` - - `GetUsers` - - `UpdateUserRoles` - - `DeleteUser` -- characters - - `CreateCharacter` - - `UpdateCharacter` - - `DeleteCharacter` - - `ActivateCharacter` - - `GetOwnCharacters` -- skills - - `CreateSkillGroup` - - `UpdateSkillGroup` - - `DeleteSkillGroup` - - `CreateSkill` - - `UpdateSkill` - - `DeleteSkill` - - `GetCharacterSheet` -- rolls/log - - `RollSkill` - - `RollCustom` - - `GetCampaignLog` - - `GetCampaignLogPage` - - `GetRollDetail` - - `GetCampaignStateSnapshot` -- rulesets - - `GetRulesets` - -### 2.2 GameAuthService + Get-Content RpgRoller\Services\GameService.cs + Get-Content RpgRoller\Services\GameCharacterService.cs + Get-Content RpgRoller\Services\GameSkillService.cs + Get-Content RpgRoller\Services\GameRollService.cs + Get-Content RpgRoller\Services\GameUserAdministrationService.cs -Own: +Create or update the shared helper files, then replace each duplicated private helper with calls into the shared files or `GameStateStore`. Keep each change small enough that a single review can verify behavior stayed the same. -- register/login/logout/session lookup -- me-response shaping tied to session and active-character resolution -- username normalization usage in auth flows -- session creation - -Dependencies: - -- `GameStateStore` -- `IPasswordHasher` -- `GamePersistenceService` -- `GameDtoMapper` - -### 2.3 GameUserAdministrationService - -Own: - -- `GetUsernames` -- `GetUsers` -- `UpdateUserRoles` -- `DeleteUser` -- role checks used by admin flows - -Dependencies: - -- `GameStateStore` -- `GamePersistenceService` -- `GameDtoMapper` -- `RoleSerializer` -- common authorization helpers +When beginning frontend cleanup, inspect the current composition surface before editing: -### 2.4 GameCampaignService + Get-Content RpgRoller\Components\Pages\Workspace.razor.cs + Get-Content RpgRoller\Components\Pages\Workspace.razor + Get-Content RpgRoller\Components\Pages\WorkspaceState.cs + Get-Content RpgRoller\Components\Pages\WorkspaceCampaignScopeCoordinator.cs + Get-Content RpgRoller\Components\Pages\WorkspacePlayCoordinator.cs -Own: +Move pure projections into `WorkspaceState`, simplify the composition root, and then retarget the Razor file to the composed surface. Keep the child components in `RpgRoller/Components/Pages/HomeControls/` stable unless a binding signature must change to support the cleanup. -- create/list/get/delete campaign flows -- campaign visibility rules -- campaign context resolution for viewable campaigns -- campaign roster shaping - -Dependencies: - -- `GameStateStore` -- `GamePersistenceService` -- `GameDtoMapper` -- shared authorization/context helpers - -### 2.5 GameCharacterService - -Own: - -- create/update/delete/activate character flows -- owner transfer rules -- active-character consistency rules -- own-character listing -- character-to-campaign state tracker updates - -Dependencies: - -- `GameStateStore` -- `GamePersistenceService` -- `GameDtoMapper` -- shared authorization/context helpers - -### 2.6 GameSkillService - -Own: - -- skill-group CRUD -- skill CRUD -- character sheet read model -- skill-group assignment rules -- skill definition validation orchestration +When code work resumes later, validate after each meaningful iteration with the repo-standard commands: -Dependencies: + pwsh ./scripts/ci-local.ps1 + pwsh ./scripts/run-playwright.ps1 -- `GameStateStore` -- `GamePersistenceService` -- `GameDtoMapper` -- `SkillDefinitionValidator` -- shared authorization/context helpers +The expected result is simple: no failing tests, no coverage regression, and the Playwright smoke flow completes successfully against a temporary database. -### 2.7 GameRollService +## Validation and Acceptance -Own: +This documentation-only revision intentionally did not run CI, tests, or Playwright. Those checks remain mandatory when implementation resumes. -- `RollSkill` -- `RollCustom` -- campaign log reads -- log page reads -- roll detail reads -- campaign state snapshot reads -- roll recording -- log visibility checks -- compact log summary generation +The backend is accepted when `RpgRoller/Services/GameService.cs` contains only collaborator wiring, ruleset enumeration, and public delegation; when shared authorization, context, mapping, and campaign-state helper logic each live in one place; and when `RpgRoller/Services/GameRollService.cs` no longer embeds the dice engines or compact log summary builders. -Dependencies: +The frontend is accepted when `RpgRoller/Components/Pages/Workspace.razor.cs` reads like a composition root instead of a state mirror, and when `RpgRoller/Components/Pages/Workspace.razor` binds primarily through `State`, `Session`, `Campaigns`, `Play`, and `Admin`. -- `GameStateStore` -- `GamePersistenceService` -- `GameDtoMapper` -- `SkillDefinitionValidator` only if needed for custom roll flows -- `RollVisibilityParser` -- `RollEngine` -- `RollBreakdownFormatter` -- `CampaignLogSummaryBuilder` +The user-visible acceptance flow is: -### 2.8 GamePersistenceService +Log in through the home page, land in the authenticated workspace, switch between Play and Campaign Management, create or edit a character, roll a skill, expand a roll detail row, and open the Admin screen as an admin user. Confirm that live connection status still updates, the selected campaign still persists, and newly recorded rolls still appear and auto-expand as before. -Own: +The repo-level acceptance flow is: -- load state from database at startup -- persist current runtime state -- clone/snapshot helpers used for persistence boundaries +Run `pwsh ./scripts/ci-local.ps1` from `D:\Code\RpgRoller` and expect the script to finish without failed tests or coverage failures. After any frontend-affecting iteration, run `pwsh ./scripts/run-playwright.ps1` and expect the smoke test to complete successfully. -Dependencies: +## Idempotence and Recovery -- `IDbContextFactory` -- `GameStateStore` -- `GameStateCloneFactory` +This refactor should stay additive and repeatable. Re-reading the repo and re-running the inspection commands is always safe. Re-applying small helper extractions is safe as long as the public contracts remain unchanged. -Implementation note: +No database schema change is planned here. If implementation work needs a running app for manual verification, prefer using a temporary SQLite database through `ConnectionStrings__RpgRoller` rather than reusing a valuable local database file. If a partial edit leaves the repository uncompilable, fix forward by finishing the helper extraction or by restoring the affected file to the last committed content through a normal edit, not through destructive git reset commands. -- keep EF persistence concerns here -- do not let other domain services talk directly to EF unless there is a deliberate redesign later +Keep commits small. If the work splits into multiple commits, pause after each commit and update the `Progress` section before continuing, so the plan remains restartable from the current tree. -## 3. Backend Cross-Cutting Helpers +## Artifacts and Notes -Some logic is shared across multiple domain services but should still remain explicit instead of hidden in the state store. +The current tree already shows the intended direction. These excerpts are the key evidence that the refactor is partway done but not finished: -Recommended small helper files: + public sealed class GameService : IGameService + { + public GameService(...) + { + m_StateStore = new(); + m_PersistenceService = new(dbContextFactory, m_StateStore); + m_AuthService = new(m_StateStore, passwordHasher, m_PersistenceService); + ... + LoadStateFromDatabase(); + } + } -### Authorization and context +That excerpt shows the facade wiring is already present, but the startup helper still sits in `GameService`. -- `GameAuthorization` - - user role check helpers - - can-view campaign - - can-edit character - - can-view roll + private UserSummary? User { get => State.User; set => State.User = value; } + private Task RollSkillAsync(Guid skillId) => Play.RollSkillAsync(skillId); -- `GameContextResolver` - - resolve user from session - - resolve campaign context - - resolve character campaign - - resolve current campaign id +Those excerpts from `Workspace.razor.cs` show the remaining mirror and pass-through pattern that should be deleted in the final cleanup. -These can be static helper classes if they remain pure over store inputs, or sealed services if constructor-injected collaborators make the code cleaner. Prefer the simpler shape once implementation starts. + public sealed class WorkspaceCampaignScopeCoordinator + { + public async Task RefreshCampaignScopeAsync() + { + ... + await RefreshCampaignRosterAsync(); + await m_RefreshSelectedCharacterSheetAsync(); + await m_RefreshCampaignLogAsync(null); + ... + } + } -### Mapping +That excerpt shows the frontend already has a real collaborator worth preserving rather than folding back into `Workspace`. -- `GameDtoMapper` - - `ToUserSummary` - - `ToAdminUserSummary` - - `ToCampaignOption` - - `ToCampaignSummary` - - `ToCampaignRoster` - - `ToCharacterSheet` - - `ToCharacterSummary` - - `ToCampaignStateSnapshot` - - `ToSkillGroupSummary` - - `ToSkillSummary` - - `ToRollResult` - - `ToLogEntry` - - `ToLogListEntry` +## Interfaces and Dependencies -### Persistence and clone helpers +The implementation should end with the following stable helper surfaces. -- `GameStateCloneFactory` - - clone user/session/campaign/character/skill/skill-group/roll-log entry +In `RpgRoller/Services/GameStateStore.cs`, define instance methods that own campaign-state tracker mutation: -- `RoleSerializer` - - parse/serialize/normalize roles + public GameCampaignStateTracker GetOrCreateCampaignStateLocked(Guid campaignId) + public void RebuildCampaignStateLocked() + public void AddCharacterStateLocked(Guid? campaignId, Guid characterId) + public void RemoveCharacterStateLocked(Guid? campaignId, Guid characterId) + public void TouchRosterLocked(Guid? campaignId) + public void TouchCharacterLocked(Guid? campaignId, Guid characterId) + public void TouchLogLocked(Guid? campaignId) -### Roll engine helpers +In `RpgRoller/Services/GameAuthorization.cs`, define static helpers for shared access rules: -- `SkillDefinitionValidator` - - expression parse/option validation + public static bool HasRole(UserAccount user, string role) + public static bool CanViewCampaign(GameStateStore stateStore, Guid actorUserId, Guid campaignId) + public static bool CanEditCharacter(Guid actorUserId, Character character, Campaign campaign) + public static bool CanViewRoll(Guid actorUserId, Campaign campaign, RollLogEntry entry) -- `RollEngine` - - top-level roll dispatch by ruleset +In `RpgRoller/Services/GameContextResolver.cs`, define static helpers for shared lookups: -- `StandardRollEngine` -- `D6RollEngine` -- `RolemasterRollEngine` + public static UserAccount? ResolveUserLocked(GameStateStore stateStore, string sessionToken) + public static ServiceResult<(UserAccount User, Campaign Campaign)> ResolveCampaignContextLocked(GameStateStore stateStore, string sessionToken, Guid campaignId) + public static bool TryResolveCharacterCampaignLocked(GameStateStore stateStore, Character character, out Campaign campaign, out ServiceError? error) -- `RollBreakdownFormatter` - - shared textual breakdown formatting +In `RpgRoller/Services/GameDtoMapper.cs`, define static mapping helpers for the backend read models returned by the services. At minimum, this file must own user summaries, admin user summaries, campaign options, campaign summaries, campaign rosters, character summaries, character sheets, roll results, campaign log entries, campaign log list entries, and campaign-state snapshots. -- `CampaignLogSummaryBuilder` - - compact summary + badges + expression extraction helpers +In `RpgRoller/Services/RollEngine.cs`, define a small dispatcher class with one public method that receives a `RulesetKind`, a parsed `DiceExpression`, the D6 wild-die settings, and the optional Rolemaster fumble range, then returns `(int Total, string Breakdown, IReadOnlyList Dice)`. The ruleset-specific implementations should live in `StandardRollEngine.cs`, `D6RollEngine.cs`, and `RolemasterRollEngine.cs`. -The roll-engine area is a strong candidate for several very small files because the behavior is algorithmic and changes independently from CRUD flows. +In `RpgRoller/Services/RollBreakdownFormatter.cs`, keep only pure string-formatting helpers. In `RpgRoller/Services/CampaignLogSummaryBuilder.cs`, keep only pure compact-log helpers such as summary text, event badges, and custom-roll expression extraction. -## 4. Recommended Backend File Layout +In `RpgRoller/Components/Pages/Workspace.razor.cs`, the final composed surface should be limited to: -One reasonable target layout: + private WorkspaceState State { get; } + private WorkspaceSessionCoordinator Session { get; } + private WorkspaceCampaignCoordinator Campaigns { get; } + private WorkspaceCampaignScopeCoordinator Scope { get; } + private WorkspacePlayCoordinator Play { get; } + private WorkspaceAdminCoordinator Admin { get; } + private WorkspaceLiveStateController Live { get; } + private WorkspaceFeedbackService Feedback { get; } -```text -RpgRoller/Services/ - GameService.cs - GameStateStore.cs - GamePersistenceService.cs - GameAuthService.cs - GameUserAdministrationService.cs - GameCampaignService.cs - GameCharacterService.cs - GameSkillService.cs - GameRollService.cs - GameAuthorization.cs - GameContextResolver.cs - GameDtoMapper.cs - GameStateCloneFactory.cs - RoleSerializer.cs - SkillDefinitionValidator.cs - RollVisibilityParser.cs - CustomRollOptionsResolver.cs - RollEngine.cs - StandardRollEngine.cs - D6RollEngine.cs - RolemasterRollEngine.cs - RollBreakdownFormatter.cs - CampaignLogSummaryBuilder.cs -``` +The component may keep tiny wrapper methods for lifecycle, `JSInvokable` entry points, disposal, and menu callbacks, but it should not keep a second copy of the state model. -Exact filenames can be adjusted during implementation if a nearby convention in the repo suggests better naming. The important part is the separation of responsibilities, not the precise suffix. +In `RpgRoller/Components/Pages/WorkspaceState.cs`, keep all plain state plus pure computed and formatting helpers needed directly by the Razor file. That includes selected campaign name, selected play character projections, screen flags, connection-state label and CSS class, app CSS class, owner labels, and skill-definition labels. -## 5. Frontend Composition Strategy - -### 5.1 Keep Workspace as the component boundary - -`Workspace` should remain the Blazor component type used by `Workspace.razor`, but it should stop directly owning all workflow code. - -Target shape: - -- `Workspace` keeps injected dependencies and lifecycle entry points -- composed sealed collaborators handle most behavior -- `Workspace` exposes only the state/actions needed by the Razor file - -This avoids breaking the component boundary while still removing the monolith. - -### 5.2 Introduce a dedicated state holder - -Create a single state holder for workspace UI state and view-model data. - -Recommended candidate name: - -- `WorkspaceState` - -Own: - -- authenticated user/session-derived state -- selected campaign and character state -- campaign collections -- rule set collections -- admin user collection -- log detail cache -- toast state -- screen/mobile-panel state -- live connection state -- modal state -- computed helpers that are pure projections over stored state - -This state holder should be easy to inspect and should reduce the risk of hidden cross-method mutations. - -### 5.3 Split workspace orchestration into sealed collaborators - -Recommended services: - -1. `WorkspaceSessionCoordinator` -2. `WorkspaceCampaignCoordinator` -3. `WorkspacePlayCoordinator` -4. `WorkspaceAdminCoordinator` -5. `WorkspaceLiveStateController` -6. `WorkspaceFeedbackService` - -These names can be adjusted, but the responsibilities should stay distinct. - -## 6. Frontend Responsibility Map - -### 6.1 Workspace component - -Own: - -- injected dependencies -- collaborator construction/wiring -- lifecycle delegation -- JS-invokable entry points delegating to collaborators -- final binding surface consumed by Razor - -Keep thin. Do not let it become a second coordinator monolith with only renamed methods. - -### 6.2 WorkspaceState - -Own: - -- scalar UI state -- collections -- selected entities -- modal visibility and form bootstrap models -- log detail cache and loading/error maps -- toast list -- computed view properties - -Recommended computed properties to live here if kept pure: - -- selected campaign name -- selected character -- play-selected campaign -- play-selected character -- play-selected character id -- filtered play skill and skill-group views if still direct projections -- current-user role flags -- campaign delete permission -- screen flags -- connection-state label/css -- app CSS class - -### 6.3 WorkspaceSessionCoordinator - -Own: - -- initial bootstrap sequence -- session reload -- logout flow -- health retry flow -- persisted session-storage reads/writes for screen, panel, campaign, roll visibility -- clearing authenticated state - -Dependencies: - -- `WorkspaceState` -- `WorkspaceQueryService` -- `RpgRollerApiClient` -- `IJSRuntime` -- `WorkspaceLiveStateController` -- `WorkspaceFeedbackService` - -### 6.4 WorkspaceCampaignCoordinator - -Own: - -- campaign reload and selection -- campaign scope refresh -- campaign roster refresh -- character campaign options reload -- management-screen campaign and character mutations -- character modal open/close and bootstrap -- selected-character synchronization - -Dependencies: - -- `WorkspaceState` -- `WorkspaceQueryService` -- `RpgRollerApiClient` -- `IJSRuntime` -- `WorkspaceLiveStateController` -- `WorkspaceFeedbackService` - -### 6.5 WorkspacePlayCoordinator - -Own: - -- selected character activation -- selected character sheet refresh -- skill roll submission -- custom roll handling -- campaign log page refresh -- roll detail expansion/loading/cache trimming -- roll visibility changes -- play-panel errors - -Dependencies: - -- `WorkspaceState` -- `WorkspaceQueryService` -- `RpgRollerApiClient` -- `IJSRuntime` -- `WorkspaceFeedbackService` - -### 6.6 WorkspaceAdminCoordinator - -Own: - -- admin screen access enforcement -- admin user load -- toggle admin role -- delete user - -Dependencies: - -- `WorkspaceState` -- `WorkspaceQueryService` -- `RpgRollerApiClient` -- `IJSRuntime` -- `WorkspaceLiveStateController` -- `WorkspaceFeedbackService` - -### 6.7 WorkspaceLiveStateController - -Own: - -- start/stop SSE state events -- JS invokable state snapshot handling -- JS invokable connection-state updates -- current campaign-state refresh logic -- campaign-state tracking reset logic - -Dependencies: - -- `WorkspaceState` -- `WorkspaceCampaignCoordinator` -- `WorkspacePlayCoordinator` -- `IJSRuntime` - -Critical rule: - -- this class should own the live-update reconciliation logic -- do not let campaign reload logic and live event logic drift into different locations again - -### 6.8 WorkspaceFeedbackService - -Own: - -- status-to-toast behavior -- live announcement updates -- delayed toast dismissal - -Dependencies: - -- `WorkspaceState` -- component refresh callback or dispatcher mechanism - -Implementation note: - -- if callback wiring becomes awkward, this service can remain a helper owned by `Workspace` rather than a DI service -- the important part is isolating feedback behavior from campaign/admin/play workflows - -## 7. Frontend Binding Strategy - -If composition is used, `Workspace.razor` bindings will need to reference state and actions through the new composed surface. - -Example direction: - -- state data from `State` -- management actions from `Campaigns` -- play actions from `Play` -- admin actions from `Admin` -- session/bootstrap actions from `Session` - -Illustrative pattern only: - -```razor - -``` - -Another example: - -```razor - -``` - -Important guardrail: - -- avoid replacing one huge component surface with many deeply chained bindings that are hard to read -- prefer a small number of clearly named composed properties on `Workspace` - -For example: - -- `State` -- `Session` -- `Campaigns` -- `Play` -- `Admin` -- `Live` - -## 8. Recommended Frontend File Layout - -One reasonable target layout: - -```text -RpgRoller/Components/Pages/ - Workspace.razor - Workspace.razor.cs - WorkspaceState.cs - WorkspaceSessionCoordinator.cs - WorkspaceCampaignCoordinator.cs - WorkspacePlayCoordinator.cs - WorkspaceAdminCoordinator.cs - WorkspaceLiveStateController.cs - WorkspaceFeedbackService.cs - WorkspaceToast.cs -``` - -If some files become too small, merge only where the change patterns are clearly the same. Do not merge simply to reduce file count. - -## 9. Implementation Order - -The implementation phase should proceed in small safe steps. - -### Phase 1: Backend enabling extractions - -1. Extract pure helper classes from `GameService` first. -2. Keep `GameService` behavior identical while moving algorithmic helpers out. -3. Add or update tests around the extracted helpers if coverage or confidence drops. - -Why first: - -- lowest integration risk -- creates stable dependencies for later service extraction - -### Phase 2: Introduce GameStateStore and GamePersistenceService - -1. Move runtime collections, gate, and persistence primitives behind the shared state owner. -2. Move database load/save logic into persistence service. -3. Keep `GameService` still acting as the main orchestrator while the shared dependencies settle. - -Why second: - -- makes later service extraction mechanical instead of risky - -### Phase 3: Extract backend domain services - -Suggested order: - -1. auth -2. campaigns -3. characters -4. skills -5. rolls/log -6. admin/users - -This order keeps the highest-risk workflow area, rolls/logs, until the shared helpers and state seams are already stable. - -### Phase 4: Thin GameService façade - -1. Replace remaining logic in `GameService` with delegation only. -2. Confirm constructor wiring stays readable. -3. Re-run tests and local CI. - -### Phase 5: Frontend state extraction - -1. Introduce `WorkspaceState`. -2. Move raw state and pure computed projections first. -3. Keep method behavior temporarily in `Workspace` until state references are stabilized. - -Why first: - -- it shrinks the cognitive load before moving orchestration - -### Phase 6: Frontend coordinator extraction - -Suggested order: - -1. feedback/toasts -2. session/bootstrap -3. campaign management -4. play/log -5. admin -6. live-state controller - -This order reduces the chance that live event logic is extracted before its dependent refresh flows are already stable. - -### Phase 7: Razor binding cleanup - -1. Update bindings to the new composed surface. -2. Keep markup structure stable. -3. Avoid stylistic churn unrelated to composition. - -### Phase 8: Documentation and verification - -1. Update `README.md` code-organization notes if file layout changed materially. -2. Run local CI. -3. Run Playwright smoke flow. -4. Re-check coverage expectations. - -## 10. Testing and Validation Expectations - -During implementation, after each meaningful iteration: - -- run `pwsh ./scripts/ci-local.ps1` -- verify test coverage remains acceptable relative to the repo rules -- add tests where extraction creates untested seams -- after frontend-affecting iterations, run the Playwright smoke flow - -Specific test focus areas: - -### Backend - -- auth/session regression -- campaign visibility rules -- character ownership transfer -- skill-group assignment and validation -- D6 roll behavior -- Rolemaster roll behavior -- custom roll behavior -- log paging and detail visibility -- persistence reload behavior -- admin role mutation and user deletion side effects - -### Frontend - -- workspace bootstrap still restores screen/campaign/panel/visibility preferences -- admin screen still enforces role access -- play screen still refreshes log/roster/sheet correctly -- newly recorded roll still appears and auto-expands as before -- live connection state still updates announcements and fallback state - -## 11. Risks and Guardrails - -### Risk: Hidden behavior drift during backend extraction - -Guardrail: - -- keep `IGameService` contract fixed -- extract pure helpers before orchestration changes -- preserve method-level tests and add new coverage around moved logic - -### Risk: Locking and persistence bugs after splitting GameService - -Guardrail: - -- centralize mutable runtime state in one store -- centralize persistence writes in one service -- do not let each domain service invent its own lock pattern - -### Risk: Workspace composition creates binding sprawl - -Guardrail: - -- expose a small composed surface from `Workspace` -- keep naming direct and role-based -- avoid deep object chains in Razor - -### Risk: Live-state refresh logic breaks after movement - -Guardrail: - -- move refresh logic together with live event handling ownership -- keep campaign roster, selected sheet, and log refresh paths explicit -- verify with Playwright after frontend extraction - -### Risk: Over-fragmentation - -Guardrail: - -- many small helper files are good for pure logic -- domain orchestration services should remain coarse enough to own meaningful workflows -- do not create tiny services that only forward one call with no real ownership - -## 12. Non-Goals - -The implementation phase described by this blueprint should not, by default: - -- redesign API contracts -- redesign the workspace UX -- replace Blazor patterns with a different UI architecture -- change persistence strategy away from the current runtime-state-plus-SQLite model -- introduce unrelated feature work - -## 13. Definition of Done For The Refactor - -The refactor is complete when all of the following are true: - -- `GameService` is a thin coordinator, not a monolith -- backend workflows are owned by composed sealed classes with clear responsibility boundaries -- algorithmic and mapping helpers live in small focused files -- `Workspace` is a thin component coordinator, not a monolithic code-behind -- workspace state and workflow orchestration are separated cleanly -- `Workspace.razor` binds against a readable composed surface -- tests still pass -- local CI still passes -- frontend smoke verification still passes -- documentation matches the resulting structure - -## 14. Implementation Reminder - -During the actual edit phase: - -- keep changes small -- validate often -- do not revert unrelated user changes -- preserve behavior first -- optimize for reducing future churn, not merely reducing line count +Revision note (2026-04-04): Replaced the old blueprint with an ExecPlan, reconciled it against the code already present in the repository, and marked completed versus remaining refactor work after direct file inspection. The reason for this rewrite is that `AGENTS.md` now requires complex refactors to be tracked as ExecPlans maintained under `PLANS.md`.