# Tasks: Persist Encounter **Input**: Design documents from `/specs/008-persist-encounter/` **Prerequisites**: plan.md, spec.md, research.md, data-model.md **Tests**: Included -- persistence logic warrants unit tests to cover serialization, validation, and error handling. **Organization**: Tasks are grouped by user story to enable independent implementation and testing of each story. ## Format: `[ID] [P?] [Story] Description` - **[P]**: Can run in parallel (different files, no dependencies) - **[Story]**: Which user story this task belongs to (e.g., US1, US2, US3) - Include exact file paths in descriptions ## Phase 1: Setup **Purpose**: Create the persistence module structure - [x] T001 Create persistence module directory at `apps/web/src/persistence/` --- ## Phase 2: Foundational (Blocking Prerequisites) **Purpose**: Core storage adapter that all user stories depend on - [x] T002 Implement `saveEncounter(encounter: Encounter): void` function in `apps/web/src/persistence/encounter-storage.ts` that serializes encounter state to localStorage under key `"initiative:encounter"` using `JSON.stringify`, wrapped in try/catch that silently swallows errors (quota exceeded, storage unavailable) - [x] T003 Implement `loadEncounter(): Encounter | null` function in `apps/web/src/persistence/encounter-storage.ts` that reads from localStorage key `"initiative:encounter"`, parses JSON, performs structural shape checks (object with `combatants` array, `activeIndex` number, `roundNumber` number; each combatant has `id` string and `name` string), rehydrates `CombatantId` values via `combatantId()`, validates through `createEncounter`, and returns `null` on any failure (parse error, shape mismatch, domain validation failure) - [x] T004 Write unit tests for `saveEncounter` and `loadEncounter` in `apps/web/src/persistence/__tests__/encounter-storage.test.ts` covering: round-trip save/load preserves encounter state, `loadEncounter` returns `null` when localStorage is empty, returns `null` for non-JSON strings, returns `null` for JSON missing required fields, returns `null` for invalid encounter data (e.g. empty combatants array, out-of-bounds activeIndex) **Checkpoint**: Storage adapter is complete and tested -- user story implementation can now begin --- ## Phase 3: User Story 1 - Encounter Survives Page Reload (Priority: P1) MVP **Goal**: Encounter state persists across page reloads with zero data loss **Independent Test**: Set up an encounter with combatants, initiative values, and advanced turns. Call `saveEncounter`, then `loadEncounter` and verify all state matches. In the hook, verify `useEffect` triggers `saveEncounter` on state changes. ### Tests for User Story 1 - [x] T005 [US1] Write tests in `apps/web/src/persistence/__tests__/encounter-storage.test.ts` for: round-trip preserves combatant IDs, names, and initiative values; round-trip preserves activeIndex and roundNumber; saving after modifications (add/remove combatant, change initiative) persists the latest state ### Implementation for User Story 1 - [x] T006 [US1] Modify `useEncounter` hook in `apps/web/src/hooks/use-encounter.ts` to initialize state from `loadEncounter()` -- if it returns a valid encounter, use it instead of `createDemoEncounter()`; derive `nextId` counter from highest numeric suffix in existing combatant IDs (parse `c-{N}` pattern) - [x] T007 [US1] Add a `useEffect` in `apps/web/src/hooks/use-encounter.ts` that calls `saveEncounter(encounter)` whenever the encounter state changes **Checkpoint**: User Story 1 is fully functional -- encounter survives page reload --- ## Phase 4: User Story 2 - Fresh Start with No Saved Data (Priority: P2) **Goal**: First-time users see the default demo encounter **Independent Test**: With no localStorage data, load the app and verify the demo encounter (Aria, Brak, Cael) is displayed. ### Implementation for User Story 2 - [x] T008 [US2] Verify and document in tests at `apps/web/src/persistence/__tests__/encounter-storage.test.ts` that `loadEncounter()` returns `null` when localStorage has no `"initiative:encounter"` key, confirming the `useEncounter` hook falls back to `createDemoEncounter()` **Checkpoint**: User Story 2 confirmed -- no saved data results in demo encounter --- ## Phase 5: User Story 3 - Graceful Handling of Corrupt Data (Priority: P3) **Goal**: Corrupt or invalid saved data never crashes the app; falls back to demo encounter **Independent Test**: Write various malformed values to the `"initiative:encounter"` localStorage key and verify `loadEncounter()` returns `null` for each. ### Tests for User Story 3 - [x] T009 [US3] Add tests in `apps/web/src/persistence/__tests__/encounter-storage.test.ts` for corrupt data scenarios: non-object JSON (string, number, array, null), object with wrong types for fields (combatants as string, activeIndex as string), combatant entries missing `id` or `name`, valid JSON structure but domain-invalid data (zero combatants, negative roundNumber) ### Implementation for User Story 3 - [x] T010 [US3] Verify `loadEncounter()` structural checks in `apps/web/src/persistence/encounter-storage.ts` cover all corrupt data scenarios from T009 -- adjust shape validation if any test cases reveal gaps **Checkpoint**: All corrupt data scenarios handled gracefully --- ## Phase 6: Polish & Cross-Cutting Concerns - [x] T011 Run `pnpm check` to verify formatting, linting, type checking, and all tests pass - [x] T012 Run quickstart.md manual verification steps to validate end-to-end behavior --- ## Dependencies & Execution Order ### Phase Dependencies - **Setup (Phase 1)**: No dependencies - **Foundational (Phase 2)**: Depends on Phase 1 -- BLOCKS all user stories - **User Story 1 (Phase 3)**: Depends on Phase 2 - **User Story 2 (Phase 4)**: Depends on Phase 2 (independent of US1) - **User Story 3 (Phase 5)**: Depends on Phase 2 (independent of US1, US2) - **Polish (Phase 6)**: Depends on all user stories complete ### User Story Dependencies - **User Story 1 (P1)**: Depends on Foundational only. Core save/load wiring in the hook. - **User Story 2 (P2)**: Depends on Foundational only. Verifies the null-fallback path. - **User Story 3 (P3)**: Depends on Foundational only. Hardens validation against corrupt data. ### Parallel Opportunities - T002 and T003 are sequential (same file, T003 depends on T002's storage key constant) - T005 and T006 can run in parallel (test file vs hook file) - T008 and T009 target the same test file as T004/T005 (`encounter-storage.test.ts`); sequence them after T005 to avoid merge conflicts. T006/T007 (hook file) can still run in parallel with test tasks. - US2 and US3 can proceed in parallel after Foundational phase --- ## Implementation Strategy ### MVP First (User Story 1 Only) 1. Complete Phase 1: Setup (T001) 2. Complete Phase 2: Foundational (T002-T004) 3. Complete Phase 3: User Story 1 (T005-T007) 4. **STOP and VALIDATE**: Run `pnpm check`, manually test reload behavior 5. This alone delivers the core value of the feature ### Incremental Delivery 1. Setup + Foundational -> Storage adapter ready 2. Add User Story 1 -> Reload persistence works (MVP!) 3. Add User Story 2 -> First-time experience confirmed 4. Add User Story 3 -> Corrupt data resilience hardened 5. Polish -> Final validation --- ## Notes - All new code is in the adapter layer (`apps/web/`); domain and application packages are unchanged - Tests use a localStorage mock (Vitest's jsdom environment or manual mock) - The `nextId` counter derivation (T006) is critical to avoid ID collisions after reload - Commit after each phase or logical group of tasks