# Addendum — Video View Manager PRD **Workspace:** `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/` **Created:** 2026-05-25 **Author:** Morr **Purpose:** Capture implementation decisions, technical notes, and downstream documentation that earned a place but does not fit the PRD's main narrative. --- ## Implementation Notes from Live Testing (May 20-25, 2026) ### Full AV Dock Replacement (Story 5-1) **Decision:** Implement complete replacement of Foundry's native AV dock rather than hooking into it. **Rationale:** - Hooking approach proved unreliable during live testing - Full replacement provides consistent behavior across all AV states - Enables advanced features like dock layouts and position persistence - Better control over rendering pipeline **Implementation Details:** - **ScryingPoolStrip**: Custom ApplicationV2-based component that serves as the main AV dock - Replaces `game.webrtc.render()` output - Opens automatically for all users when AV is active - Uses Handlebars templates (roster-strip.hbs) - Integrates with RoleRenderer for participant management - **RoleRenderer**: Orchestrator component that: - Manages ScryingPoolStrip lifecycle - Coordinates between StateStore, ScryingPoolController, AVTileAdapter - Handles participant list building with proper fallback priorities - Provides `rerenderStrip()` method for on-demand refreshes **Files Modified:** - `src/ui/gm/ScryingPoolStrip.js` - Complete rewrite to ApplicationV2 - `src/ui/RoleRenderer.js` - Enhanced with rerenderStrip capability - `module.js` - Updated to open strip for all users, not just GM - `templates/roster-strip.hbs` - Custom template for participant tiles - `styles/scrying-pool.css` - Main stylesheet - `styles/components/_roster-strip.less` - Strip-specific styles **Technical Notes:** - Strip is created and opened during `ready` hook - Previous implementation only opened for GM; now opens for all users - Position loading happens in `_loadPosition()` method - Strip is automatically closed and reopened when layout changes **Compatibility Considerations:** - Must coexist with other modules that patch AV-related hooks - Proper hook chaining required to avoid conflicts - Native Foundry AV controls remain functional --- ### Dock Layout System **Decision:** Implement 6 configurable layout options with GM control and per-user override. **Implementation Details:** **World-Scoped Setting: `dockLayout`** - Type: String - Default: `"vertical-sm"` - Options: `vertical-sm`, `vertical-md`, `horizontal-sm`, `horizontal-md`, `mosaic-sm`, `mosaic-md` - OnChange: Triggers `roleRenderer?.rerenderStrip()` - UI: Configured via Director's Board with visual icon selector **Client-Scoped Setting: `dockLayoutExpanded`** - Type: String - Default: `""` (empty string = inherit from world) - Options: `""` (inherit), `"sm"` (force small), `"md"` (force large) - OnChange: Triggers `roleRenderer?.rerenderStrip()` - Purpose: Allows individual users to override GM's size preference **Layout Resolution Logic:** ```javascript const rawLayout = settings.get('dockLayout'); // e.g., "vertical-sm" const baseLayout = typeof rawLayout === 'string' ? rawLayout : 'vertical-sm'; const sizeOverride = settings.get('dockLayoutExpanded'); // '' | 'sm' | 'md' const parts = baseLayout.split('-'); const dir = parts.slice(0, -1).join('-'); // e.g., "vertical" const canonicalSize = parts[parts.length - 1]; // e.g., "sm" const effectiveSize = (sizeOverride === 'sm' || sizeOverride === 'md') ? sizeOverride : canonicalSize; const dockLayout = `${dir}-${effectiveSize}`; // e.g., "vertical-md" ``` **Layout Characteristics:** - `vertical-sm`: Compact vertical list, small tiles, icons only - `vertical-md`: Expanded vertical list, large tiles, shows names - `horizontal-sm`: Compact horizontal row, small tiles - `horizontal-md`: Expanded horizontal row, large tiles with names - `mosaic-sm`: Grid layout, small tiles - `mosaic-md`: Grid layout, large tiles with names **Files Modified:** - `src/ui/gm/DirectorsBoard.js` - Added dock layout selector to UI - `src/ui/gm/ScryingPoolStrip.js` - Layout resolution and rendering logic - `module.js` - Setting registrations with onChange callbacks - `lang/en.json` - Added localization strings for layout options - `templates/directors-board.hbs` - Added layout selector UI - `styles/components/_directors-board.less` - Layout selector styling - `styles/components/_roster-strip.less` - Per-layout styling **Migration Notes:** - Legacy boolean values for `dockLayoutExpanded` are automatically migrated to string format - Migration code in `ready` hook checks for boolean values and resets to `""` --- ### Position Persistence **Decision:** Save and restore ScryingPoolStrip window position. **Implementation Details:** **Storage Mechanism:** - Position saved to GM user flags: `game.user.setFlag('scrying-pool', 'stripState', { left, top })` - Loaded during strip construction via `_loadPosition()` method - Applied to options.position before initial render **Position Object:** ```javascript { left: number, // CSS pixel value top: number // CSS pixel value } ``` **Lifecycle:** 1. Strip constructed with default position 2. `_loadPosition()` called in constructor 3. If saved position exists, applied to `this.options.position` 4. Strip rendered at saved position 5. On drag/move, new position saved to flags **Files Modified:** - `src/ui/gm/ScryingPoolStrip.js` - Added `_loadPosition()` method - `module.js` - No changes needed; handled internally **Edge Cases:** - First-time users: No saved position, uses default - Invalid position values: Caught and ignored, uses default - Corrupted flags: Handled gracefully with try/catch --- ### PortraitFallbackHandler Integration **Decision:** Enhance portrait fallback with dedicated handler component. **Implementation Details:** **Priority Chain:** 1. Custom fallback portrait (from Player Privacy Panel) 2. User avatar (`user.avatar`) 3. Character portrait (`user.character?.img`) 4. Mystery-man placeholder (`icons/svg/mystery-man.svg`) **Integration Points:** - Passed to `buildParticipantList()` function - Used in RoleRenderer construction - Applied when building participant objects for strip rendering **Code Example:** ```javascript const avatarSrc = portraitFallbackHandler?.getFallbackImageURL(userId) || user.avatar || user.character?.img || 'icons/svg/mystery-man.svg'; ``` **Files Modified:** - `src/ui/RoleRenderer.js` - Passes handler to strip - `src/ui/gm/ScryingPoolStrip.js` - Accepts handler in constructor - `src/ui/gm/ScryingPoolStrip.js` - Uses handler in `buildParticipantList` - `module.js` - Handler created and passed to RoleRenderer --- ### ApplicationV2 Migration **Decision:** Migrate all UI components to ApplicationV2 API. **Changes from Previous Implementation:** **Before (Application base class):** ```javascript static get defaultOptions() { return { id: 'scrying-pool-strip', popOut: true, resizable: false, title: 'Scrying Pool', classes: ['scrying-pool-strip'], }; } ``` **After (ApplicationV2 with HandlebarsApplicationMixin):** ```javascript static DEFAULT_OPTIONS = { id: 'scrying-pool-strip', classes: ['scrying-pool-strip'], window: { title: 'Scrying Pool', resizable: false }, position: { width: 240 }, }; static PARTS = { strip: { template: 'modules/scrying-pool/templates/roster-strip.hbs', }, }; constructor(options) { super(options); // ApplicationV2 lifecycle methods } async _prepareContext(options) { // Return context for template } _onRender(context, options) { // Called after render } _onClose() { // Called on close } ``` **Key Differences:** - Uses `DEFAULT_OPTIONS` static property instead of `defaultOptions` getter - Uses `PARTS` static property for template configuration - Lifecycle methods: `_prepareContext`, `_onRender`, `_onClose` - Proper position management via `setPosition()` - Better separation of concerns **Files Modified:** - `src/ui/gm/ScryingPoolStrip.js` - Complete migration to ApplicationV2 - `src/ui/gm/DirectorsBoard.js` - Verified ApplicationV2 compatibility - Fixed jQuery parameter handling in `_onRender` methods **Bug Fixes:** - Fixed `DirectorsBoard` position loading error (D-19) - Fixed ApplicationV2 jQuery parameter handling (a05d3ca) --- ### CSS Build Pipeline **Decision:** Automate CSS compilation from LESS source files. **Implementation Details:** **Build Script:** - Added to `package.json`: ```json "scripts": { "build:css": "lessc styles/scrying-pool.less > styles/scrying-pool.css", "postinstall": "npm run build:css" } ``` **File Structure:** ``` styles/ ├── scrying-pool.less # Main LESS file (imports components) ├── scrying-pool.css # Compiled output (gitignored, generated) └── components/ ├── _directors-board.less # Director's Board styles ├── _roster-strip.less # Strip styles └── ... ``` **Changes:** - Moved CSS to root `styles/` folder for FoundryVTT compatibility - Added component LESS files for better organization - Updated `.gitignore` to allow `dist/styles/` for distribution - Updated `module.json` styles reference - Added build comments to scrying-pool.less **Git Changes:** - `styles/scrying-pool.css` - Generated, not tracked in git - `styles/scrying-pool.less` - Source file, tracked - `.gitignore` - Added `dist/styles/` exception - `package.json` - Added build scripts --- ## Module Settings Reference ### World-Scoped Settings | Setting Key | Type | Default | Description | onChange | |------------|------|---------|-------------|----------| | `visibilityMatrix` | Object | `{ _version: 1, matrix: {} }` | Visibility state for all participants | Triggers stateStore.init() and strip rerender | | `dockLayout` | String | `"vertical-sm"` | Dock layout configuration | Triggers roleRenderer.rerenderStrip() | | `autoApplyEnabled` | Boolean | `true` | Enable Scene Preset auto-apply | None | | `showGMSelfFeed` | Boolean | `true` | Show GM's own feed in their view | None | ### Client-Scoped Settings | Setting Key | Type | Default | Description | onChange | |------------|------|---------|-------------|----------| | `dockLayoutExpanded` | String | `""` | Per-user layout size override | Triggers roleRenderer.rerenderStrip() | | `notificationVerbosity` | String | `"all"` | Notification output mode | None | --- ## Technical Architecture Notes ### Component Hierarchy ``` FoundryVTT Core ├── game.webrtc (native AV) │ └── Module: scrying-pool ├── module.js (entry point) │ ├── Hooks registration │ ├── Settings registration │ └── Component initialization │ ├── Data Layer │ ├── FoundryAdapter (settings, users, hooks wrapper) │ ├── StateStore (visibility matrix persistence) │ └── SocketHandler (broadcast and receive) │ ├── Core Logic │ ├── ScryingPoolController (visibility management) │ └── VisibilityManager (state mutations) │ ├── UI Layer │ ├── RoleRenderer (AV dock orchestrator) │ │ └── ScryingPoolStrip (custom AV dock) │ │ ├── templates/roster-strip.hbs │ │ └── styles/components/_roster-strip.less │ │ │ ├── GM Controls │ │ ├── DirectorsBoard (bulk management) │ │ │ ├── templates/directors-board.hbs │ │ │ └── styles/components/_directors-board.less │ │ ├── ConfirmationBar (preset feedback) │ │ └── ActionPopover (right-click menu) │ │ │ ├── Player Controls │ │ └── PlayerPrivacyPanel (opt-in management) │ │ │ └── Shared │ ├── ScryingPoolCameraViews (AV integration) │ ├── StripOverlayLayer (UI overlays) │ └── PortraitFallbackHandler (image resolution) │ └── Utilities ├── uuid.js (ID generation) └── ... ``` ### Hook Registration **Hooks Used:** - `init` - Module initialization, settings registration - `ready` - Component construction, AV integration - `setup` - Early initialization - `renderAVConfig` - AV configuration rendering - `updateScene` - Scene activation (for preset auto-apply) - Various AV-related hooks for full replacement **Hook Pattern:** ```javascript Hooks.on('hookName', () => { // Handler code }); ``` --- ## Testing Notes ### Unit Tests - Tests located in `tests/unit/` - Primarily test ScryingPoolStrip functionality - Mock FoundryVTT globals for test environment - Test files: `ScryingPoolStrip.test.js` ### Test Coverage (from live testing) - ✅ Full AV dock replacement - ✅ Dock layout switching (all 6 options) - ✅ Position persistence across sessions - ✅ Per-user size override - ✅ ApplicationV2 compatibility - ✅ Portrait fallback with all priority levels - ✅ PortraitFallbackHandler integration - ✅ CSS build pipeline ### Known Issues Resolved - DirectorsBoard position loading error - Fixed (5b421d6) - ApplicationV2 jQuery parameter handling - Fixed (a05d3ca) - CSS class selector in StripOverlayLayer - Fixed (ea4462e) - StripOverlayLayer initialization timing - Fixed (6f07e48) - RegisterMenu method missing - Fixed (0cb046b) - GMPlayerPrivacySelectorMenu ApplicationV2 - Fixed (e2da477) --- ## File Changes Summary (May 20-25, 2026) ### Major Enhancements | Commit | Date | Description | Files Changed | |--------|------|-------------|---------------| | 6d7a0b5 | 2026-05-20 | Story 4.2: Implement full AV replacement | 5 files | | c4a375f | 2026-05-20 | Story 4.2: WebRTC stream access | 3 files | | 20d13fc | 2026-05-20 | Story 4.2: Fix lint errors | 2 files | | 25dd427 | 2026-05-20 | Update tests for Story 5-1 | 1 file | | f8cbb75 | 2026-05-20 | Code review patches | 4 files | | 7b56d62 | 2026-05-25 | **Finalize deck strip and management** | **12 files** | ### Commit 7b56d62 Details (Final Live Test Enhancements) **Title:** Finalize deck strip and management **Author:** LeRatierBretonnier **Date:** Mon May 25 00:51:46 2026 +0200 **Changes:** - `lang/en.json` - Added dock layout localization strings - `module.js` - Added dockLayout, dockLayoutExpanded settings with onChange handlers; added legacy value migration; updated strip initialization - `src/ui/RoleRenderer.js` - Added rerenderStrip() method; passes portraitFallbackHandler to strip - `src/ui/gm/DirectorsBoard.js` - Added dock layout selector UI and handler - `src/ui/gm/ScryingPoolStrip.js` - Complete ApplicationV2 migration; added _loadPosition(); layout resolution logic; position persistence; PortraitFallbackHandler integration - `src/ui/shared/ScryingPoolCameraViews.js` - Minor updates - `styles/components/_directors-board.less` - Layout selector styling - `styles/components/_roster-strip.less` - Per-layout styling for all 6 options - `styles/scrying-pool.css` - Built from LESS source - `templates/directors-board.hbs` - Dock layout selector UI - `templates/roster-strip.hbs` - Updated for layout support - `tests/unit/ui/gm/ScryingPoolStrip.test.js` - Updated tests for new functionality **Lines Changed:** +755, -141 across 12 files --- ## Future Considerations ### Potential v1.1 Features 1. **Compatibility Testing:** Thorough testing with other popular FoundryVTT modules 2. **Performance Optimization:** Strip rendering with large numbers of participants (20+) 3. **Additional Layout Options:** More mosaic variants, custom grid configurations 4. **Accessibility Enhancements:** Screen reader testing, high contrast modes 5. **Mobile Support:** Better touch interaction for Director's Board ### Architecture Improvements 1. **TypeScript Migration:** Consider migrating from JavaScript to TypeScript for better type safety 2. **Component Testing:** Expand unit test coverage for all components 3. **Integration Tests:** Add end-to-end testing for critical user journeys 4. **Documentation:** Expand API documentation for module developers ### Roadmap Alignment The following Later-roadmap features from §10 are now more feasible with the full AV replacement architecture: - **Token-Anchored Floating Cams** - Can leverage ScryingPoolStrip infrastructure - **Spectator View** - Can use similar dock replacement pattern - **Dual Layout System** - Can extend current layout system - **Browser Source API** - Can expose strip layouts for OBS integration --- ## References - **Module ID:** `scrying-pool` - **Module Title:** Scrying Pool - **Version:** 0.1.0 - **FoundryVTT Compatibility:** v14+ - **Repository:** video-view-manager - **PRD Location:** `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/` - **Implementation Artifacts:** `_bmad-output/implementation-artifacts/` - **Tests:** `tests/unit/` --- *This addendum captures technical implementation details that support the PRD but do not belong in its main narrative. For authoritative requirements, see `prd.md`. For decision rationale, see `.decision-log.md`.*