Files
initiative/specs/007-json-import-export/research.md
Lukas fba83bebd6 Add JSON import/export for full encounter state
Export and import encounter, undo/redo history, and player characters
as a downloadable .json file. Export/import actions are in the action
bar overflow menu. Import validates using existing rehydration functions
and shows a confirmation dialog when replacing a non-empty encounter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-27 14:28:39 +01:00

5.1 KiB

Research: JSON Import/Export

Feature: 007-json-import-export Date: 2026-03-27

Decision 1: Export Bundle Contents

Decision: Export includes encounter, undo/redo stacks, and player characters. Excludes bestiary cache, theme, and rules edition.

Rationale: The spec explicitly includes undo/redo history and player characters. Theme and rules edition are user preferences that should not transfer between DMs. Bestiary cache is large and can be rebuilt from sources.

Alternatives considered:

  • Include theme/edition settings — rejected because these are personal preferences, not encounter data.
  • Exclude undo/redo — rejected because the spec explicitly requires it and it enables full session restore.
  • Include bestiary cache — rejected because it's large, device-specific, and reconstructable from source URLs.

Decision 2: Import Strategy — Full Replacement vs Merge

Decision: Full state replacement. Import replaces encounter, undo/redo, and player characters entirely.

Rationale: The spec states "Import replaces all existing state." Merging would require conflict resolution (duplicate IDs, name collisions) which adds significant complexity for unclear benefit.

Alternatives considered:

  • Merge player characters — rejected because ID conflicts between different sessions would be complex to resolve and the spec doesn't call for it.
  • Selective import (pick which parts to load) — rejected as out of MVP scope.

Decision 3: Validation Approach

Decision: Reuse existing rehydrateEncounter() and player character validation from the persistence layer. These functions already handle all field validation, type checking, and graceful degradation for invalid fields.

Rationale: The spec explicitly states "validated using the same rules as localStorage loading." The existing rehydrateEncounter() function already validates every combatant field, filters invalid conditions, clamps HP values, and rejects structurally malformed data. Reusing it ensures consistency and avoids duplication.

Alternatives considered:

  • Write separate import validation — rejected because it would duplicate existing validation logic and risk divergence.
  • Stricter validation (reject files with any invalid field) — rejected because the existing approach gracefully degrades (strips invalid optional fields) which is more user-friendly.

Decision 4: File Download Mechanism

Decision: Use URL.createObjectURL() with an anchor element's download attribute for triggering the file download.

Rationale: This is the standard browser-native approach that works across all modern browsers without popup blockers interfering. No server-side component needed.

Alternatives considered:

  • window.open() with data URI — rejected because popup blockers can interfere.
  • FileSaver.js library — rejected because the native approach is sufficient and avoids an additional dependency.

Decision 5: File Upload Mechanism

Decision: Use an <input type="file" accept=".json"> element, consistent with the existing pattern in source-fetch-prompt.tsx which uses file.text() + JSON.parse().

Rationale: The codebase already has this pattern for bestiary source uploads. Reusing the same approach keeps the UX consistent.

Decision 6: UI Placement

Decision: Place export and import actions in the action bar's overflow menu, alongside existing items like "Players", "Manage Sources", and "Settings".

Rationale: The overflow menu already groups secondary actions. Import/export are infrequent operations that don't need primary button placement. The action bar's buildOverflowItems() function makes this straightforward to add.

Alternatives considered:

  • Settings modal — rejected because import/export are actions, not settings.
  • Dedicated toolbar buttons — rejected because import/export are infrequent and would clutter the primary UI.

Decision 7: Export File Naming

Decision: Use a filename pattern like initiative-export-YYYY-MM-DD.json with the current date.

Rationale: The date provides context for when the export was created. Including "initiative" in the name makes the file's purpose clear when browsing a downloads folder.

Decision 8: State Restoration After Import

Decision: Import must update both React state and localStorage in one operation. The encounter hook's setEncounter() triggers a useEffect that auto-saves to localStorage, and setUndoRedoState() similarly auto-saves. For player characters, the same auto-save pattern applies.

Rationale: Following the existing state flow ensures consistency. Setting React state triggers the existing persistence effects, so no manual localStorage writes are needed for the import path.

Decision 9: Export Format Versioning

Decision: Include a version field in the export format (e.g., 1) but do not implement migration logic in MVP.

Rationale: The spec's assumptions state "Future format versioning is not included in MVP baseline." Including the version field costs nothing and enables future migration logic without breaking existing exports.