1a9e5e2c18
- 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>
14 KiB
14 KiB
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 beforeensureSettings()seeds defaults.indexeddb.ts:97-104 - H2: No
onblockedhandler on DB open — hangs indefinitely if another tab has connection.indexeddb.ts:648-734 - H3: DB connection not reset after unexpected close —
onclosesetsdb=nullbutinitPromisestays stale.indexeddb.ts:725-730 - H4:
handleClearAlldeletes 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/recoveryCheckuseMath.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:
ProficiencyRanktype conflict — numeric vs string across two modules.shared.ts:20vstypes.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:
importDDBCharacterdoes zero actual D&D Beyond parsing.characterStore.ts:91-109
Storage
- M9:
Number("0") || 1coalesces zero to one in ConditionManager.ConditionManager.tsx:36 - M10:
parseHpreturns HP object with potentially zeromaxcausing NaN% bar.CharacterList.tsx:18-26
React Patterns
- M11:
useMemoused for side-effects — should beuseEffect.BestiarySearch.tsx:157-159 - M12:
useCallbackmisused asuseMemoin QuestDetail.QuestDetail.tsx:219-233 - M13: Stale closures from
eslint-disable react-hooks/exhaustive-deps— 9+ instances.
Accessibility
- M14: No
aria-liveregions — 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) onbg-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 ontoISOString().
Performance
- L8: Zero
React.memousage — entire tree rerenders on any change. - L9: No code splitting — 10+ features eagerly imported including MB-scale JSON.
- L10: Synchronous
localStorageon every handout/calendar mutation.
Theme System (Wave 2)
- L11: Light theme is a null-op —
AppSettings.themeaccepts'light'butglobals.cssdefines only dark colors. Toggling theme silently does nothing.
Python Scripts (Wave 2)
- L12:
fetch_data.pycontains hardcoded paths (/home/nilsb/Downloads/) — won't work on other machines. - L13:
parse_mpmb.pyregex 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-queryimported but barely used — 13KB gzipped dead weight. - L16:
idMappopulated but never read inimportCampaign.indexeddb.ts:572-577 - L17:
data-tabattributes 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)
- Enable
"strict": truein tsconfig - Add React ErrorBoundary wrapping
<main>in App.tsx - Add DOMPurify sanitization for all note content rendering
- Fix
promisifyto awaittransaction.oncomplete(or usetransaction.done) - Implement cascade delete for campaigns
- Fix combatStore turn index on remove/add/reorder
- Add form-level input validation (NaN/Infinity guards)
Week 2 — High (16 bugs)
- Fix heavy armor DEX modifier bug
- Fix PF2e temp HP bypass
- Replace
Math.random()with seedable RNG for game rolls - Unify Handout types and migrate localStorage→IndexedDB
- Fix stale closures in detail components
- Fix short rest to use Hit Dice
- Add
onblockedhandler to IndexedDB open
Week 3 — Medium (15 bugs)
- Add dead combatant skipping in initiative
- Fix character sheet default persistence
- Add
aria-liveregions and focus trapping - Replace
useMemoside-effects withuseEffect - Fix
Number("0") || 1to 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:
combatMetastored in ReactuseState— 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
requestAnimationFramenot 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:
setTimeoutwithout 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:
advanceDaytimezone/DST fragility (MEDIUM-HIGH) - Dice store:
addRollcan't associate with campaigns;clearHistorypartial-failure inconsistency - Settings store:
initializedoesn't merge defaults;resetSettingsskips init guard - UUID inconsistency: 9 files use
uuidv4(), 1 file usescrypto.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) = -1should 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