14 KiB
Tasks: On-Demand Bestiary with Pre-Indexed Search
Input: Design documents from /specs/029-on-demand-bestiary/
Prerequisites: plan.md (required), spec.md (required for user stories), research.md, data-model.md, contracts/
Tests: No test tasks included — not explicitly requested in the feature specification.
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 (Shared Infrastructure)
Purpose: Install dependencies and add domain types that all stories depend on
- T001 Install
idbdependency inapps/web/package.jsonviapnpm --filter web add idb - T002 Add
BestiaryIndexEntryandBestiaryIndexreadonly interfaces topackages/domain/src/creature-types.ts—BestiaryIndexEntryhas fields: name (string), source (string), ac (number), hp (number), dex (number), cr (string), initiativeProficiency (number), size (string), type (string).BestiaryIndexhas fields: sources (Record<string, string>), creatures (readonly BestiaryIndexEntry[]) - T003 Export
BestiaryIndexEntryandBestiaryIndextypes frompackages/domain/src/index.ts
Phase 2: Foundational (Blocking Prerequisites)
Purpose: Core adapters and port interfaces that MUST be complete before ANY user story can be implemented
CRITICAL: No user story work can begin until this phase is complete
- T004 [P] Create
apps/web/src/adapters/bestiary-index-adapter.ts— importdata/bestiary/index.jsonas a Vite static JSON import; export aloadBestiaryIndex()function that maps compact index fields (n→name, s→source, ac→ac, hp→hp, dx→dex, cr→cr, ip→initiativeProficiency, sz→size, tp→type) toBestiaryIndexdomain type; exportgetDefaultFetchUrl(sourceCode: string)returninghttps://raw.githubusercontent.com/5etools-mirror-3/5etools-src/main/data/bestiary/bestiary-{sourceCode-lowercase}.json; exportgetSourceDisplayName(sourceCode: string)resolving from the sources map - T005 [P] Create
apps/web/src/adapters/bestiary-cache.ts— implement IndexedDB cache adapter usingidblibrary; database name"initiative-bestiary", object store"sources"with keyPath"sourceCode"; implement functions:getCreature(creatureId)extracts source from ID prefix, looks up source record, finds creature by ID;isSourceCached(sourceCode)checks store;cacheSource(sourceCode, displayName, creatures)storesCachedSourceRecordwith cachedAt timestamp and creatureCount;getCachedSources()returns all records' metadata;clearSource(sourceCode)deletes one record;clearAll()clears store;loadAllCachedCreatures()returns aMap<CreatureId, Creature>from all cached sources for in-memory lookup; include in-memoryMapfallback if IndexedDB open fails (private browsing) - T006 [P] Add
BestiarySourceCacheport interface topackages/application/src/ports.ts— define interface with methods:getCreature(creatureId: CreatureId): Creature | undefined,isSourceCached(sourceCode: string): boolean; export frompackages/application/src/index.ts
Checkpoint: Foundation ready — index adapter parses shipped data, cache adapter persists fetched data, port interface defined
Phase 3: User Story 1 — Search All Creatures Instantly (Priority: P1)
Goal: Replace the single-source bundled bestiary with multi-source index search. All 3,312 creatures searchable instantly. Adding a creature populates HP/AC/initiative from the index without any network fetch.
Independent Test: Type a creature name → see results from multiple sources with display names → select one → combatant added with correct HP, AC, initiative modifier. No network requests made.
Implementation for User Story 1
- T007 [US1] Rewrite
apps/web/src/hooks/use-bestiary.ts— replace thexmm.jsondynamic import withloadBestiaryIndex()from bestiary-index-adapter; storeBestiaryIndexin state; rewritesearch(query)to filterindex.creaturesby case-insensitive substring on name (min 2 chars, max 10 results, sorted alphabetically), returning results withsourceDisplayNameresolved fromindex.sources; keepgetCreature(id)for stat block lookup (initially returns undefined for all — cache integration comes in US2); exportsearchIndexfunction andsourceDisplayNameresolver; updateBestiaryHookinterface to expose search results as index entries with source display names - T008 [US1] Update
apps/web/src/components/bestiary-search.tsx— modify search result rendering to show source display name alongside creature name (e.g., "Goblin (Monster Manual (2025))"); update the type of items fromCreatureto index-based search result type; ensureonSelectCreaturecallback receives the index entry data needed by addFromBestiary - T009 [US1] Update
addFromBestiaryinapps/web/src/hooks/use-encounter.ts— accept aBestiaryIndexEntry(or compatible object with name, hp, ac, dex, cr, initiativeProficiency, source) instead of requiring a fullCreature; derivecreatureIdfrom{source.toLowerCase()}:{slugify(name)}using existing slug logic; calladdCombatantUseCase,setHpUseCase(hp),setAcUseCase(ac); setcreatureIdon the combatant for later stat block lookup - T010 [US1] Remove
data/bestiary/xmm.jsonfrom the repository (git rm); verify no remaining imports reference it; update any test files that imported xmm.json to use test fixtures or the index instead
Checkpoint: Search works across all 102 sources. Creatures can be added from any source with correct stats. No bundled copyrighted content. Stat blocks not yet available (US2).
Phase 4: User Story 2 — View Full Stat Block via On-Demand Source Fetch (Priority: P2)
Goal: When a stat block is opened for a creature whose source is not cached, prompt the user to fetch the source data from a URL. After fetching, normalize and cache all creatures from that source in IndexedDB. Subsequent lookups for any creature from that source are instant.
Independent Test: Add a creature → open its stat block → see fetch prompt with pre-filled URL → confirm → stat block renders. Open another creature from same source → stat block renders instantly with no prompt.
Implementation for User Story 2
- T011 [P] [US2] Create
apps/web/src/components/source-fetch-prompt.tsx— dialog/card component that displays "Load [sourceDisplayName] bestiary data?" with an editable URL input pre-filled viagetDefaultFetchUrl(sourceCode)from bestiary-index-adapter; "Load" button triggers fetch; show loading spinner during fetch; on success callonSourceLoadedcallback; on error show error message with retry option and option to change URL; include error state for network failures and normalization failures - T012 [US2] Integrate cache into
apps/web/src/hooks/use-bestiary.ts— on mount, callloadAllCachedCreatures()from bestiary-cache to populate an in-memoryMap<CreatureId, Creature>; updategetCreature(id)to look up from this map; exportisSourceCached(sourceCode)delegating to bestiary-cache; exportfetchAndCacheSource(sourceCode, url)that fetches the URL, parses JSON, callsnormalizeBestiary()from bestiary-adapter, callscacheSource()from bestiary-cache, and updates the in-memory creature map; return cache loading state - T013 [US2] Update
apps/web/src/components/stat-block-panel.tsx— whengetCreature(creatureId)returnsundefinedfor a combatant that has acreatureId, extract the source code from the creatureId prefix; ifisSourceCached(source)is false, renderSourceFetchPromptinstead of the stat block; afteronSourceLoadedcallback fires, re-lookup the creature and render the stat block; if source is cached but creature not found (edge case), show appropriate message
Checkpoint: Full stat block flow works end-to-end. Source data fetched once per source, cached in IndexedDB, persists across sessions.
Phase 5: User Story 3 — Manual File Upload as Fetch Alternative (Priority: P3)
Goal: Add a file upload option to the source fetch prompt so users can load bestiary data from a local JSON file when the URL is inaccessible.
Independent Test: Open source fetch prompt → click "Upload file" → select a local bestiary JSON → stat blocks become available.
Implementation for User Story 3
- T014 [US3] Add file upload to
apps/web/src/components/source-fetch-prompt.tsx— add an "Upload file" button/link below the URL fetch section; clicking opens a native file picker (<input type="file" accept=".json">); on file selection, read via FileReader, parse JSON, callnormalizeBestiary(), callcacheSource(), and invokeonSourceLoadedcallback; handle errors (invalid JSON, normalization failure) with user-friendly messages and retry option
Checkpoint: Both URL fetch and file upload paths work. Users in offline/restricted environments can still load stat blocks.
Phase 6: User Story 4 — Manage Cached Sources (Priority: P4)
Goal: Provide a UI for viewing which sources are cached and clearing individual or all cached data.
Independent Test: Cache one or more sources → open management UI → see cached sources listed → clear one → verify it requires re-fetch → clear all → verify all require re-fetch.
Implementation for User Story 4
- T015 [P] [US4] Create
apps/web/src/components/source-manager.tsx— component showing a list of cached sources viagetCachedSources()from bestiary-cache; each row shows source display name, creature count, and a "Clear" button callingclearSource(sourceCode); include a "Clear All" button callingclearAll(); show empty state when no sources are cached; after clearing, update the in-memory creature map in use-bestiary - T016 [US4] Wire source manager into the app UI — add a settings/gear icon button (Lucide
Settingsicon) in the top bar or an accessible location that opens theSourceManagercomponent in a dialog or panel; ensure it integrates with the existing layout without disrupting the encounter flow
Checkpoint: Cache management fully functional. Users can inspect and clear cached data.
Phase 7: Polish & Cross-Cutting Concerns
Purpose: Edge cases, fallbacks, and merge gate validation
- T017 Verify IndexedDB-unavailable fallback in
apps/web/src/adapters/bestiary-cache.ts— ensure that when IndexedDB open fails (e.g., private browsing), the adapter silently falls back to in-memory storage; show a non-blocking warning via console or UI toast that cached data will not persist across sessions - T018 Run
pnpm check(knip + format + lint + typecheck + test) and fix all issues — ensure no unused exports from removed xmm.json references; verify layer boundary checks pass; fix any TypeScript errors from type changes; ensure Biome formatting is correct
Dependencies & Execution Order
Phase Dependencies
- Setup (Phase 1): No dependencies — can start immediately
- Foundational (Phase 2): Depends on Phase 1 completion — BLOCKS all user stories
- US1 (Phase 3): Depends on Phase 2 — core search and add flow
- US2 (Phase 4): Depends on Phase 3 — stat block fetch needs working search/add
- US3 (Phase 5): Depends on Phase 4 — file upload extends the fetch prompt from US2
- US4 (Phase 6): Depends on US2 (Phase 4) — T015 needs the in-memory creature map from T012
- Polish (Phase 7): Depends on all user stories being complete
User Story Dependencies
- US1 (P1): Requires Foundational (Phase 2) — no other story dependencies
- US2 (P2): Requires US1 (uses rewritten use-bestiary hook and creatureId links)
- US3 (P3): Requires US2 (extends source-fetch-prompt.tsx created in US2)
- US4 (P4): Requires US2 (T012 establishes the in-memory creature map that T015 must update after clearing)
Within Each User Story
- Adapter/model tasks before hook integration tasks
- Hook integration before component tasks
- Core implementation before edge cases
Parallel Opportunities
- Phase 2: T004, T005, T006 can all run in parallel (different files, no dependencies)
- Phase 3: T007 first, then T008/T009 can parallelize (different files), T010 after all
- Phase 4: T011 can parallelize with T012 (different files), T013 after both
- Phase 6: T015 depends on T012 (US2); T016 after T015
Parallel Example: Phase 2 (Foundational)
# All three foundational tasks can run simultaneously:
Task T004: "Create bestiary-index-adapter.ts" (apps/web/src/adapters/)
Task T005: "Create bestiary-cache.ts" (apps/web/src/adapters/)
Task T006: "Add BestiarySourceCache port" (packages/application/src/ports.ts)
Parallel Example: User Story 1
# After T007 (rewrite use-bestiary.ts):
Task T008: "Update bestiary-search.tsx" (apps/web/src/components/)
Task T009: "Update addFromBestiary" (apps/web/src/hooks/use-encounter.ts)
# Then T010 after both complete
Implementation Strategy
MVP First (User Story 1 Only)
- Complete Phase 1: Setup (T001–T003)
- Complete Phase 2: Foundational (T004–T006)
- Complete Phase 3: User Story 1 (T007–T010)
- STOP and VALIDATE: Search works across 102 sources, creatures add with correct stats, no bundled copyrighted content
- Deploy/demo if ready — app is fully functional for adding creatures; stat blocks unavailable until US2
Incremental Delivery
- Setup + Foundational → Foundation ready
- US1 → Multi-source search and add (MVP!)
- US2 → On-demand stat blocks via URL fetch
- US3 → File upload alternative
- US4 → Cache management
- Polish → Edge cases and merge gate
- Each story adds value without breaking previous stories
Notes
- [P] tasks = different files, no dependencies
- [Story] label maps task to specific user story for traceability
- US4 (cache management) is independent of US2/US3 and can be built in parallel if desired
- The existing
normalizeBestiary()andstripTags()are unchanged — no tasks needed for them - The existing
stat-block.tsxrendering component is unchanged - The existing
encounter-storage.tspersistence is unchanged - Commit after each task or logical group
- Stop at any checkpoint to validate story independently