Implement the 008-persist-encounter feature that saves encounter state to localStorage so it survives page reloads
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
34
specs/008-persist-encounter/checklists/requirements.md
Normal file
34
specs/008-persist-encounter/checklists/requirements.md
Normal file
@@ -0,0 +1,34 @@
|
||||
# Specification Quality Checklist: Persist Encounter
|
||||
|
||||
**Purpose**: Validate specification completeness and quality before proceeding to planning
|
||||
**Created**: 2026-03-05
|
||||
**Feature**: [spec.md](../spec.md)
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details (languages, frameworks, APIs)
|
||||
- [x] Focused on user value and business needs
|
||||
- [x] Written for non-technical stakeholders
|
||||
- [x] All mandatory sections completed
|
||||
|
||||
## Requirement Completeness
|
||||
|
||||
- [x] No [NEEDS CLARIFICATION] markers remain
|
||||
- [x] Requirements are testable and unambiguous
|
||||
- [x] Success criteria are measurable
|
||||
- [x] Success criteria are technology-agnostic (no implementation details)
|
||||
- [x] All acceptance scenarios are defined
|
||||
- [x] Edge cases are identified
|
||||
- [x] Scope is clearly bounded
|
||||
- [x] Dependencies and assumptions identified
|
||||
|
||||
## Feature Readiness
|
||||
|
||||
- [x] All functional requirements have clear acceptance criteria
|
||||
- [x] User scenarios cover primary flows
|
||||
- [x] Feature meets measurable outcomes defined in Success Criteria
|
||||
- [x] No implementation details leak into specification
|
||||
|
||||
## Notes
|
||||
|
||||
- All items pass validation. Spec is ready for `/speckit.clarify` or `/speckit.plan`.
|
||||
50
specs/008-persist-encounter/data-model.md
Normal file
50
specs/008-persist-encounter/data-model.md
Normal file
@@ -0,0 +1,50 @@
|
||||
# Data Model: Persist Encounter
|
||||
|
||||
## Existing Entities (unchanged)
|
||||
|
||||
### Combatant
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| id | CombatantId (branded string) | Unique identifier |
|
||||
| name | string | Display name |
|
||||
| initiative | number or undefined | Initiative value, optional |
|
||||
|
||||
#### ID Format Convention (existing code)
|
||||
- Demo combatants use plain numeric IDs: `"1"`, `"2"`, `"3"`
|
||||
- User-added combatants use the pattern `c-{N}` (e.g., `"c-1"`, `"c-2"`)
|
||||
- On reload, `nextId` counter is derived by scanning existing IDs matching `c-{N}` and starting from `max(N) + 1`
|
||||
- IDs not matching `c-{N}` (e.g., demo IDs) are ignored during counter derivation
|
||||
|
||||
### Encounter
|
||||
| Field | Type | Notes |
|
||||
|-------|------|-------|
|
||||
| combatants | readonly Combatant[] | Ordered list of combatants |
|
||||
| activeIndex | number | Index of the combatant whose turn it is |
|
||||
| roundNumber | number | Current round (positive integer) |
|
||||
|
||||
## Persisted State
|
||||
|
||||
### Storage Key
|
||||
`"initiative:encounter"`
|
||||
|
||||
### Serialized Format
|
||||
The `Encounter` object is serialized as-is via `JSON.stringify`. The branded `CombatantId` serializes as a plain string. On deserialization, IDs are rehydrated with `combatantId()`.
|
||||
|
||||
### Validation on Load
|
||||
1. Parse JSON string into unknown value
|
||||
2. Structural check: verify it is an object with `combatants` (array), `activeIndex` (number), `roundNumber` (number)
|
||||
3. Verify each combatant has `id` (string) and `name` (string)
|
||||
4. Pass through `createEncounter` to enforce domain invariants
|
||||
5. On any failure: discard and return `null` (caller falls back to demo encounter)
|
||||
|
||||
### State Transitions
|
||||
|
||||
```
|
||||
App Load
|
||||
├─ localStorage has valid data → restore Encounter
|
||||
└─ localStorage empty/invalid → create demo Encounter
|
||||
|
||||
State Change (any use case)
|
||||
└─ new Encounter saved to React state
|
||||
└─ useEffect triggers → serialize to localStorage
|
||||
```
|
||||
74
specs/008-persist-encounter/plan.md
Normal file
74
specs/008-persist-encounter/plan.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Implementation Plan: Persist Encounter
|
||||
|
||||
**Branch**: `008-persist-encounter` | **Date**: 2026-03-05 | **Spec**: [spec.md](./spec.md)
|
||||
**Input**: Feature specification from `/specs/008-persist-encounter/spec.md`
|
||||
|
||||
## Summary
|
||||
|
||||
Persist the current encounter state to browser localStorage so it survives page reloads. The web adapter layer will serialize encounter state on every change and deserialize on load, falling back to the demo encounter when no valid data exists. The domain and application layers remain unchanged -- persistence is purely an adapter concern.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: TypeScript 5.x (strict mode, verbatimModuleSyntax)
|
||||
**Primary Dependencies**: React 19, Vite 6, Biome 2.0, existing domain/application packages
|
||||
**Storage**: Browser localStorage (adapter layer only)
|
||||
**Testing**: Vitest (unit tests for serialization/deserialization logic)
|
||||
**Target Platform**: Modern browsers (Chrome, Firefox, Safari, Edge)
|
||||
**Project Type**: Web application (monorepo with domain/application/web layers)
|
||||
**Performance Goals**: Encounter restore under 1 second (negligible for small JSON payloads)
|
||||
**Constraints**: No domain or application layer changes; persistence is adapter-only
|
||||
**Scale/Scope**: Single encounter, single user, single tab (MVP baseline)
|
||||
|
||||
## Constitution Check
|
||||
|
||||
*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*
|
||||
|
||||
| Principle | Status | Notes |
|
||||
|-----------|--------|-------|
|
||||
| I. Deterministic Domain Core | PASS | No domain changes. localStorage access is confined to the adapter layer. |
|
||||
| II. Layered Architecture | PASS | Persistence logic lives in the web adapter (apps/web). Domain and application layers are untouched. The existing `EncounterStore` port is already implemented by the `useEncounter` hook; persistence wraps around this. |
|
||||
| III. Agent Boundary | N/A | No agent features involved. |
|
||||
| IV. Clarification-First | PASS | Feature is well-scoped; no non-trivial assumptions. Storage key name and serialization format are implementation details within adapter scope. |
|
||||
| V. Escalation Gates | PASS | Implementation stays within spec scope. |
|
||||
| VI. MVP Baseline Language | PASS | Cross-tab sync and multi-encounter support noted as "MVP baseline does not include." |
|
||||
| VII. No Gameplay Rules | PASS | No gameplay mechanics involved. |
|
||||
|
||||
**Gate result**: PASS -- no violations.
|
||||
|
||||
## Project Structure
|
||||
|
||||
### Documentation (this feature)
|
||||
|
||||
```text
|
||||
specs/008-persist-encounter/
|
||||
├── plan.md # This file
|
||||
├── research.md # Phase 0 output
|
||||
├── data-model.md # Phase 1 output
|
||||
├── quickstart.md # Phase 1 output
|
||||
└── tasks.md # Phase 2 output (via /speckit.tasks)
|
||||
```
|
||||
|
||||
### Source Code (repository root)
|
||||
|
||||
```text
|
||||
packages/domain/src/
|
||||
├── types.ts # Encounter, Combatant, CombatantId (unchanged)
|
||||
└── index.ts # Exports (unchanged)
|
||||
|
||||
packages/application/src/
|
||||
├── ports.ts # EncounterStore interface (unchanged)
|
||||
└── index.ts # Exports (unchanged)
|
||||
|
||||
apps/web/src/
|
||||
├── hooks/
|
||||
│ └── use-encounter.ts # Modified: initialize from localStorage, persist on change
|
||||
├── persistence/
|
||||
│ └── encounter-storage.ts # New: localStorage read/write/validate logic
|
||||
└── main.tsx # Unchanged
|
||||
```
|
||||
|
||||
**Structure Decision**: All new code goes into `apps/web/src/persistence/` as a new adapter module. The `useEncounter` hook is modified to use this module for initialization and persistence. No new packages or layers are introduced.
|
||||
|
||||
## Complexity Tracking
|
||||
|
||||
No constitution violations to justify.
|
||||
39
specs/008-persist-encounter/quickstart.md
Normal file
39
specs/008-persist-encounter/quickstart.md
Normal file
@@ -0,0 +1,39 @@
|
||||
# Quickstart: Persist Encounter
|
||||
|
||||
## What This Feature Does
|
||||
|
||||
Saves the current encounter to browser localStorage so it survives page reloads. No user action required -- saving and restoring happens automatically.
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `apps/web/src/persistence/encounter-storage.ts` | New: read/write/validate encounter in localStorage |
|
||||
| `apps/web/src/hooks/use-encounter.ts` | Modified: load from storage on init, persist on change |
|
||||
|
||||
## How It Works
|
||||
|
||||
1. **On app load**: The `useEncounter` hook calls `loadEncounter()` from the storage module. If valid data exists, it initializes state from it. Otherwise, the demo encounter is used.
|
||||
2. **On every state change**: A `useEffect` watches the encounter state and calls `saveEncounter()` to write it to localStorage.
|
||||
3. **On error**: All storage operations are wrapped in try/catch. Failures are silent -- the app continues working in memory-only mode.
|
||||
|
||||
## Testing
|
||||
|
||||
```bash
|
||||
pnpm vitest run apps/web/src/persistence/__tests__/encounter-storage.test.ts
|
||||
```
|
||||
|
||||
Tests cover:
|
||||
- Round-trip serialization (save then load returns same encounter)
|
||||
- Invalid/corrupt data returns null
|
||||
- Missing fields return null
|
||||
- Non-JSON data returns null
|
||||
|
||||
## Manual Verification
|
||||
|
||||
1. Run `pnpm --filter web dev`
|
||||
2. Add/remove combatants, set initiative, advance turns
|
||||
3. Refresh the page -- encounter state should be preserved
|
||||
4. Open DevTools > Application > localStorage to inspect the stored data
|
||||
5. Delete the storage key and refresh -- demo encounter should appear
|
||||
6. Set the storage value to `"garbage"` and refresh -- demo encounter should appear
|
||||
62
specs/008-persist-encounter/research.md
Normal file
62
specs/008-persist-encounter/research.md
Normal file
@@ -0,0 +1,62 @@
|
||||
# Research: Persist Encounter
|
||||
|
||||
## R1: Serialization Format for Encounter State
|
||||
|
||||
**Decision**: JSON via `JSON.stringify` / `JSON.parse`
|
||||
|
||||
**Rationale**: The `Encounter` type is a plain data structure (no classes, no functions, no circular references). JSON serialization is native to browsers, zero-dependency, and produces human-readable output for debugging. The branded `CombatantId` type serializes as a plain string and can be rehydrated with the `combatantId()` constructor.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Structured clone / IndexedDB: Overkill for a single small object. Adds async complexity for no benefit.
|
||||
- Custom binary format: Unnecessary complexity for small payloads.
|
||||
|
||||
## R2: Validation Strategy on Load
|
||||
|
||||
**Decision**: Validate deserialized data through the existing `createEncounter` domain function before accepting it.
|
||||
|
||||
**Rationale**: `createEncounter` already enforces all encounter invariants (at least one combatant, valid activeIndex, valid roundNumber). Passing deserialized data through it ensures that corrupt or tampered data is rejected by the same rules that govern encounter creation. Additional structural checks (is it an object? does it have the right shape?) are needed before calling `createEncounter` since `JSON.parse` can return any type.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Schema validation library (Zod, AJV): Adds a dependency for a single validation point. The domain function plus a lightweight shape check is sufficient.
|
||||
- No validation (trust localStorage): Fragile; any manual edit or version mismatch would crash the app.
|
||||
|
||||
## R3: Persistence Trigger
|
||||
|
||||
**Decision**: Persist encounter state on every state change via a `useEffect` that watches the encounter value.
|
||||
|
||||
**Rationale**: The encounter state changes infrequently (user actions only), so writing on every change has negligible performance impact. This is simpler than debouncing or batching, and guarantees the latest state is always saved.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Debounced writes: Unnecessary complexity; encounter changes are user-driven and infrequent.
|
||||
- Manual save button: Poor UX; users expect auto-save in modern apps.
|
||||
- `beforeunload` event only: Unreliable; may not fire on mobile or crash scenarios.
|
||||
|
||||
## R4: localStorage Key and Namespace
|
||||
|
||||
**Decision**: Use a single key `"initiative:encounter"` with a namespaced prefix.
|
||||
|
||||
**Rationale**: Namespacing avoids collisions with other apps on the same origin. A single key is sufficient for the MVP (one encounter at a time).
|
||||
|
||||
**Alternatives considered**:
|
||||
- Multiple keys (one per field): Unnecessarily complex; atomic read/write of the full encounter is simpler and avoids partial state issues.
|
||||
- Versioned key (e.g., `initiative:encounter:v1`): The validation layer already handles schema mismatches by falling back to the demo encounter. An explicit version field inside the stored data could be added later if needed but is unnecessary for MVP.
|
||||
|
||||
## R5: ID Counter Persistence
|
||||
|
||||
**Decision**: Derive the next ID counter from existing combatant IDs on load rather than persisting it separately.
|
||||
|
||||
**Rationale**: The `useEncounter` hook currently uses a `useRef(0)` counter with `c-{N}` format IDs. After a reload, we can scan existing combatant IDs, extract the highest numeric suffix, and start the counter from there. This avoids persisting a separate counter value and keeps the storage format simple.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Persist counter as a separate field: Works but adds coupling between the storage format and the hook's internal ID generation strategy.
|
||||
- Use UUID for new IDs: Would work but changes the ID format; unnecessary for MVP single-user scope.
|
||||
|
||||
## R6: Error Handling for Storage Operations
|
||||
|
||||
**Decision**: Wrap all localStorage operations in try/catch. On write failure (quota exceeded, storage unavailable), silently continue. On read failure, fall back to demo encounter.
|
||||
|
||||
**Rationale**: The app must never crash due to storage issues (FR-004). Silent failure on write means the user's current session is unaffected. Fallback on read means the app always starts in a usable state.
|
||||
|
||||
**Alternatives considered**:
|
||||
- Show a warning toast on write failure: Could be added later but is not in the spec scope. Silent failure is the simplest correct behavior for MVP.
|
||||
- Retry logic: Unnecessary; if localStorage is unavailable, retrying won't help.
|
||||
90
specs/008-persist-encounter/spec.md
Normal file
90
specs/008-persist-encounter/spec.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Feature Specification: Persist Encounter
|
||||
|
||||
**Feature Branch**: `008-persist-encounter`
|
||||
**Created**: 2026-03-05
|
||||
**Status**: Draft
|
||||
**Input**: User description: "Persist the encounter in browser localStorage so the current encounter survives page reloads."
|
||||
|
||||
## User Scenarios & Testing *(mandatory)*
|
||||
|
||||
### User Story 1 - Encounter Survives Page Reload (Priority: P1)
|
||||
|
||||
A user is managing a combat encounter (combatants added, initiative set, turns advanced). They accidentally refresh the page or their browser restarts. When the page reloads, the encounter is restored exactly as it was -- same combatants, same initiative values, same active turn, same round number.
|
||||
|
||||
**Why this priority**: This is the core value of the feature. Without persistence, all encounter progress is lost on any page navigation or reload, which is the primary pain point.
|
||||
|
||||
**Independent Test**: Can be fully tested by setting up an encounter, refreshing the page, and verifying all encounter state is preserved.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** an encounter with combatants, initiative values, active turn, and round number, **When** the user reloads the page, **Then** the encounter is restored with all state intact (combatants, initiative, active index, round number).
|
||||
2. **Given** an encounter that has been modified (combatant added, removed, renamed, or initiative changed), **When** the user reloads the page, **Then** the latest state is reflected.
|
||||
3. **Given** the user advances the turn multiple times, **When** the user reloads the page, **Then** the active turn and round number are preserved.
|
||||
|
||||
---
|
||||
|
||||
### User Story 2 - Fresh Start with No Saved Data (Priority: P2)
|
||||
|
||||
A first-time user opens the application with no previously saved encounter. The application shows the default demo encounter so the user can immediately start exploring.
|
||||
|
||||
**Why this priority**: Ensures backward compatibility and a smooth first-use experience.
|
||||
|
||||
**Independent Test**: Can be tested by clearing browser storage and loading the application, verifying the demo encounter appears.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** no saved encounter exists in the browser, **When** the user opens the application, **Then** the default demo encounter is displayed.
|
||||
2. **Given** saved encounter data has been manually cleared from the browser, **When** the user opens the application, **Then** the default demo encounter is displayed.
|
||||
|
||||
---
|
||||
|
||||
### User Story 3 - Graceful Handling of Corrupt Data (Priority: P3)
|
||||
|
||||
Saved data may become invalid (e.g., manually edited in dev tools, schema changes between versions). The application handles this gracefully rather than crashing.
|
||||
|
||||
**Why this priority**: Protects the user experience from edge cases that would otherwise render the app unusable without manual intervention.
|
||||
|
||||
**Independent Test**: Can be tested by writing malformed data to the storage key and loading the application.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. **Given** the saved encounter data is malformed or unparseable, **When** the user opens the application, **Then** the default demo encounter is displayed and the corrupt data is discarded.
|
||||
2. **Given** the saved data is missing required fields, **When** the user opens the application, **Then** the default demo encounter is displayed.
|
||||
|
||||
---
|
||||
|
||||
### Edge Cases
|
||||
|
||||
- What happens when browser storage quota is exceeded? The application continues to function normally; persistence silently fails without disrupting the user's current session.
|
||||
- What happens when browser storage is unavailable (e.g., private browsing in some browsers)? The application falls back to in-memory-only behavior, functioning identically to the current experience.
|
||||
- What happens when the user has multiple browser tabs open? MVP baseline does not include cross-tab synchronization. Each tab operates independently; the last tab to save wins.
|
||||
|
||||
## Requirements *(mandatory)*
|
||||
|
||||
### Functional Requirements
|
||||
|
||||
- **FR-001**: System MUST save the full encounter state (combatants, activeIndex, roundNumber) to browser storage after every state change.
|
||||
- **FR-002**: System MUST restore the saved encounter state when the application loads, if valid saved data exists.
|
||||
- **FR-003**: System MUST fall back to the default demo encounter when no saved data exists or saved data is invalid.
|
||||
- **FR-004**: System MUST NOT crash or show an error to the user when storage is unavailable or data is corrupt.
|
||||
- **FR-005**: System MUST preserve combatant identity (IDs, names, initiative values) across reloads.
|
||||
- **FR-006**: System MUST preserve the active turn position and round number across reloads.
|
||||
|
||||
### Key Entities
|
||||
|
||||
- **Persisted Encounter**: A serialized representation of the Encounter (combatants with IDs, names, and initiative values; activeIndex; roundNumber) stored in the browser.
|
||||
|
||||
## Success Criteria *(mandatory)*
|
||||
|
||||
### Measurable Outcomes
|
||||
|
||||
- **SC-001**: Users can reload the page and see their encounter fully restored within 1 second, with zero data loss.
|
||||
- **SC-002**: First-time users see the demo encounter immediately on first visit with no extra steps.
|
||||
- **SC-003**: 100% of corrupt or missing data scenarios result in a usable application (demo encounter displayed), never a crash or blank screen.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- The encounter state is small enough that serialization/deserialization has negligible performance impact.
|
||||
- A single browser storage key is sufficient for the MVP (one encounter at a time).
|
||||
- Cross-tab synchronization is not required for the MVP baseline.
|
||||
- The ID counter for new combatants must also be persisted or derived from existing state so that new combatants added after a reload do not collide with existing IDs.
|
||||
145
specs/008-persist-encounter/tasks.md
Normal file
145
specs/008-persist-encounter/tasks.md
Normal file
@@ -0,0 +1,145 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user