From 748c7d7f851e61ec0c29a430eb0d74306ddded63 Mon Sep 17 00:00:00 2001 From: LeRatierBretonnier Date: Mon, 25 May 2026 10:32:49 +0200 Subject: [PATCH] Story 3.3 done --- .../3-3-preset-import-and-export.md | 26 +++ .../implementation-artifacts/deferred-work.md | 4 + .../review-3-3/01-blind-hunter-prompt.md | 16 ++ .../review-3-3/02-edge-case-hunter-prompt.md | 41 ++++ .../03-acceptance-auditor-prompt.md | 47 +++++ .../.decision-log.md | 155 ++++++++++++--- .../prd-video-view-manager-2026-05-19/prd.md | 181 +++++++++++++----- src/core/PresetImportExportManager.js | 9 +- src/ui/gm/PresetExportDialog.js | 11 +- src/ui/gm/PresetImportDialog.js | 59 +++--- templates/preset-export.hbs | 2 +- .../core/PresetImportExportManager.test.js | 5 +- 12 files changed, 451 insertions(+), 105 deletions(-) create mode 100644 _bmad-output/implementation-artifacts/review-3-3/01-blind-hunter-prompt.md create mode 100644 _bmad-output/implementation-artifacts/review-3-3/02-edge-case-hunter-prompt.md create mode 100644 _bmad-output/implementation-artifacts/review-3-3/03-acceptance-auditor-prompt.md diff --git a/_bmad-output/implementation-artifacts/3-3-preset-import-and-export.md b/_bmad-output/implementation-artifacts/3-3-preset-import-and-export.md index 0886a46..48ede77 100644 --- a/_bmad-output/implementation-artifacts/3-3-preset-import-and-export.md +++ b/_bmad-output/implementation-artifacts/3-3-preset-import-and-export.md @@ -729,3 +729,29 @@ All acceptance criteria (AC-1 through AC-9) are satisfied except AC-9 (README do --- *This story file was created using the BMad Method Ultimate Context Engine. The developer now has everything needed for flawless implementation.* + +--- + +### Review Findings + +#### decision-needed + +- [x] [Review][Decision] AC-8 merge success message format differs from spec — Spec says `"Imported N presets (M new, K replaced)"` but implementation correctly outputs `"Imported X presets (Y new, Z skipped as duplicates)"`. **Resolution**: Update spec AC-8 to match implementation — merge mode skips duplicates, never replaces. + +#### patch + +- [x] [Review][Patch] PresetExport filename mismatch preview vs actual download [`src/ui/gm/PresetExportDialog.js`] +- [x] [Review][Patch] World name fallback inconsistency between `exportAllPresets` and `generateExportFilename` [`src/core/PresetImportExportManager.js:84` vs `:109`] +- [x] [Review][Patch] Hardcoded notification strings bypass localization keys defined in en.json [`src/ui/gm/PresetExportDialog.js:_onExport`, `src/ui/gm/PresetImportDialog.js:_onImport`] +- [x] [Review][Patch] `_onExport` double-calls `adapter.scenes.current()` [`src/ui/gm/PresetExportDialog.js:73-74`] +- [x] [Review][Patch] `success` flag not recomputed after merging extraction errors into result [`src/core/PresetImportExportManager.js:341-347`] +- [x] [Review][Patch] Import preview marks merge duplicates as invalid (red X) instead of skipped (yellow) [`src/ui/gm/PresetImportDialog.js:_parseAndPreviewFile`] +- [x] [Review][Patch] Regex `\s` in preset name character validation allows newlines/tabs [`src/core/PresetImportExportManager.js:238`] +- [x] [Review][Patch] No concurrency guard on async `_parseAndPreviewFile` — rapid file selection races [`src/ui/gm/PresetImportDialog.js:_parseAndPreviewFile`] +- [x] [Review][Patch] Import preview doesn't validate each preset structure with `isValidScenePreset()` [`src/ui/gm/PresetImportDialog.js:_parseAndPreviewFile`] +- [x] [Review][Patch] Export dialog renders when no active scene (then fails on export) [`src/ui/gm/PresetExportDialog.js`] +- [x] [Review][Patch] Import mode labels hardcoded as `'Merge'`/`'Replace'` instead of localized [`src/ui/gm/PresetImportDialog.js:_prepareContext`] + +#### defer + +- [x] [Review][Defer] Replace mode rollback can fail partially leaving corrupted state [`src/core/PresetImportExportManager.js:434-448`] — deferred, browser env lacks transactions; existing error reporting is reasonable diff --git a/_bmad-output/implementation-artifacts/deferred-work.md b/_bmad-output/implementation-artifacts/deferred-work.md index 5841686..23ab6ec 100644 --- a/_bmad-output/implementation-artifacts/deferred-work.md +++ b/_bmad-output/implementation-artifacts/deferred-work.md @@ -60,3 +60,7 @@ - [ ] 5MB MAX_PORTRAIT_SIZE vs ~50KB Foundry flag limit — documented design limitation; flag limit is server-dependent and can't be changed in code - [ ] No magic-byte file content validation — spec mentions "MIME type AND file content" but only format/MIME check implemented; enhancement for future - [ ] No animated-vs-static GIF distinction — FR-26 requires static GIF only but MIME-type alone can't distinguish; requires binary GIF parsing + +## Deferred from: code review of 3-3-preset-import-and-export (2026-05-26) + +- [ ] Replace mode rollback can fail partially leaving corrupted state — Browser env lacks transaction support; existing error reporting is reasonable but state can be partially deleted/restored when both delete and rollback fail. [`src/core/PresetImportExportManager.js:434-448`] diff --git a/_bmad-output/implementation-artifacts/review-3-3/01-blind-hunter-prompt.md b/_bmad-output/implementation-artifacts/review-3-3/01-blind-hunter-prompt.md new file mode 100644 index 0000000..f1c8bee --- /dev/null +++ b/_bmad-output/implementation-artifacts/review-3-3/01-blind-hunter-prompt.md @@ -0,0 +1,16 @@ +# Blind Hunter — Code Review: Story 3.3 (Preset Import & Export) + +You are the **Blind Hunter**. You receive ONLY the diff below. No project context, no spec file, no documentation. + +Your job: Find bugs, security vulnerabilities, logic errors, code smells, and anti-patterns. Be adversarial. Assume nothing. + +## Rules +- Output findings as a Markdown list with severity labels: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` +- Each finding: one-line title + evidence from the diff +- If nothing is found, output: `No findings.` + +## Diff + +``` +[PASTE THE FULL DIFF HERE] +``` diff --git a/_bmad-output/implementation-artifacts/review-3-3/02-edge-case-hunter-prompt.md b/_bmad-output/implementation-artifacts/review-3-3/02-edge-case-hunter-prompt.md new file mode 100644 index 0000000..a36d2ec --- /dev/null +++ b/_bmad-output/implementation-artifacts/review-3-3/02-edge-case-hunter-prompt.md @@ -0,0 +1,41 @@ +# Edge Case Hunter — Code Review: Story 3.3 (Preset Import & Export) + +You are the **Edge Case Hunter**. You receive the diff AND read access to the project. + +Your job: Walk every branching path and boundary condition in the diff. Report only unhandled edge cases — conditions where the code crashes, silently fails, behaves inconsistently, or leaves state corrupted. + +## Rules +- Output findings as a Markdown list with severity labels: `CRITICAL`, `HIGH`, `MEDIUM`, `LOW` +- Each finding: one-line title + reproduction path + evidence from the diff +- Only report genuine unhandled edge cases, not theoretical impossibilities +- For non-trivial findings, suggest how to reproduce + +## Diff +Files changed for Story 3.3 (Preset Import & Export): + +``` +src/core/PresetImportExportManager.js (NEW - 463 lines) +src/ui/gm/PresetExportDialog.js (NEW - 200 lines) +src/ui/gm/PresetImportDialog.js (NEW - 436 lines) +templates/preset-export.hbs (NEW - 29 lines) +templates/preset-import.hbs (NEW - 90 lines) +styles/components/_preset-import-export.less (NEW - 403 lines) +tests/unit/core/PresetImportExportManager.test.js (NEW - 476 lines) +src/ui/gm/DirectorsBoard.js (MODIFIED - lines 7-8 imports, 92-94 refs, 435-438, 735-770, 793-808) +templates/directors-board.hbs (MODIFIED - added export/import buttons) +lang/en.json (MODIFIED - added presetExport/presetImport keys) +styles/scrying-pool.less (MODIFIED - added _preset-import-export import) +``` + +Full diff file: `/tmp/opencode/story-3.3-diff.txt` + +## Project Access +Root: `/home/morr/work/foundryvtt/video-view-manager/` + +Key reference files to read as needed: +- `src/contracts/scene-preset.js` — `isValidScenePreset()`, `MAX_PRESETS_PER_WORLD`, `SCENE_PRESET_VERSION` +- `src/core/ScenePresetManager.js` — `list()`, `save()`, `delete()`, `get()` methods +- `src/foundry/FoundryAdapter.js` — adapter surface used by the new code +- `src/utils/html.js` — `escapeHtml()` helper + +Walk every path in the diff and report any unhandled edge cases. diff --git a/_bmad-output/implementation-artifacts/review-3-3/03-acceptance-auditor-prompt.md b/_bmad-output/implementation-artifacts/review-3-3/03-acceptance-auditor-prompt.md new file mode 100644 index 0000000..85ec589 --- /dev/null +++ b/_bmad-output/implementation-artifacts/review-3-3/03-acceptance-auditor-prompt.md @@ -0,0 +1,47 @@ +# Acceptance Auditor — Code Review: Story 3.3 (Preset Import & Export) + +You are the **Acceptance Auditor**. Review this diff against the spec and context docs. + +Your job: Check for violations of acceptance criteria, deviations from spec intent, missing implementation of specified behavior, and contradictions between spec constraints and actual code. + +## Rules +- Output findings as a Markdown list +- Each finding: one-line title, which AC/constraint it violates, and evidence from the diff +- Use AC numbers (AC-1 through AC-9) when referencing acceptance criteria + +## Spec File + +Path: `_bmad-output/implementation-artifacts/3-3-preset-import-and-export.md` (full 731-line story file, also in diff) + +Key Acceptance Criteria: +- AC-1: Export All Presets as JSON — click "Export Presets" downloads JSON +- AC-2: Export File Format — JSON with `{ _version: 1, presets: { [name]: ScenePreset } }`, filename `scrying-pool-presets-[world-name]-[timestamp].json` +- AC-3: Import Dialog with Merge/Replace — file picker, merge/replace choice, preview +- AC-4: Merge Behavior — add new, keep existing, success message +- AC-5: Replace with Confirmation — warn, list removed count, confirm +- AC-6: Invalid JSON Error Handling — error notification, no changes +- AC-7: Schema Validation — error notification with field details, no changes +- AC-8: Import Confirmation Notification — "Imported N presets (M new, K replaced)" format +- AC-9: README Documentation — deferred after code review + +## Diff +Files changed for Story 3.3: + +Full diff path: `/tmp/opencode/story-3.3-diff.txt` + +New files: +- `src/core/PresetImportExportManager.js` — Core import/export logic +- `src/ui/gm/PresetExportDialog.js` — Export dialog +- `src/ui/gm/PresetImportDialog.js` — Import dialog with merge/replace +- `templates/preset-export.hbs` — Export template +- `templates/preset-import.hbs` — Import template with preview +- `styles/components/_preset-import-export.less` — Dialog styles +- `tests/unit/core/PresetImportExportManager.test.js` — 38 tests + +Modified files for 3.3: +- `src/ui/gm/DirectorsBoard.js` — Import/Export button handlers +- `templates/directors-board.hbs` — Export/Import buttons in footer +- `lang/en.json` — Localization keys +- `styles/scrying-pool.less` — Import for preset styles + +Read the source files referenced above and the spec file, then report any deviations. diff --git a/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/.decision-log.md b/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/.decision-log.md index c4087fe..268cf30 100644 --- a/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/.decision-log.md +++ b/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/.decision-log.md @@ -3,6 +3,7 @@ **Workspace:** `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/` **Created:** 2026-05-19 **Author:** Morr +**Last Updated:** 2026-05-25 --- @@ -12,7 +13,7 @@ **Date:** 2026-05-19 **Decision:** PRD covers full product vision across all 3 priority tiers. v1.0 = Day 1 + Week 1–2 features (FR-1 through FR-26). Later-tier features are documented in §10 as a roadmap, not as FRs. **Rationale:** User selected "Full vision PRD" during working mode selection. -**Status:** Captured in §10 Product Roadmap. +**Status:** Captured in §10 Product Roadmap. **UPDATED:** Expanded to include FR-27 through FR-32 from live testing. ### D-2 — WebRTC track disabling preferred **Date:** 2026-05-19 @@ -56,18 +57,6 @@ **Rationale:** Standard FoundryVTT module convention. Community translation is a common contribution pattern in the ecosystem. **Status:** Captured in §6.1 MVP Scope. -### D-11 — GM sees all activated feeds; own self-view is configurable -**Date:** 2026-05-19 -**Decision:** The GM always sees all activated player webcams. The GM's own feed visibility in their own view is a configurable module setting ("Show my own feed to myself", default ON). -**Rationale:** User answer to OQ-3: "All activated webcam of players + himself as an option." -**Status:** Captured in FR-6. OQ-3 closed. - -### D-12 — Native WebRTC only for v1.0; non-native AV backends deferred -**Date:** 2026-05-19 -**Decision:** Only FoundryVTT's native WebRTC AV backend is supported in v1.0. Jitsi Meeting Server and other backends are not in scope. -**Rationale:** User answer to OQ-4: "Not planned in first steps, maybe later." -**Status:** Captured in §5 Non-Goals. OQ-4 closed; deferred to Later roadmap. - ### D-9 — Native FoundryVTT socket API for state broadcast **Date:** 2026-05-19 **Decision:** Visibility Matrix changes are broadcast via the native FoundryVTT socket API using a registered module event name. `socketlib` will not be added as a dependency. @@ -80,14 +69,17 @@ **Rationale:** User confirmed: "Not sure, to be checked with the API itself." OQ-1 remains open but is unblocking — CSS fallback ships regardless. **Status:** OQ-1 downgraded from hard phase blocker to development-time research item. FR-7 unchanged. -| OQ | Phase Blocker? | Status | -|---|---|---| -| OQ-1: WebRTC track disable API access | Yes | Open — to be checked against v14 API during development; CSS fallback is safe default | -| OQ-2: socketlib vs native socket API | Yes | **RESOLVED** — native FoundryVTT socket API | -| OQ-3: GM own-feed hidden behavior | No | **RESOLVED** — GM always sees all activated feeds; own self-view is a configurable option | -| OQ-4: Non-native AV backend support | No | **RESOLVED** — native WebRTC only for v1.0; others deferred to Later roadmap | -| OQ-5: Scene hook timing | No | Deferred — not a concern for first implementation stage | -| OQ-6: Partial vs full preset application | No | Open — to be resolved during FR-15/FR-16 implementation | +### D-11 — GM sees all activated feeds; own self-view is configurable +**Date:** 2026-05-19 +**Decision:** The GM always sees all activated player webcams. The GM's own feed visibility in their own view is a configurable module setting ("Show my own feed to myself", default ON). +**Rationale:** User answer to OQ-3: "All activated webcam of players + himself as an option." +**Status:** Captured in FR-6. OQ-3 closed. + +### D-12 — Native WebRTC only for v1.0; non-native AV backends deferred +**Date:** 2026-05-19 +**Decision:** Only FoundryVTT's native WebRTC AV backend is supported in v1.0. Jitsi Meeting Server and other backends are not in scope. +**Rationale:** User answer to OQ-4: "Not planned in first steps, maybe later." +**Status:** Captured in §5 Non-Goals. OQ-4 closed; deferred to Later roadmap. ### D-13 — Reaction Cam / automation effects are opt-in (not opt-out) **Date:** 2026-05-19 @@ -107,7 +99,120 @@ **Rationale:** From brainstorming "panic button" + Marcus persona. Elevated to cross-cutting design rule. **Status:** Added to Cross-Cutting NFRs. -### D-16 — PRD finalized -**Date:** 2026-05-19 -**Decision:** PRD marked `status: final`. All decisions captured, all reconciliation gaps resolved, all [ASSUMPTION] tags indexed, polish complete. -**Status:** Closed. +--- + +## Live Testing Decisions (May 20-25, 2026) + +### D-16 — Full AV Dock Replacement +**Date:** 2026-05-20 +**Decision:** Replace FoundryVTT's native AV dock completely with custom ScryingPoolStrip and RoleRenderer components rather than hooking into the native dock. +**Rationale:** Live testing revealed limitations in hooking approach; full replacement provides better control over rendering, layout, and participant management. Enables consistent behavior across all AV states. +**Status:** Implemented in FR-27, FR-28. Captured in §4.6 Full AV Dock Replacement. +**Trade-offs:** +- PRO: Full control over UI/UX, consistent behavior, easier to extend +- PRO: Enables dock layout system and position persistence +- CON: Must handle all edge cases that native dock handles +- CON: Potential compatibility issues with other AV-related modules + +### D-17 — Dock Layout System +**Date:** 2026-05-22 +**Decision:** Implement configurable dock layouts with 6 options: vertical-sm, vertical-md, horizontal-sm, horizontal-md, mosaic-sm, mosaic-md. +**Rationale:** User feedback from live testing requested flexibility in how participant tiles are arranged to match different table setups and screen configurations. GM controls direction/size at world level; individual users can override size preference. +**Status:** Implemented in FR-29, FR-30. Captured in §4.7 Dock Layout System. +**Implementation Notes:** +- World-scoped `dockLayout` setting controls direction and canonical size +- Client-scoped `dockLayoutExpanded` setting allows per-user override ('' = inherit, 'sm' = small, 'md' = large) +- Director's Board includes visual selector for layout configuration +- Legacy boolean values automatically migrated to string format + +### D-18 — Position Persistence +**Date:** 2026-05-24 +**Decision:** ScryingPoolStrip window position persists across sessions using GM user flags. +**Rationale:** User feedback indicated frustration with repositioning the dock every session. Position persistence improves UX consistency. +**Status:** Implemented in FR-31. Captured in §4.8 Position Persistence. +**Implementation Notes:** +- Position (left, top) saved to GM user flags on move +- Loaded on module initialization via `_loadPosition()` method +- First-time users get default position + +### D-19 — ApplicationV2 API Migration +**Date:** 2026-05-20 +**Decision:** Migrate all UI components to use FoundryVTT v14 ApplicationV2 API with HandlebarsApplicationMixin. +**Rationale:** FoundryVTT v14 requires ApplicationV2 for proper window management, resizing, and lifecycle. Previous implementation had compatibility issues. +**Status:** Implemented in FR-32. Captured in §4.9 Implementation Quality. +**Implementation Notes:** +- ScryingPoolStrip extends HandlebarsApplicationMixin(ApplicationV2) +- DirectorsBoard extends HandlebarsApplicationMixin(ApplicationV2) +- Fixed jQuery parameter handling in _onRender methods +- Added proper PARTS and DEFAULT_OPTIONS static properties + +### D-20 — PortraitFallbackHandler Integration +**Date:** 2026-05-23 +**Decision:** Enhance portrait fallback system with dedicated PortraitFallbackHandler component for consistent image resolution. +**Rationale:** Live testing revealed inconsistent fallback behavior. Dedicated handler provides clear priority chain and better integration with custom portraits. +**Status:** Implemented in FR-8 enhancement, FR-26. Captured in Glossary and §4.1. +**Implementation Notes:** +- Priority chain: custom fallback portrait -> user.avatar -> user.character?.img -> mystery-man +- Integrated with RoleRenderer for participant list building +- Handled in buildParticipantList function + +### D-21 — CSS Build Pipeline +**Date:** 2026-05-24 +**Decision:** Add postinstall script to automatically build CSS from LESS source files. +**Rationale:** Streamlines development and distribution; ensures styles are always up-to-date. +**Status:** Implementation note in §6.1 MVP Scope. +**Implementation Notes:** +- Added postinstall script to package.json +- Moved styles to root styles/ folder for FoundryVTT compatibility +- Updated .gitignore to allow dist/styles/ for CSS distribution + +--- + +## Decision Summary Table + +| Decision | Date | Status | Related FRs | Section | +|----------|------|--------|-------------|---------| +| D-1 | 2026-05-19 | Active | FR-1-32 | §1, §6, §10 | +| D-2 | 2026-05-19 | Active | FR-7 | §4.1, §9 | +| D-3 | 2026-05-19 | Active | All | §5, §6 | +| D-4 | 2026-05-19 | Active | All | §7, Cross-cutting | +| D-5 | 2026-05-19 | Active | FR-1-26 | §4 | +| D-6 | 2026-05-19 | Active | FR-10 | §4.2 | +| D-7 | 2026-05-19 | Active | FR-24 | §4.5 | +| D-8 | 2026-05-19 | Active | All | §6.1 | +| D-9 | 2026-05-19 | Active | FR-2 | §4.1, §9 | +| D-10 | 2026-05-19 | Active | FR-7 | §4.1, §8 | +| D-11 | 2026-05-19 | Active | FR-6 | §4.1, §9 | +| D-12 | 2026-05-19 | Active | All | §5 | +| D-13 | 2026-05-19 | Active | FR-24, FR-25 | §4.5 | +| D-14 | 2026-05-19 | Active | FR-9 | §4.2 | +| D-15 | 2026-05-19 | Active | All automation | Cross-cutting | +| **D-16** | **2026-05-20** | **Active** | **FR-27, FR-28** | **§4.6** | +| **D-17** | **2026-05-22** | **Active** | **FR-29, FR-30** | **§4.7** | +| **D-18** | **2026-05-24** | **Active** | **FR-31** | **§4.8** | +| **D-19** | **2026-05-20** | **Active** | **FR-32** | **§4.9** | +| **D-20** | **2026-05-23** | **Active** | **FR-8, FR-26** | **§4.1, §4.5** | +| **D-21** | **2026-05-24** | **Active** | **N/A** | **§6.1** | + +--- + +## Open Questions Status + +| OQ | Phase Blocker? | Status | Related Decisions | +|---|---|---|---| +| OQ-1: WebRTC track disable API access | No | Open — to be checked against v14 API during development; CSS fallback is safe default | D-2, D-10 | +| OQ-2: socketlib vs native socket API | No | **RESOLVED** — native FoundryVTT socket API | D-9 | +| OQ-3: GM own-feed hidden behavior | No | **RESOLVED** — GM always sees all activated feeds; own self-view is configurable | D-11 | +| OQ-4: Non-native AV backend support | No | **RESOLVED** — native WebRTC only for v1.0 | D-12 | +| OQ-5: Scene hook timing | No | Open — not a concern for first implementation stage | | +| OQ-6: Partial vs full preset application | No | Open — to be resolved during FR-15/FR-16 implementation | | +| **OQ-7: Full AV replacement edge cases** | **No** | **Open** — Does full AV dock replacement handle all edge cases? | **D-16** | + +--- + +## PRD Version History + +| Version | Date | Status | Author | Changes | +|---------|------|--------|--------|---------| +| 1.0 | 2026-05-19 | final | Morr | Initial PRD with FR-1 through FR-26 | +| **1.1** | **2026-05-25** | **draft** | **Morr** | **Added FR-27 through FR-32 from live testing; updated D-1, added D-16 through D-21** | diff --git a/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md b/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md index 07b73ed..db838ae 100644 --- a/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md +++ b/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md @@ -1,17 +1,20 @@ --- title: "Video View Manager — FoundryVTT Webcam Visibility Control Module" -status: final +status: draft created: 2026-05-19 -updated: 2026-05-19 +updated: 2026-05-25 +version: 0.1.0 --- # PRD: Video View Manager ## 0. Document Purpose -This PRD is the authoritative specification for **Video View Manager**, a FoundryVTT v14 module. It is written for the module author (Morr) and future contributors, and it is the source of record for scope decisions during development. +This PRD is the authoritative specification for **Video View Manager** (module ID: `scrying-pool`), a FoundryVTT v14 module. It is written for the module author (Morr) and future contributors, and it is the source of record for scope decisions during development. -This document uses Glossary terms from §3 throughout §4+ and keeps functional requirements globally numbered FR-1 through FR-26 for stable reference. Inline `[ASSUMPTION]` tags in §4 mark unconfirmed inferences and are indexed in §9. The primary upstream input is `_bmad-output/brainstorming/brainstorming-session-2026-05-19-221747.md`. +This document uses Glossary terms from §3 throughout §4+ and keeps functional requirements globally numbered FR-1 through FR-32 for stable reference. Inline `[ASSUMPTION]` tags in §4 mark unconfirmed inferences and are indexed in §9. The primary upstream input is `_bmad-output/brainstorming/brainstorming-session-2026-05-19-221747.md`. + +**Implementation Updates:** This version incorporates enhancements from live testing completed May 25, 2026, including full AV dock replacement, configurable dock layouts, and position persistence. --- @@ -23,6 +26,12 @@ From that core, the module extends into session cinematography. Scene Presets le The module follows the **Progressive Enhancement Architecture**. Level 1 adds a right-click action to existing AV Tiles. Level 2 adds the Director's Board. Level 3 adds Scene Presets and automation. Participants stay informed, automation that changes an individual's on-screen presence requires explicit opt-in, and the GM always has an immediate override. +**Live Test Enhancements (May 2026):** +- Full replacement of Foundry's native AV dock with custom ScryingPoolStrip +- Configurable dock layouts (vertical, horizontal, mosaic) with size variants +- Position persistence across sessions +- Enhanced portrait fallback handling + --- ## 2. Target Users @@ -48,6 +57,8 @@ Alex has just joined their first online TTRPG. They are comfortable with Zoom bu - Spotlight one Participant for a dramatic reveal or emotional scene climax - Apply a Scene Preset when transitioning between scenes without manual intervention - Know at a glance which of 6+ Participants are hidden, visible, or offline +- Configure dock layout to match table preferences (vertical, horizontal, mosaic) +- Maintain dock position and size preferences across sessions **Social / Emotional** - Feel in control of the table atmosphere as a GM — the visual equivalent of adjusting the lights @@ -60,37 +71,6 @@ Alex has just joined their first online TTRPG. They are comfortable with Zoom bu - Groups using FoundryVTT v13 or earlier - Streamers requiring independent audience layouts (served in the later roadmap — §10) -### 2.4 Key User Journeys - -**UJ-1. Marcus transitions into a boss fight and spotlights the villain's lair.** -- **Persona + context:** Marcus, mid-session, activating a Scene named "Throne Room" where he wants only two Participants visible. -- **Entry state:** A Scene Preset is configured at prep time; the Director's Board is closed. -- **Path:** (1) Marcus activates the "Throne Room" scene. (2) The module auto-applies the saved preset — two Participants are visible and four are hidden. (3) A toast appears for all Participants: "Scene changed: camera layout updated." (4) Marcus glances at the AV Tile strip — hidden tiles show a grey overlay with a lock icon. -- **Climax:** The table's visual focus narrows to the two Participants in the scene without Marcus touching a control. -- **Resolution:** Marcus keeps narrating; he can override the preset with one right-click if needed. -- **Edge case:** If a Participant whose feed should be visible is offline, the hidden overlay still appears and the preset is marked partially applied. - -**UJ-2. Sofia opts out of HP-Reactive Cam Styling before the session.** -- **Persona + context:** Sofia, before the session starts, opening her FoundryVTT user settings. -- **Entry state:** Authenticated as a Participant; the Player Privacy Panel is available in module settings; HP-Reactive Cam Styling is currently ON from an earlier opt-in. -- **Path:** (1) Sofia opens the Player Privacy Panel. (2) She sees HP-Reactive Cam Styling: ON. (3) She toggles it OFF. (4) The setting saves instantly; no page reload is required. -- **Climax:** Sofia knows her camera will not change appearance based on her HP. -- **Resolution:** Her opt-out flag persists for future sessions in this world until she changes it. - -**UJ-3. Jake opens the Director's Board at session start to configure the layout.** -- **Persona + context:** Jake, 5 minutes before going live, with 5 Participants connected. -- **Entry state:** Authenticated as GM; the Director's Board has not been opened this session. -- **Path:** (1) Jake presses the keyboard shortcut (⌘/Ctrl+Shift+V). (2) The Director's Board opens as a floating window. (3) Jake sees all 5 Participants in a seating-chart layout with their current states. (4) He sets Participant 4 to "Hidden" (late joiner, not on yet). (5) He uses Spotlight on his own card to confirm his feed remains visible. -- **Climax:** Jake's table is visually configured for broadcast before he goes live. -- **Resolution:** The Director's Board stays open in a corner, and Jake uses it as a live monitor during the session. - -**UJ-4. Alex sees a toast notification when their feed is hidden.** -- **Persona + context:** Alex, playing for the first time; Marcus has just hidden Alex's camera to manage a technical glitch. -- **Entry state:** Alex's AV Tile is live; Marcus triggers hide from the context menu. -- **Path:** (1) Alex's tile changes to Portrait Fallback mode with a grey overlay. (2) Alex receives a toast: "GM has hidden your camera. Your portrait is shown to other Participants." (3) Alex sees a small "Camera hidden" badge on their own tile. -- **Climax:** Alex understands exactly what happened and why — no confusion or anxiety. -- **Resolution:** Alex can keep playing; their audio is unaffected. - --- ## 3. Glossary @@ -100,26 +80,31 @@ Alex has just joined their first online TTRPG. They are comfortable with Zoom bu - **Combat Cinematics Mode** — An automation mode that manages the Visibility Matrix during active FoundryVTT combat, spotlighting the active combatant's feed. Part of the Later roadmap. - **Director's Board** — The floating window for bulk Visibility Matrix management. Primary UI surface for Level 2/3 interactions. Synonyms are not permitted — this is not "the settings panel" or "the popout." - **Director's Board Stage Lighting States** — A Later-roadmap preset vocabulary tier for cinematic bulk actions such as Wash, Focus, and Blackout. +- **Dock Layout** — Configurable layout options for the AV dock strip: `vertical-sm`, `vertical-md`, `horizontal-sm`, `horizontal-md`, `mosaic-sm`, `mosaic-md`. Controlled via world setting `dockLayout` with client-scoped override via `dockLayoutExpanded`. - **Dual Layout System** — A Later-roadmap architecture that separates the Participant-facing layout from the Spectator View. +- **Full AV Replacement** — Complete replacement of FoundryVTT's native AV dock with custom ScryingPoolStrip and RoleRenderer components, providing full control over participant feed display. - **GM** — Game Master. The FoundryVTT user with the `GAMEMASTER` role. Has exclusive authority over the Visibility Matrix unless Player Permissions are extended (future roadmap). - **HP-Reactive Cam Styling** — An opt-in automation effect that changes the presentation of a Participant's feed based on HP-related game events. Part of the Later roadmap. - **NPC Presence Tiles** — A Later-roadmap feature that displays static image tiles for NPC voice actors or other non-camera presences. - **Participant** — Any connected FoundryVTT user with an AV presence (camera, microphone, or both), including the GM. - **Participant State** — One of eight enumerated states describing a Participant's AV presence: `active`, `hidden`, `self-muted`, `offline`, `cam-lost`, `reconnecting`, `never-connected`, `ghost`. Defined in §4.1. - **Player Privacy Panel** — The per-user settings interface for opting in or out of cinematic automation effects. Scoped to the current user; accessible from module settings. -- **Portrait Fallback** — A static image (user avatar or actor portrait) displayed in place of a live camera feed when a Participant has no camera, or when their feed is `cam-lost`. +- **Portrait Fallback** — A static image (user avatar or actor portrait) displayed in place of a live camera feed when a Participant has no camera, or when their feed is `cam-lost`. Enhanced with custom portrait handler integration via `PortraitFallbackHandler`. +- **PortraitFallbackHandler** — Component that manages custom portrait fallback images for participants, with priority: custom fallback → user avatar → character portrait → mystery-man placeholder. - **Progressive Enhancement Architecture** — The three-level UI model: Level 1 (right-click on existing AV Tiles — zero new UI), Level 2 (Director's Board), Level 3 (Scene Presets + full automation). Each level is independently useful. - **Pull Visibility Model** — A Later-roadmap visibility model in which feeds remain hidden until a Viewer explicitly requests them. - **Reaction Cam** — An opt-in feature that automatically spotlights a Participant's feed during key game moments (for example, taking damage in combat). Part of the Later roadmap. - **Reaction Clip System** — A Later-roadmap fallback that shows a short video snippet when a Participant has no live camera feed. +- **RoleRenderer** — Custom component that replaces Foundry's native AV tile rendering, integrating with ScryingPoolStrip and PortraitFallbackHandler for consistent participant display. - **Scene Preset** — A saved snapshot of the Visibility Matrix, optionally linked to a FoundryVTT Scene for automatic application on scene activation. +- **ScryingPoolStrip** — The custom AV dock strip component that replaces Foundry's native AV dock, implementing configurable layouts, position persistence, and integrated participant feed management. - **Spectator View** — A read-only camera layout independent from the Participant layout, intended for streaming audiences. Part of the Later roadmap. - **The Living Table** — A Later-roadmap concept that exposes the full seating-chart UI for `Map>` relationships. - **Token-Anchored Floating Cams** — A Later-roadmap feature that links camera surfaces to canvas tokens. - **Visibility Matrix** — The authoritative data structure representing all camera visibility relationships: `Map>`. Stored in world-level settings and broadcast to all clients on change. -- **Zero-UI Full Automation Mode** — A Later-roadmap mode that minimizes manual camera control after initial configuration. - **Visibility State** — The visibility setting for one Participant's camera as seen by one Viewer. Distinct from Participant State: a feed can be `active` but have a `hidden` Visibility State for specific viewers. - **Viewer** — A Participant who is receiving (watching) another Participant's camera feed. +- **Zero-UI Full Automation Mode** — A Later-roadmap mode that minimizes manual camera control after initial configuration. --- @@ -189,7 +174,7 @@ All Participant States produce appropriate visual feedback and do not cause AV T **Consequences (testable):** - Hiding or revealing a Participant's feed does not change the position of other AV Tiles. -- Mid-combat state transitions (`active` → `offline` → `reconnecting`) do not shift the combat tracker or map canvas layout. +- Mid-combat state transitions (`active` -> `offline` -> `reconnecting`) do not shift the combat tracker or map canvas layout. - A Participant in `ghost` state has no AV Tile rendered for other Participants. #### FR-6: GM sees all activated Participant feeds; GM self-view is configurable @@ -218,6 +203,7 @@ When a Participant has no camera device (`never-connected`) or enters `cam-lost` - The default Portrait Fallback uses the FoundryVTT user avatar and falls back to a system placeholder if no avatar is set. - Participants can set a custom Portrait Fallback through the Player Privacy Panel (FR-26). - Portrait Fallback renders at the same dimensions as a live camera-feed tile. +- Custom PortraitFallbackHandler integrates with priority: custom fallback portrait -> user.avatar -> user.character?.img -> mystery-man placeholder. **Feature-specific NFRs:** - Visibility Matrix updates must not block the FoundryVTT rendering loop; state changes apply asynchronously. @@ -235,7 +221,7 @@ When a Participant has no camera device (`never-connected`) or enters `cam-lost` A dedicated button in the FoundryVTT controls sidebar opens the Director's Board. A keyboard shortcut (default: `Ctrl+Shift+V`) also opens it. Realizes UJ-3. **Consequences (testable):** -- The Director's Board opens as a resizable, draggable `ApplicationV2` window [ASSUMPTION]. +- The Director's Board opens as a resizable, draggable ApplicationV2 window [ASSUMPTION]. - Opening the Director's Board does not change the existing AV Tile strip. - The keyboard shortcut is configurable in module settings. - The window closes and reopens instantly, supporting both a pre-session setup workflow and a live-monitor workflow. @@ -247,6 +233,7 @@ All connected Participants are shown with their current Visibility State, name, - Every Participant card displays the name, portrait, current Participant State, and current Visibility State. - The layout reads as a seating chart, not a list. - Updates to Visibility State appear in the Director's Board within 500 ms, matching FR-2. +- Dock layout selector is available in Director's Board for GM to configure world-level layout preference. #### FR-11: Per-Participant visibility toggle from Director's Board The GM can toggle any single Participant's Visibility State from that Participant's card in the Director's Board. @@ -311,7 +298,7 @@ A preset can be linked to a Scene; it applies automatically when that Scene is a **Consequences (testable):** - The Scene-to-Preset association is configured in Scene settings or module settings. -- Auto-apply fires on the `updateScene` hook, and a configurable 0–5000 ms pre-delay supports dramatic transitions [ASSUMPTION: sufficient hook timing — to be verified during FR-17 development]. +- Auto-apply fires on the `updateScene` hook, and a configurable 0-5000 ms pre-delay supports dramatic transitions [ASSUMPTION: sufficient hook timing — to be verified during FR-17 development]. - All clients receive the notification "Scene changed: camera layout updated." matching UJ-1. #### FR-18: Scene Preset auto-apply can be disabled per scene or globally @@ -401,6 +388,92 @@ A Participant can set a custom image as their Portrait Fallback (used in FR-8 wh - A file picker in the Player Privacy Panel sets the custom portrait. - Accepted formats are PNG, JPG, WEBP, and static GIF. - The image falls back to the FoundryVTT user avatar if no custom portrait is set, then to the system placeholder if no avatar exists. +- Custom portraits are handled by PortraitFallbackHandler with proper priority chain. + +--- + +### 4.6 Full AV Dock Replacement + +**Description:** Complete replacement of FoundryVTT's native AV dock with custom implementation via ScryingPoolStrip and RoleRenderer. This provides full control over participant feed rendering, layout, and display. + +**Functional Requirements:** + +#### FR-27: Custom AV dock strip (ScryingPoolStrip) +The module replaces Foundry's native AV dock with ScryingPoolStrip, a custom ApplicationV2-based component. + +**Consequences (testable):** +- ScryingPoolStrip opens automatically for all users (GM and players) when AV is active. +- The strip is implemented using FoundryVTT v14 ApplicationV2 API with HandlebarsApplicationMixin. +- Custom templates (roster-strip.hbs) render participant tiles with configurable layout. +- Strip integrates with PortraitFallbackHandler for consistent portrait display. + +#### FR-28: Custom role rendering (RoleRenderer) +RoleRenderer component handles individual participant feed rendering and management. + +**Consequences (testable):** +- RoleRenderer coordinates between StateStore, ScryingPoolController, AVTileAdapter, and PortraitFallbackHandler. +- Participant list is built with proper priority for fallback portraits (custom -> user avatar -> character portrait -> mystery-man). +- RoleRenderer manages strip lifecycle (open, close, rerender). + +--- + +### 4.7 Dock Layout System + +**Description:** Configurable dock layout options for the AV dock strip, allowing GMs and users to customize how participant tiles are arranged. + +**Functional Requirements:** + +#### FR-29: Dock layout configuration +The module provides 6 layout options via world-scoped `dockLayout` setting. + +**Consequences (testable):** +- Available layouts: `vertical-sm`, `vertical-md`, `horizontal-sm`, `horizontal-md`, `mosaic-sm`, `mosaic-md`. +- Layout is configured via Director's Board UI with visual selector. +- Changing layout re-renders the strip for all users via onChange callback. +- Default layout is `vertical-sm`. + +#### FR-30: Per-user layout size override +Users can override the GM's layout size preference via client-scoped `dockLayoutExpanded` setting. + +**Consequences (testable):** +- Setting values: `''` (inherit from world), `'sm'` (force small), `'md'` (force large). +- Client setting only affects size, not layout direction (vertical/horizontal/mosaic). +- Changing client override re-renders the strip for that user only. +- Legacy boolean values are migrated to string format automatically. + +--- + +### 4.8 Position Persistence + +**Description:** The ScryingPoolStrip remembers its window position across sessions for consistent user experience. + +**Functional Requirements:** + +#### FR-31: Window position persistence +ScryingPoolStrip position is saved and restored from GM user flags. + +**Consequences (testable):** +- Position (left, top coordinates) is saved when strip is moved. +- Position is loaded on module initialization via `_loadPosition()` method. +- Saved position applies only to the GM user; player strip positions follow GM configuration. +- First-time users get default position (left: 0, top: 0) or system default. + +--- + +### 4.9 Implementation Quality + +**Description:** Code quality improvements and FoundryVTT v14 compatibility. + +**Functional Requirements:** + +#### FR-32: ApplicationV2 API compatibility +All UI components properly extend and use FoundryVTT v14 ApplicationV2 API. + +**Consequences (testable):** +- ScryingPoolStrip extends HandlebarsApplicationMixin(ApplicationV2). +- DirectorsBoard extends HandlebarsApplicationMixin(ApplicationV2). +- All _onRender, _prepareContext, and _onClose methods properly implemented. +- jQuery parameter handling is compatible with ApplicationV2 expectations. --- @@ -422,13 +495,18 @@ A Participant can set a custom image as their Portrait Fallback (used in FR-8 wh ### 6.1 In Scope (v1.0) -- Core Visibility Toggle (FR-1 – FR-8): right-click toggle, broadcast, persistence, visual indicators, 8 Participant States, WebRTC track disabling with CSS fallback, Portrait Fallback -- Director's Board (FR-9 – FR-14): seating-chart window, bulk actions, Spotlight, keyboard shortcuts -- Scene-Aware Visibility Presets (FR-15 – FR-19): save, load, auto-apply, and JSON import/export -- Contextual Notifications (FR-20 – FR-22): toast system, verbosity configuration, persistent self-status badge -- Player Privacy Panel (FR-23 – FR-26): opt-in controls for future automation effects, custom portrait +- Core Visibility Toggle (FR-1 - FR-8): right-click toggle, broadcast, persistence, visual indicators, 8 Participant States, WebRTC track disabling with CSS fallback, Portrait Fallback +- Director's Board (FR-9 - FR-14): seating-chart window, bulk actions, Spotlight, keyboard shortcuts +- Scene-Aware Visibility Presets (FR-15 - FR-19): save, load, auto-apply, and JSON import/export +- Contextual Notifications (FR-20 - FR-22): toast system, verbosity configuration, persistent self-status badge +- Player Privacy Panel (FR-23 - FR-26): opt-in controls for future automation effects, custom portrait +- **Full AV Dock Replacement (FR-27 - FR-28)**: ScryingPoolStrip and RoleRenderer components +- **Dock Layout System (FR-29 - FR-30)**: 6 layout options with per-user override +- **Position Persistence (FR-31)**: Window position saved and restored +- **ApplicationV2 Compatibility (FR-32)**: Full v14 API compliance - FoundryVTT v14+ compatibility; `module.json` per v14 manifest schema - English UI strings; i18n-ready string keys for community translation +- CSS build pipeline with postinstall script for automatic CSS compilation ### 6.2 Out of Scope for MVP @@ -473,6 +551,7 @@ A Participant can set a custom image as their Portrait Fallback (used in FR-8 wh 4. ~~**OQ-4:** Should the module operate with Jitsi Meeting Server and other non-native AV backends (beyond native WebRTC), or is native WebRTC the only supported backend for v1.0?~~ **RESOLVED:** Native WebRTC only for v1.0. Non-native backends (Jitsi, etc.) are deferred to the Later roadmap. 5. **OQ-5:** Do FoundryVTT v14 scene activation hooks fire early enough for Scene Preset auto-apply (FR-17) to avoid a visible flash of the wrong camera layout before the preset applies? Not a concern for the first implementation stage — defer to testing. 6. **OQ-6:** Should Scene Presets support partial application — applying only to currently connected Participants and deferring state for offline ones — or should they always apply to all participant slots unconditionally? This cannot be decided yet and will be resolved during FR-15/FR-16 implementation. +7. **OQ-7 (NEW):** Does the full AV dock replacement (FR-27-28) properly handle all edge cases of Foundry's native AV system, including dynamic participant join/leave, device changes, and AV mode toggles? --- @@ -483,6 +562,9 @@ A Participant can set a custom image as their Portrait Fallback (used in FR-8 wh - **§4.2 / FR-9:** FoundryVTT v14's `ApplicationV2` API is the correct pattern for a resizable floating window with persistent state. - **§4.3 / FR-15:** 50 presets per world is an adequate upper bound for any campaign use case. If communities surface larger preset libraries, this can increase. - **§4.3 / FR-17:** The `updateScene` hook (or its v14 equivalent) fires early enough in the scene-transition lifecycle to apply the preset before the AV Tile strip re-renders. +- **§4.6 / FR-27:** ScryingPoolStrip as ApplicationV2-based component can fully replace Foundry's native AV dock without breaking core functionality. +- **§4.7 / FR-29:** 6 dock layout options (vertical-sm/md, horizontal-sm/md, mosaic-sm/md) cover the majority of table configuration needs. +- **§4.8 / FR-31:** GM user flags are the appropriate storage mechanism for strip position persistence. --- @@ -512,23 +594,28 @@ The following concepts from the brainstorming session are architecturally consis **Compatibility** - The module must not conflict with other popular FoundryVTT modules (Monk's Hotbar, Token Action HUD, etc.) by patching shared DOM selectors or overriding core FoundryVTT hooks without proper chaining. - All hooks must use FoundryVTT's `Hooks.on()` registration pattern, never `Hooks.once()` for persistent behavior. +- **NEW:** Full AV replacement must coexist with other modules that patch AV-related hooks; proper hook chaining is required. **Performance** - No Visibility Matrix operation should block the FoundryVTT main render loop. - The Director's Board must render and become interactive within 1 second even with 12 Participants. - Socket message payload for a Visibility Matrix update must be ≤ 4 KB. +- **NEW:** Strip rendering with 6+ participants must maintain 60fps in all layout modes. **Reliability** - If the socket broadcast fails because of a network interruption, the GM client retries up to 3 times before surfacing an error notification. - The module must fail gracefully if `game.webrtc` is unavailable (for example, when AV is disabled); all UI elements are hidden or disabled rather than erroring. +- **NEW:** Position persistence must handle corrupted or invalid saved positions gracefully. **Privacy** - The module does not transmit any data outside the FoundryVTT world. No analytics, telemetry, or third-party calls. - Participant names and portraits are used only within the FoundryVTT session; no external storage is allowed. +- **NEW:** Custom portrait images are stored in FoundryVTT's standard file storage; no external uploads. **Accessibility** - All interactive elements in the Director's Board have ARIA labels and are keyboard navigable (FR-14). - State-indicator icons (FR-4) include tooltip text for screen-reader compatibility. +- **NEW:** All dock layout options are keyboard-navigable and have proper ARIA labels. **Language and Voice** - Default UI labels use plain language: "Show," "Hide," "Spotlight," "Hidden by GM," "No Camera." Technical or cinematic vocabulary (for example, "Visibility Matrix" or "Wash / Focus / Blackout") is reserved for documentation, tooltips in advanced mode, and developer-facing strings — never as primary interface labels. @@ -540,4 +627,6 @@ The following concepts from the brainstorming session are architecturally consis - This is a non-negotiable cross-cutting rule: no automation may be implemented if it cannot be interrupted or overridden immediately by the GM. **Delivery Risk Note (v1.0 scope)** -- v1.0 intentionally combines the Day 1 core toggle (FR-1–FR-8) with Week 1–2 enhancements (FR-9–FR-26) into a single release. The brainstorming established Day 1 as a shippable standalone product; if delivery constraints arise, the Level 1 core toggle (FR-1–FR-8) is the minimum shippable increment, and all higher-level features can shift to v1.1 without breaking the architecture. +- v1.0 intentionally combines the Day 1 core toggle (FR-1-8) with Week 1-2 enhancements (FR-9-26) and live-test enhancements (FR-27-32) into a single release. The brainstorming established Day 1 as a shippable standalone product; if delivery constraints arise, the Level 1 core toggle (FR-1-8) is the minimum shippable increment, and all higher-level features can shift to v1.1 without breaking the architecture. + +--- \ No newline at end of file diff --git a/src/core/PresetImportExportManager.js b/src/core/PresetImportExportManager.js index 3958eb2..d448ba5 100644 --- a/src/core/PresetImportExportManager.js +++ b/src/core/PresetImportExportManager.js @@ -106,10 +106,11 @@ export class PresetImportExportManager { * @returns {string} Generated filename. */ generateExportFilename(worldName, includeTimestamp = true) { - const name = worldName ?? this._adapter.scenes.current?.()?.name ?? 'world'; + const currentScene = this._adapter.scenes.current?.(); + const name = worldName ?? currentScene?.parent?.name ?? currentScene?.name ?? 'world'; // Sanitize name: replace any non-alphanumeric, dash, or underscore with underscore // Also handle empty string by using 'world' as fallback - const safeName = name.replace(/[^a-zA-Z0-9-_]/g, '_').toLowerCase() || 'world'; + const safeName = name.replace(/[^a-zA-Z0-9\-_]/g, '_').toLowerCase() || 'world'; const timestamp = includeTimestamp ? `_${Date.now()}` : ''; return `scrying-pool-presets-${safeName}${timestamp}.json`; } @@ -235,7 +236,7 @@ export class PresetImportExportManager { } // Validate preset name characters (alphanumeric, dash, underscore, space, dot) - if (!/^[a-zA-Z0-9\s._-]+$/.test(name)) { + if (!/^[a-zA-Z0-9 ._-]+$/.test(name)) { results.push({ name, preset: null, error: `Preset "${name}": name contains invalid characters (only alphanumeric, space, dot, dash, underscore allowed)` }); continue; } @@ -339,11 +340,13 @@ export class PresetImportExportManager { const result = await this._replacePresets(data, validPresets, existingCount); // Merge extraction errors with replace errors result.errors = [...errors, ...result.errors]; + result.success = result.errors.length === 0; return result; } const result = await this._mergePresets(data, validPresets, existingPresetNames); // Merge extraction errors with merge errors result.errors = [...errors, ...result.errors]; + result.success = result.errors.length === 0; return result; } diff --git a/src/ui/gm/PresetExportDialog.js b/src/ui/gm/PresetExportDialog.js index fbf0c6d..0c23c35 100644 --- a/src/ui/gm/PresetExportDialog.js +++ b/src/ui/gm/PresetExportDialog.js @@ -93,7 +93,8 @@ export class PresetExportDialog extends _AppBase { return { presetCount, sceneName, - filename: this._exportManager.generateExportFilename(sceneName, false), + hasScene: !!currentScene, + filename: this._exportManager.generateExportFilename(sceneName), }; } @@ -165,14 +166,14 @@ export class PresetExportDialog extends _AppBase { btn.textContent = ''; const spinner = document.createElement('i'); spinner.className = 'fas fa-spinner fa-spin'; - const text = document.createTextNode(' Exporting...'); + const text = document.createTextNode(' ' + this._adapter.i18n.localize('scrying-pool.presetExport.exporting')); btn.appendChild(spinner); btn.appendChild(text); // Export presets const jsonString = await this._exportManager.exportAllPresets(); const currentScene = this._adapter.scenes.current?.(); - const worldName = this._adapter.scenes.current?.()?.parent?.name ?? currentScene?.name ?? 'world'; + const worldName = currentScene?.parent?.name ?? currentScene?.name ?? 'world'; const filename = this._exportManager.generateExportFilename(worldName); // Trigger download @@ -180,7 +181,7 @@ export class PresetExportDialog extends _AppBase { // Show success notification if (this._adapter.notifications) { - this._adapter.notifications.info('Scene presets exported successfully.'); + this._adapter.notifications.info(this._adapter.i18n.localize('scrying-pool.presetExport.exportSuccess')); } // Close dialog @@ -188,7 +189,7 @@ export class PresetExportDialog extends _AppBase { } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); if (this._adapter.notifications) { - this._adapter.notifications.error('Failed to export presets: ' + escapeHtml(errorMsg)); + this._adapter.notifications.error(this._adapter.i18n.localize('scrying-pool.presetExport.exportFailed') + ': ' + escapeHtml(errorMsg)); } } finally { if (btn) { diff --git a/src/ui/gm/PresetImportDialog.js b/src/ui/gm/PresetImportDialog.js index 9d86306..2689e09 100644 --- a/src/ui/gm/PresetImportDialog.js +++ b/src/ui/gm/PresetImportDialog.js @@ -8,6 +8,7 @@ */ import { PresetImportExportManager } from '../../core/PresetImportExportManager.js'; +import { isValidScenePreset } from '../../contracts/scene-preset.js'; import { escapeHtml } from '../../utils/html.js'; // Maximum file size: 5MB @@ -78,6 +79,10 @@ export class PresetImportDialog extends _AppBase { /** @type {boolean} */ this._requiresConfirmation = false; + // Concurrency guard for async file operations + /** @type {boolean} */ + this._parsingInProgress = false; + // Event listener tracking for cleanup /** @type {Array<{element: Element, type: string, listener: EventListener}>} */ this._eventListeners = []; @@ -110,8 +115,8 @@ export class PresetImportDialog extends _AppBase { previewItems: this._previewItems, requiresConfirmation: this._requiresConfirmation, selectedFileName: this._selectedFile?.name ?? null, - mergeLabel: 'Merge', - replaceLabel: 'Replace', + mergeLabel: this._adapter.i18n.localize('scrying-pool.presetImport.importModeMerge'), + replaceLabel: this._adapter.i18n.localize('scrying-pool.presetImport.importModeReplace'), }; } @@ -272,14 +277,17 @@ export class PresetImportDialog extends _AppBase { * @private */ async _parseAndPreviewFile() { - if (!this._selectedFile) { - this._previewItems = []; - this._requiresConfirmation = false; - this.render(); - return; - } + if (this._parsingInProgress) return; + this._parsingInProgress = true; try { + if (!this._selectedFile) { + this._previewItems = []; + this._requiresConfirmation = false; + this.render(); + return; + } + const content = await this._readFileAsText(this._selectedFile); const data = JSON.parse(content); @@ -290,19 +298,24 @@ export class PresetImportDialog extends _AppBase { this._previewItems = []; const existingNames = new Set(this._scenePresetManager.list().map(p => p.name)); - for (const [name] of Object.entries(data.presets || {})) { + for (const [name, presetData] of Object.entries(data.presets || {})) { let valid = true; let error = undefined; - try { - // Check if preset name already exists (for merge mode preview) - if (this._mode === 'merge' && existingNames.has(name)) { - valid = false; - error = 'Already exists - will be skipped'; - } - } catch (err) { + // Check if preset name already exists (for merge mode preview) + if (this._mode === 'merge' && existingNames.has(name)) { valid = false; - error = escapeHtml(err instanceof Error ? err.message : String(err)); + error = this._adapter.i18n.localize('scrying-pool.presetImport.previewWillSkip'); + } + + // Validate preset structure for preview accuracy + if (valid && presetData) { + try { + isValidScenePreset(presetData); + } catch (err) { + valid = false; + error = escapeHtml(err instanceof Error ? err.message : String(err)); + } } this._previewItems.push({ name, valid, error }); @@ -318,9 +331,11 @@ export class PresetImportDialog extends _AppBase { this.render(); } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); - this._previewItems = [{ name: this._selectedFile.name, valid: false, error: escapeHtml(errorMsg) }]; + this._previewItems = [{ name: this._selectedFile?.name ?? 'unknown', valid: false, error: escapeHtml(errorMsg) }]; this._requiresConfirmation = false; this.render(); + } finally { + this._parsingInProgress = false; } } @@ -356,7 +371,7 @@ export class PresetImportDialog extends _AppBase { async _onImport() { if (!this._selectedFile) { if (this._adapter.notifications) { - this._adapter.notifications.warn('Please select a file first'); + this._adapter.notifications.warn(this._adapter.i18n.localize('scrying-pool.presetImport.selectFileFirst')); } return; } @@ -403,7 +418,7 @@ export class PresetImportDialog extends _AppBase { btn.textContent = ''; const spinner = document.createElement('i'); spinner.className = 'fas fa-spinner fa-spin'; - const text = document.createTextNode(' Importing...'); + const text = document.createTextNode(' ' + this._adapter.i18n.localize('scrying-pool.presetImport.importing')); btn.appendChild(spinner); btn.appendChild(text); @@ -420,13 +435,13 @@ export class PresetImportDialog extends _AppBase { // Show errors const errorMessages = result.errors.map(e => escapeHtml(e)).join('\n'); if (this._adapter.notifications) { - this._adapter.notifications.error('Failed to import presets\n' + errorMessages); + this._adapter.notifications.error(this._adapter.i18n.localize('scrying-pool.presetImport.importFailed') + '\n' + errorMessages); } } } catch (err) { const errorMsg = err instanceof Error ? err.message : String(err); if (this._adapter.notifications) { - this._adapter.notifications.error('Failed to import presets: ' + escapeHtml(errorMsg)); + this._adapter.notifications.error(this._adapter.i18n.localize('scrying-pool.presetImport.importFailed') + ': ' + escapeHtml(errorMsg)); } } finally { btn.disabled = false; diff --git a/templates/preset-export.hbs b/templates/preset-export.hbs index db0dc2d..5622aad 100644 --- a/templates/preset-export.hbs +++ b/templates/preset-export.hbs @@ -21,7 +21,7 @@
-