Build MVP: campaigns, characters, combat, dice, compendium

- Rules abstraction (5e + pf2e) behind one interface, fully tested
- Pure combat engine: turn-order safe across add/remove/reorder/init-change
- Dexie storage with real cascade deletes + Zod validation on write
- Seedable dice engine with notation parser
- Lazy SRD compendium (code-split), bestiary -> combat
- Strict TS, 54 unit tests, Playwright e2e smoke (all green)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-08 00:09:42 +02:00
parent fe84dc365d
commit 1a9e5e2c18
93 changed files with 412052 additions and 9 deletions
+264
View File
@@ -0,0 +1,264 @@
# TTRPG Manager — Comprehensive Bug Hunt Report
**Date**: 2026-06-03
**Methodology**: 24 parallel AI agents searched 100+ source files across 20+ bug categories. Wave 1: 16 agents. Wave 2: 8 agents (addressing Oracle-identified gaps).
**Baseline**: 197 existing tests all pass (math engine only; 0 store/adapter/UI tests).
---
## 🔴 CRITICAL (19 bugs — data loss, crashes, security incidents)
### Infrastructure & Config
| ID | Finding | File |
|----|---------|------|
| C1 | **`tsconfig.app.json` missing `"strict": true`** — no strictNullChecks, noImplicitAny | `tsconfig.app.json` |
| C2 | **No React ErrorBoundary** — any uncaught render error white-screens entire app in React 19 | `App.tsx` |
| C3 | **CSP set to `null`** — all Content Security Policy protections disabled | `tauri.conf.json:23` |
### Tauri Integration
| ID | Finding | File |
|----|---------|------|
| C4 | **`capabilities.json` is `{}`** — no Tauri v2 permissions | `src-tauri/gen/schemas/capabilities.json` |
| C5 | **`import_campaign`/`export_campaign` are no-op stubs** | `commands.rs:1-9` |
| C6 | **Zero Tauri API usage in frontend** — no `@tauri-apps` imports | All `src/` |
| C7 | **SQLite configured but never queried** — no SQL adapter implemented | `tauri.conf.json:27-31`, `storage/instance.ts:8` |
### Storage & Data Integrity
| ID | Finding | File |
|----|---------|------|
| C8 | **Transaction completion not checked**`promisify` resolves on `request.onsuccess` BEFORE transaction commits. All 30+ writes can silently lose data. | `indexeddb.ts:62-67` |
| C9 | **Campaign deletion does NOT cascade** — 9 child entity types orphaned forever | `campaignStore.ts:80-86` |
| C10 | **Handout data split across localStorage AND IndexedDB** — export/import silently loses all handouts | `handoutStore.ts` / `types.ts:142-150` |
| C11 | **Combat encounter IDs inside `combatantsJson` not remapped on import** | `indexeddb.ts:628-629` |
| C12 | **`updateSettings` reads stale data across separate transactions** | `indexeddb.ts:518-525` |
| C13 | **No data migration strategy**`DB_VERSION = 1` hardcoded, `onupgradeneeded` only creates new stores | `indexeddb.ts:30, 651-723` |
### Security
| ID | Finding | File |
|----|---------|------|
| C14 | **No HTML sanitization library**`contentMd` stores raw HTML (not Markdown). DOMPurify missing. | All note components |
### State Management
| ID | Finding | File |
|----|---------|------|
| C15 | **Direct state mutation in handoutStore** — localStorage split-brain with IndexedDB; dual `Handout` type definitions | `handoutStore.ts:99-105` |
### Combat System
| ID | Finding | File |
|----|---------|------|
| C16 | **`removeCombatant` corrupts turn order** — doesn't decrement index when removing combatant before current turn | `combatStore.ts:165-175` |
| C17 | **`addCombatant`/`updateCombatant`/`reorderCombatants` don't adjust `currentTurnIndex`** | `combatStore.ts:143-150`, `152-163`, `222-229` |
| C18 | **`nextTurn()` calls async `nextRound()` without `await`** — race condition | `combatStore.ts:191-193` |
### Race Conditions (Wave 2)
| ID | Finding | File |
|----|---------|------|
| C19 | **Read-modify-write without locking in all stores** — two rapid mutations can overwrite each other (e.g., double `addCombatant`) | All store files |
---
## 🟠 HIGH (16 bugs — functional breakage under realistic conditions)
### Tauri & Storage
- **H1**: `init()` race — second caller returns promise resolving before `ensureSettings()` seeds defaults. `indexeddb.ts:97-104`
- **H2**: No `onblocked` handler on DB open — hangs indefinitely if another tab has connection. `indexeddb.ts:648-734`
- **H3**: DB connection not reset after unexpected close — `onclose` sets `db=null` but `initPromise` stays stale. `indexeddb.ts:725-730`
- **H4**: `handleClearAll` deletes campaigns but not related entities. `SettingsPage.tsx:148-156`
- **H5**: Full table scan on dice roll history — `getAll()` loads every roll. `indexeddb.ts:415-423`
- **H6**: Full table scan on note search — Fuse.js index rebuilt every keystroke. `indexeddb.ts:369-380`
### Math & Game Mechanics
- **H7**: Heavy armor incorrectly applies negative DEX modifier to AC — `Math.min(-1, 0) = -1`. `five-e/combat.ts:137-138`
- **H8**: `rollD20` / `recoveryCheck` use `Math.random()` despite seedable RNG available. `five-e/rolling.ts:35-36`
- **H9**: Recovery check returns `'success'` not `'criticalSuccess'` for nat 20. `pf2e/dying.ts:44-46`
### Combat System
- **H10**: No tie-breaking in initiative sort — unstable ordering. `combatStore.ts:130`
- **H11**: Reaction resets at end of turn instead of start — double-dipping on reactions. `CombatScreen.tsx:67-80`
- **H12**: Short Rest fully heals instead of requiring Hit Dice rolls. `CombatDetail.tsx:153-154`
### Character System
- **H13**: `ProficiencyRank` type conflict — numeric vs string across two modules. `shared.ts:20` vs `types.ts:12-18`
- **H14**: PF2e HP damage bypasses temporary HP entirely. `CharacterSheetPF2e.tsx:60-62`
### Data Integrity
- **H15**: NaN/Infinity accepted in ALL combat number inputs (7+ locations). `HPManager.tsx:83`, etc.
- **H16**: SettingsPage "Export All" produces non-importable format. `SettingsPage.tsx:129-134`
---
## 🟡 MEDIUM (16 bugs — degraded behavior, fragile states)
### Combat
- **M1**: Dead/unconscious combatants remain in initiative — no skip mechanism. `combatStore.ts:185-194`
- **M2**: HP clamped at 0 — no instant death / massive damage tracking. `HPManager.tsx:31-43`
- **M3**: Shield Block allowed when shield lowered/broken (PF2e rules). `ShieldManager.tsx:151`
- **M4**: PF2e custom condition value only added when > 1, presets use > 0 — inconsistency. `ConditionManager.tsx:74`
### Character System
- **M5**: Character sheet defaults rendered but NEVER persisted. `CharacterSheet5e.tsx:510-531`
- **M6**: Spell slot / hit dice "used" not clamped when "total" reduced. `CharacterSheet5e.tsx:326,385`
- **M7**: No auto-save — unsaved changes silently lost on navigation. `CharacterSheet5e.tsx:534-538`
- **M8**: `importDDBCharacter` does zero actual D&D Beyond parsing. `characterStore.ts:91-109`
### Storage
- **M9**: `Number("0") || 1` coalesces zero to one in ConditionManager. `ConditionManager.tsx:36`
- **M10**: `parseHp` returns HP object with potentially zero `max` causing NaN% bar. `CharacterList.tsx:18-26`
### React Patterns
- **M11**: `useMemo` used for side-effects — should be `useEffect`. `BestiarySearch.tsx:157-159`
- **M12**: `useCallback` misused as `useMemo` in QuestDetail. `QuestDetail.tsx:219-233`
- **M13**: Stale closures from `eslint-disable react-hooks/exhaustive-deps` — 9+ instances.
### Accessibility
- **M14**: No `aria-live` regions — screen readers get no dynamic announcements.
- **M15**: No focus trapping in any modal (4 modals affected).
- **M16**: Icon-only buttons missing `aria-label` — 8 instances.
---
## 🟢 LOW (17 bugs — maintenance concerns, minor UX)
### UI/UX
- **L1**: Fixed sidebar widths not responsive. Multiple components.
- **L2**: Delete buttons invisible until parent hover — undiscoverable for keyboard users.
- **L3**: Color contrast: `text-dnd-muted` (#8a8a9a) on `bg-dnd-bg` (#1a1a2e) ~3.8:1 — fails WCAG AA.
- **L4**: Version number not semantic. `App.tsx:102`
### Edge Cases
- **L5**: No level bounds on proficiency bonus — level 0→+1, level 1000→+251.
- **L6**: Fractional CR monsters corrupted to CR 0 — `Number("1/8")` = NaN.
- **L7**: `advanceDay()` crashes on invalid date — RangeError on `toISOString()`.
### Performance
- **L8**: Zero `React.memo` usage — entire tree rerenders on any change.
- **L9**: No code splitting — 10+ features eagerly imported including MB-scale JSON.
- **L10**: Synchronous `localStorage` on every handout/calendar mutation.
### Theme System (Wave 2)
- **L11**: **Light theme is a null-op**`AppSettings.theme` accepts `'light'` but `globals.css` defines only dark colors. Toggling theme silently does nothing.
### Python Scripts (Wave 2)
- **L12**: `fetch_data.py` contains hardcoded paths (`/home/nilsb/Downloads/`) — won't work on other machines.
- **L13**: `parse_mpmb.py` regex parsing has no error recovery on malformed input.
### PWA (Wave 2)
- **L14**: Service worker registered but no update prompt — old SW persists indefinitely until manual unregister.
### Dead Code (Wave 2)
- **L15**: `@tanstack/react-query` imported but barely used — 13KB gzipped dead weight.
- **L16**: `idMap` populated but never read in `importCampaign`. `indexeddb.ts:572-577`
- **L17**: `data-tab` attributes set but never consumed. `App.tsx:122`
---
## 📊 Summary by System Area
| System Area | Crit | High | Med | Low | Total |
|-------------|------|------|-----|-----|-------|
| Storage/IndexedDB | 6 | 6 | 2 | 0 | **14** |
| Combat System | 3 | 3 | 4 | 0 | **10** |
| Tauri Integration | 4 | 0 | 0 | 0 | **4** |
| Character System | 0 | 2 | 4 | 0 | **6** |
| State Management | 1 | 0 | 0 | 0 | **1** |
| Math/Game Mechanics | 0 | 3 | 0 | 2 | **5** |
| Security | 1 | 0 | 0 | 0 | **1** |
| React/Accessibility | 0 | 0 | 3 | 3 | **6** |
| Infrastructure | 3 | 0 | 0 | 0 | **3** |
| Data Integrity | 0 | 2 | 2 | 0 | **4** |
| Performance | 0 | 0 | 0 | 3 | **3** |
| Race Conditions | 1 | 0 | 0 | 0 | **1** |
| Theme/PWA/Python | 0 | 0 | 0 | 4 | **4** |
| Dead Code | 0 | 0 | 0 | 3 | **3** |
| **TOTAL** | **19** | **16** | **15** | **15** | **65** |
---
## 🎯 Fix Priority Roadmap
### Week 1 — Critical (19 bugs)
1. Enable `"strict": true` in tsconfig
2. Add React ErrorBoundary wrapping `<main>` in App.tsx
3. Add DOMPurify sanitization for all note content rendering
4. Fix `promisify` to await `transaction.oncomplete` (or use `transaction.done`)
5. Implement cascade delete for campaigns
6. Fix combatStore turn index on remove/add/reorder
7. Add form-level input validation (NaN/Infinity guards)
### Week 2 — High (16 bugs)
8. Fix heavy armor DEX modifier bug
9. Fix PF2e temp HP bypass
10. Replace `Math.random()` with seedable RNG for game rolls
11. Unify Handout types and migrate localStorage→IndexedDB
12. Fix stale closures in detail components
13. Fix short rest to use Hit Dice
14. Add `onblocked` handler to IndexedDB open
### Week 3 — Medium (15 bugs)
15. Add dead combatant skipping in initiative
16. Fix character sheet default persistence
17. Add `aria-live` regions and focus trapping
18. Replace `useMemo` side-effects with `useEffect`
19. Fix `Number("0") || 1` to use nullish coalescing
### Backlog — Low (15 bugs)
- Performance: add React.memo, code splitting
- Accessibility: fix color contrast, responsive widths
- Theme: implement light theme or remove the setting
- Python: fix hardcoded paths
- PWA: add update prompt
- Dead code: remove unused imports and `@tanstack/react-query`
---
---
## 🔴 Wave 3 — Oracle-Identified Gap Closures
### CombatMeta Data Loss (HIGH)
- **W3-1**: `combatMeta` stored in React `useState` — lost on tab switch. Temp HP, shield HP, used actions, MAP counts all reset. `CombatScreen.tsx:34`
### MapCanvas Touch Support (HIGH)
- **W3-2**: MapCanvas uses mouse events only (`onMouseDown/Up/Move/Wheel`) — no touch equivalents. Fog of war revelation impossible on tablets. `MapCanvas.tsx:430-434`
### Handout File Size Validation (MEDIUM)
- **W3-3**: Handout upload has no file size validation. Large files silently fail on localStorage quota. `handoutStore.ts:87`, `HandoutSection.tsx:36-42`. Compare with MapUpload.tsx which HAS 20MB validation.
### Memory/Resource Leaks (MEDIUM)
- **W3-4**: MapCanvas `requestAnimationFrame` not cancelled on unmount. `MapCanvas.tsx:200`
- **W3-5**: Unbounded async in useEffect without abort — 3 components. `CampaignDashboard.tsx:139-165`, `CampaignOrganizer.tsx:100-127`, `PlayerViewPage.tsx:175-190`
- **W3-6**: `setTimeout` without cleanup — 2 components. `LootGenerator.tsx:212-215`, `DiceRoller.tsx:53-55`
- **W3-7**: Handout data URLs in localStorage — quota exhaustion risk. `handoutStore.ts:44-57,82`
- **W3-8**: SRD reference data permanently resident in memory (~500KB+). `SRDViewer.tsx:3-8,419`
- **W3-9**: IndexedDB exportCampaign opens N+1 separate transactions. `indexeddb.ts:532-561`
### Test Coverage Gaps (MEDIUM)
- **W3-10**: 15 test files cover only math engine + 1 UI component. 0 tests for 9 stores, 747-line storage adapter, 30+ UI components. ~10% file coverage.
- **W3-11**: No integration tests — stores never tested with adapter. Zero data-flow verification.
- **W3-12**: E2E tests are smoke-only — verify page loads, no CRUD workflow testing.
- **W3-13**: No vitest coverage thresholds configured. No CI enforcement.
---
## Verification Notes
- **Oracle-reviewed**: Two false positives corrected (HandoutStore Zustang detection, Stored XSS via innerHTML)
- **Wave 2 additions**: Race conditions, input validation, Python scripts, PWA, theme system, dead code, memory leaks, schema migration, test coverage
- **Wave 3 additions**: CombatMeta data loss, MapCanvas touch support, handout size validation, 6 memory/resource leaks, 4 test coverage gaps
- **197 existing tests**: All pass, but cover only the math engine (0 store/adapter/UI tests)
- **Total agents deployed**: 30 across 4 waves
- **Total unique bugs**: 65 (Wave 1-2) + 13 (Wave 3) + 57 (Wave 4) ≈ **135 bugs**
### Wave 4 Additions (Oracle-identified coverage gaps)
- **Bundle bloat**: 15MB monolithic build, 12MB monsters-full.json imported 3× eagerly, zero code splitting
- **Calendar store**: `advanceDay` timezone/DST fragility (MEDIUM-HIGH)
- **Dice store**: `addRoll` can't associate with campaigns; `clearHistory` partial-failure inconsistency
- **Settings store**: `initialize` doesn't merge defaults; `resetSettings` skips init guard
- **UUID inconsistency**: 9 files use `uuidv4()`, 1 file uses `crypto.randomUUID()` — competing strategies
- **Tauri capabilities**: CRITICAL — no `capabilities/default.json`, all plugins dead at runtime
- **PWA triple-manifest**: static manifest, plugin manifest, HTML link — browser confusion
- **5e heavy armor DEX bug**: `Math.min(-1, 0) = -1` should be 0 (rules violation)
- **Encounter builder**: XP thresholds hardcoded for level 1; PF2e budget per-player vs party; PF2e uses 5e HP heuristics
- **Loot engine**: Individual treasure uses multiply instead of sum (wrong distribution); CR 0-4 art objects completely wrong; duplicate art objects CR 11-16 vs 17+
- **ActionTracker**: PF2e action diamond click logic inverted (used actions call onUseAction instead of onUndoAction)
- **BestiaryList**: monster name as React key (non-unique)
- **20+ component-level bugs** across InitiativeTracker, CombatantCard, CampaignForm, RestControls, EncounterBuilder