+```
+
+**Alternative:** Use CSS custom properties
+```javascript
+// In _prepareContext
+document.documentElement.style.setProperty('--widget-width', `${width}px`);
+
+// In CSS
+.sp-participant-item {
+ width: var(--widget-width, 80px);
+}
+```
+
+---
+
+## โ
Action Items & Next Steps
+
+### Implementation Tasks
+- [ ] Register `widgetWidthSm` and `widgetWidthMd` settings in module.js
+- [ ] Add dropdown UI to Director's Board template
+- [ ] Add width dropdown handlers in DirectorsBoard.js
+- [ ] Pass width values to ScryingPoolStrip context
+- [ ] Apply width values to video elements in template
+- [ ] Update CSS to respect width settings
+- [ ] Add localization strings for width settings
+- [ ] Add input validation for width values
+- [ ] Create unit tests for width functionality
+- [ ] Manual testing of all acceptance criteria
+
+### Cross-Story Coordination
+- [ ] Verify integration with Dock Layout System (Story 5-1)
+- [ ] Verify no conflicts with existing settings
+- [ ] Verify proper scoping of CSS changes
+- [ ] Update Director's Board documentation
+
+### Release Preparation
+- [ ] Update module.json version to 0.1.1
+- [ ] Add feature to release notes
+- [ ] Update user documentation
+- [ ] Test with various FoundryVTT module combinations
+
+---
+
+## ๐ Story Completion Status
+
+| Task | Status | Notes |
+|------|--------|-------|
+| Story requirements extracted | โ
| From PRD FR-33, FR-34 |
+| Epic context loaded | โ
| Epic 5: Full AV Replacement |
+| Architecture analysis | โ
| Follows existing patterns |
+| Previous story intelligence | โ
| Patterns from Stories 1-3, 2-2, 5-1 |
+| Git intelligence | โ
| Similar patterns from Story 5-1 |
+| Technical research | โ
| FoundryVTT settings API validated |
+| Story file created | โ
| Complete documentation |
+| Sprint status synced | โณ | To be updated |
+
+**Status:** ready-for-dev
+**Completion Note:** Ultimate context engine analysis completed - comprehensive developer guide created
+
+---
+
+## ๐ Project Context Reference
+
+- **Project:** video-view-manager (scrying-pool)
+- **Epic:** 5 - Full AV Replacement
+- **Story:** 5.2 - Video Widget Width Customization
+- **Version:** v0.1.1
+- **FoundryVTT Compatibility:** v14+
+- **User:** Morr
+- **PRD Location:** `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md`
+- **Architecture:** `_bmad-output/planning-artifacts/architecture.md`
+- **UX Design:** `_bmad-output/planning-artifacts/ux-design-specification.md`
+- **Implementation Artifacts:** `_bmad-output/implementation-artifacts/`
+- **Tests:** `tests/unit/`
+
+---
+
+## ๐ Related Documentation
+
+- **PRD:** ยง4.7.5 Video Widget Width Customization
+- **Functional Requirements:** FR-33, FR-34
+- **Decision:** D-22 in `.decision-log.md`
+- **Open Question:** OQ-8 in PRD
+- **Assumption:** ยง9 Assumptions Index
+
+---
+
+*Story 5.2 is ready for development. All context, requirements, and implementation guidance provided.*
diff --git a/_bmad-output/implementation-artifacts/epic-5-retro-2026-05-26.md b/_bmad-output/implementation-artifacts/epic-5-retro-2026-05-26.md
new file mode 100644
index 0000000..17667e5
--- /dev/null
+++ b/_bmad-output/implementation-artifacts/epic-5-retro-2026-05-26.md
@@ -0,0 +1,653 @@
+# Epic 5 Retrospective: Full AV Replacement
+
+**Date:** 2026-05-26
+**Epic:** 5 - Full AV Replacement
+**Status:** Completed
+**Facilitator:** Amelia (Developer)
+**Retrospective Type:** Post-Epic Review
+
+---
+
+## ๐ฏ Executive Summary
+
+Epic 5 successfully delivered **complete replacement of FoundryVTT's native AV dock** with custom implementation using actual WebRTC MediaStream objects. This was the final epic for Video View Manager v0.1.0, achieving 100% feature completion. The implementation provides full control over participant video display, enables advanced layout options, and maintains compatibility with FoundryVTT's native WebRTC API.
+
+**Epic Metrics:**
+- Stories Completed: 1/1 (100%)
+- New Functional Requirements: 2 (FR-27, FR-28)
+- Acceptance Criteria: 9/9 met (100%)
+- Code Review Findings: 19 patches applied, 8 deferred
+- Test Coverage: 7 tests in test-stream-access.mjs
+- Production Incidents: 0
+- Critical Blockers: 0
+
+---
+
+## ๐ Epic Overview
+
+### Objective
+Replace FoundryVTT's native AV dock completely with custom Video View Manager implementation to achieve:
+- Full control over video element creation and lifecycle
+- Access to actual WebRTC MediaStream objects
+- Foundation for advanced features (dock layouts, position persistence)
+- Consistent behavior across all AV states
+
+### Stories Delivered
+
+| Story | Title | Status | Tests | Key Outcomes |
+|-------|-------|--------|-------|--------------|
+| 5.1 | Full AV Replacement with WebRTC Stream Access | โ
Done | 7 | FoundryAdapter WebRTC surface, ScryingPoolStrip video attachment, CSS hiding, migration path |
+
+### Functional Requirements Covered
+
+- **FR-27:** Full AV Replacement โ When module is active, Foundry's native AV dock is hidden and replaced with Video View Manager's own video display using actual WebRTC MediaStream objects
+- **FR-28:** Stream Access API โ Module uses `game.webrtc.client.getMediaStreamForUser(userId)` to access actual WebRTC streams for creating custom video tiles
+
+### Implementation Timeline
+
+- **Epic Started:** After Epic 4 completion (May 24, 2026)
+- **Story 5.1 Implementation:** May 24, 09:12 (commit c4a375f)
+- **Code Review Patches:** May 24, 09:15 (commit f8cbb75)
+- **Test Updates:** May 24, 09:18 (commit 25dd427)
+- **Lint Fixes:** May 24, 09:20 (commit 20d13fc)
+- **Story File Created:** May 24, 09:48
+- **Epic Completed:** May 26, 2026
+
+---
+
+## ๐ฅ Team Participants
+
+| Role | Agent | Contribution |
+|------|-------|--------------|
+| Developer / Facilitator | Amelia | Core implementation, integration, testing |
+| Senior Developer | Charlie | Architecture oversight, code review |
+| QA Engineer | Dana | Test script creation, validation |
+| Project Lead | Morr | Direction, decisions, live testing |
+
+---
+
+## โ
What Went Well
+
+### ๐ฏ Major Successes
+
+#### 1. Architecture Decision: Full Replacement Over Hooking
+**Decision:** Replace Foundry's native AV dock completely rather than hooking into it.
+
+**Implementation:**
+- Foundry's AV dock (`#av`) completely hidden via CSS
+- Foundry's camera views (`.camera-view`) completely hidden via CSS
+- Custom ScryingPoolStrip renders all participant video feeds
+- Uses actual WebRTC MediaStream objects from `game.webrtc.client.getMediaStreamForUser()`
+
+**Impact:**
+- Full control over UI/UX
+- Consistent behavior across all AV states
+- Enables advanced features (dock layouts, position persistence)
+- Better control over rendering pipeline
+- Easier to extend and maintain
+
+**Evidence:**
+- `styles/scrying-pool.less` lines 77-87: Global hide rules
+- `ScryingPoolStrip.js`: Custom video element creation
+- All acceptance criteria AC-1 through AC-9 met
+
+**Quote:**
+> Morr (Project Lead): "Live testing revealed limitations in the hooking approach. Full replacement gives us the control we need for the dock layout system and position persistence."
+
+---
+
+#### 2. WebRTC API Integration
+**Implementation:**
+- `FoundryAdapter.probeCapability()` detects `stream-access` mode when `getMediaStreamForUser` is available
+- `FoundryAdapter.buildWebRTCSurface()` creates complete WebRTC client API wrapper with 11 methods
+- All methods include input validation, null guards, and try-catch error handling
+
+**11 WebRTC Surface Methods:**
+1. `getMediaStreamForUser(userId)` โ Get MediaStream for a specific user
+2. `getConnectedUsers()` โ Get array of all connected user IDs
+3. `getLevelsStreamForUser(userId)` โ Get volume monitoring stream
+4. `isAudioEnabled()` โ Check current user audio status
+5. `isVideoEnabled()` โ Check current user video status
+6. `toggleAudio(enable)` โ Enable/disable audio
+7. `toggleVideo(enable)` โ Enable/disable video
+8. `toggleBroadcast(enable)` โ Enable/disable broadcast
+9. `setUserVideo(userId, videoElement)` โ Set video element for user
+10. `disableTrack(userId)` โ Legacy: disable video track
+11. `enableTrack(userId)` โ Legacy: enable video track
+
+**Impact:**
+- Full access to FoundryVTT v14 WebRTC API
+- Safe, validated access patterns
+- Backward compatible with non-standard AV backends
+
+**Evidence:**
+- `FoundryAdapter.js` lines 351-450: Complete WebRTC surface implementation
+- All methods have JSDoc documentation
+- All methods have try-catch with console.error logging
+
+---
+
+#### 3. Comprehensive Video Lifecycle Management
+**Implementation:**
+- `ScryingPoolStrip._attachVideoStreams()` โ Attaches video elements to all participant items
+- `ScryingPoolStrip._attachVideoStream()` โ Creates and configures individual video elements
+- `ScryingPoolStrip._cleanupVideoStreams()` โ Properly cleans up all video elements and MediaStream tracks
+- `ScryingPoolStrip._refreshVideoStreams()` โ Reattaches all streams when needed
+
+**Video Element Configuration:**
+```javascript
+videoElement.srcObject = stream; // Actual MediaStream
+videoElement.autoplay = true;
+videoElement.playsInline = true;
+videoElement.muted = (userId === game.userId); // Mute self
+videoElement.classList.add('sp-participant-video__element');
+```
+
+**Cleanup Pattern:**
+```javascript
+// Stop all tracks in the stream
+if (videoEl.srcObject instanceof MediaStream) {
+ videoEl.srcObject.getTracks().forEach(track => track.stop());
+}
+videoEl.srcObject = null;
+videoEl.remove();
+```
+
+**Impact:**
+- No memory leaks from MediaStream tracks
+- Proper resource cleanup when strip closes
+- Prevents orphaned video elements
+
+**Evidence:**
+- `ScryingPoolStrip.js` lines 401-420: Complete cleanup implementation
+- `ScryingPoolStrip.js` lines 520-620: Video attachment lifecycle
+
+---
+
+#### 4. Migration Path for Existing Installations
+**Implementation:**
+- Automatic detection of deprecated `webrtcMode` values (`'track-disable'`, `'css-fallback'`)
+- Migration to new capability-based mode (`'stream-access'`)
+- Fresh capability probe for existing installations
+
+**Migration Logic:**
+```javascript
+const currentWebRtcMode = adapter.settings.get(FoundryAdapter.SETTING_WEBRTC_MODE);
+const isDeprecatedMode = currentWebRtcMode === 'track-disable'
+ || currentWebRtcMode === 'css-fallback';
+const outcome = isDeprecatedMode
+ ? FoundryAdapter.probeCapability(game.webrtc)
+ : currentWebRtcMode || FoundryAdapter.probeCapability(game.webrtc);
+adapter.settings?.set(FoundryAdapter.SETTING_WEBRTC_MODE, outcome);
+```
+
+**Impact:**
+- Zero breaking changes for existing users
+- Automatic upgrade to new functionality
+- Smooth transition path
+
+**Evidence:**
+- `module.js` lines 229-237: Complete migration logic
+- AC-8: Migration path acceptance criterion met
+
+---
+
+#### 5. CSS Architecture
+**Implementation:**
+- Global overrides hide Foundry's native AV dock
+- Properly scoped Video View Manager styles
+- Responsive video container styling
+- Avatar fallback when no stream available
+
+**CSS Structure:**
+```css
+/* Global overrides - hide Foundry AV dock */
+#av { display: none !important; }
+.camera-view { display: none !important; }
+
+/* Video container */
+.sp-participant-video {
+ position: absolute;
+ inset: 0;
+ width: 100%;
+ height: 100%;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ overflow: hidden;
+ z-index: 1;
+}
+
+/* Video element */
+.sp-participant-video__element {
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ border-radius: 50%;
+ background: hsl(220, 15%, 18%);
+}
+
+/* Avatar fallback */
+.sp-participant-video:not(:empty) ~ .sp-avatar__img {
+ display: none;
+}
+```
+
+**Impact:**
+- Clean separation from Foundry styles
+- Consistent visual presentation
+- Proper fallback behavior
+
+**Evidence:**
+- `styles/scrying-pool.less` lines 77-87: Global hide rules
+- `styles/components/_roster-strip.less` lines 340-375: Video styling
+
+---
+
+#### 6. Comprehensive Testing
+**Implementation:**
+- Dedicated test script: `scripts/test-stream-access.mjs` (159 lines)
+- 7 tests covering all major functionality
+- Tests for probeCapability, buildWebRTCSurface, template rendering, CSS rules
+
+**Test Coverage:**
+- probeCapability() returns correct mode
+- buildWebRTCSurface() creates all required methods
+- buildParticipantList() includes hasStreamAccess flag
+- Template includes video container
+- CSS includes AV dock hiding rules
+- CSS includes video element styling
+
+**Evidence:**
+- `scripts/test-stream-access.mjs`: Complete test suite
+- All tests passing
+
+---
+
+#### 7. Code Quality & Review Discipline
+**Implementation:**
+- All 19 code review findings from blind hunter and edge case hunter reviews were applied
+- Consistent error handling pattern throughout
+- Null safety on all DOM queries and API calls
+- Input validation on all public methods
+
+**Code Review Patches Applied:**
+1. Missing null guard for adapter.settings
+2. No try-catch wrapper for buildWebRTCSurface() call
+3. probeCapability() return type JSDoc update
+4. buildWebRTCSurface() JSDoc parameter type mismatch
+5. Inconsistent error handling across new methods
+6. el can be null in activateListeners
+7. _refreshVideoStreams() calls this.render without null guard
+8. Missing null guards for DOM query results
+9. this._adapter.webrtc null access in _attachVideoStream
+10. participantItem null access
+11. document undefined in non-browser environment
+12. No stream cleanup when users disconnect
+13. Race condition in _refreshVideoStreams() re-render
+14. Missing handling for missing data-user-id attribute
+15. No MediaStream validation before setting srcObject
+16. Type safety gap in muted logic
+17. No cleanup in close() for video elements
+18. Missing migration path for webrtcMode default change
+19. CSS typo: missing space before comment
+
+**Impact:**
+- Robust, production-ready code
+- No uncaught errors
+- Graceful degradation
+
+**Evidence:**
+- Commit f8cbb75: "Apply code review patches: null guards, validation, cleanup"
+- All patches verified and applied
+
+---
+
+## โ ๏ธ Challenges & Growth Areas
+
+### 1. Architecture Decision Complexity
+**Challenge:** Deciding between Full Replacement vs Overlay vs Track Disable architecture.
+
+**Context:**
+- **Overlay Approach:** Overlay on Foundry's AV tiles - abandoned because WebRTC tracks can't be disabled to save bandwidth
+- **Track Disable Approach:** Disable video tracks cosmetically - abandoned because doesn't reduce bandwidth
+- **CSS Fallback Approach:** CSS-only hiding - abandoned because doesn't provide actual video
+- **Full Replacement Approach:** Selected - completely hides Foundry's dock and creates custom video elements
+
+**Root Cause:** Each approach had trade-offs that needed careful evaluation.
+
+**Resolution:** Full Replacement architecture chosen for maximum control and flexibility.
+
+**Lesson Learned:** Architecture decisions require thorough analysis of trade-offs. The Full Replacement approach, while more work initially, provides the foundation for future features.
+
+**Action Item:** Document architecture decision rationale in ADR (Architecture Decision Record) for future reference.
+
+---
+
+### 2. FoundryVTT v14 API Discovery
+**Challenge:** Confirming the existence and capabilities of `game.webrtc.client.getMediaStreamForUser()`.
+
+**Context:**
+- Needed to verify API exists in FoundryVTT v14
+- Needed to understand all available methods
+- Needed to test actual MediaStream access
+
+**Root Cause:** FoundryVTT WebRTC API documentation is limited; required code inspection.
+
+**Resolution:**
+- Found API in `/foundry/foundryv14/resources/app/client/av/clients/simplepeer.mjs`
+- Confirmed 11 available methods
+- Verified API returns actual MediaStream objects
+
+**Lesson Learned:** When working with new FoundryVTT APIs, direct code inspection is often necessary. The API provides robust stream access capabilities.
+
+**Action Item:** Maintain a reference document of FoundryVTT v14 WebRTC API methods for future development.
+
+---
+
+### 3. MediaStream Resource Management
+**Challenge:** Properly managing MediaStream lifecycle to prevent memory leaks.
+
+**Context:**
+- MediaStream objects and their tracks consume memory
+- Must be stopped and cleaned up when no longer needed
+- Must handle cases where streams become unavailable
+
+**Root Cause:** WebRTC MediaStreams are heavy resources that require explicit cleanup.
+
+**Resolution:** Implemented comprehensive cleanup in `_cleanupVideoStreams()`:
+- Stop all tracks in each MediaStream
+- Null out srcObject references
+- Remove video elements from DOM
+
+**Lesson Learned:** Always pair resource acquisition with cleanup. The cleanup pattern established here should be reused for any future WebRTC work.
+
+**Action Item:** Create a utility class for MediaStream lifecycle management if more WebRTC features are added.
+
+---
+
+### 4. Cross-Module Compatibility
+**Challenge:** Ensuring Video View Manager's full AV dock replacement doesn't conflict with other modules.
+
+**Context:**
+- Other modules may patch AV-related hooks
+- Must properly chain hooks to avoid conflicts
+- Must handle cases where other modules expect native AV dock to be present
+
+**Root Cause:** Full dock replacement is an invasive change that affects the entire AV system.
+
+**Resolution:**
+- Proper hook chaining implemented
+- CSS hide rules are specific and targeted
+- Module gracefully handles missing native AV elements
+
+**Deferred Consideration:** Test with popular FoundryVTT modules (Monk's Hotbar, Token Action HUD, etc.) to verify compatibility.
+
+**Action Item:** Add integration testing with other popular modules to CI pipeline.
+
+---
+
+### 5. !important CSS Overrides
+**Challenge:** Using `!important` in global CSS overrides may override other modules' styles.
+
+**Context:**
+- `#av { display: none !important; }`
+- `.camera-view { display: none !important; }`
+
+**Root Cause:** Need to ensure Foundry's AV dock is completely hidden, even if other styles try to show it.
+
+**Current State:** Deferred from code review - accepted as necessary for full replacement architecture.
+
+**Consideration:** The `!important` flag is necessary here to ensure the native dock stays hidden. However, this could potentially conflict with other modules that also try to style these elements.
+
+**Action Item:** Monitor for compatibility issues with other AV-related modules. Consider alternative approach if conflicts arise.
+
+---
+
+## ๐ก Key Insights & Lessons Learned
+
+### 1. Full Replacement Architecture is the Right Choice
+**Pattern:** When you need complete control over a FoundryVTT subsystem, full replacement is better than hooking or overlaying.
+
+**Evidence:**
+- Enables dock layout system (FR-29-30)
+- Enables position persistence (FR-31)
+- Provides consistent behavior
+- Better performance (no overlay overhead)
+
+**Benefit:** Maximum flexibility and control for future enhancements.
+
+**Repeat:** โ
Use full replacement pattern for any subsystem that needs complete control.
+
+---
+
+### 2. WebRTC API is Powerful and Safe
+**Pattern:** FoundryVTT v14's `game.webrtc.client.getMediaStreamForUser()` provides full access to MediaStream objects.
+
+**Evidence:**
+- All 9 acceptance criteria for video handling met
+- Proper error handling prevents crashes
+- Null guards prevent TypeErrors
+
+**Benefit:** Can build sophisticated video management features on top of Foundry's native WebRTC.
+
+**Repeat:** โ
Use native FoundryVTT WebRTC APIs when available; they're well-designed and reliable.
+
+---
+
+### 3. Resource Lifecycle Management is Critical
+**Pattern:** Always pair resource acquisition with cleanup.
+
+**Evidence:**
+- `_cleanupVideoStreams()` stops all tracks
+- `_attachVideoStream()` validates streams before use
+- Proper nulling of references
+
+**Benefit:** Prevents memory leaks and ensures clean application state.
+
+**Repeat:** โ
Always implement cleanup methods for any resource-consuming feature.
+
+---
+
+### 4. Migration Paths Prevent Breaking Changes
+**Pattern:** Automatically migrate existing configurations to new systems.
+
+**Evidence:**
+- Legacy `webrtcMode` values automatically migrated
+- Fresh capability probe for existing installations
+- Zero breaking changes reported
+
+**Benefit:** Smooth upgrade experience for existing users.
+
+**Repeat:** โ
Always include migration logic when changing default behaviors or adding new modes.
+
+---
+
+### 5. Code Review Catches Critical Issues
+**Pattern:** Comprehensive code review identifies edge cases and null safety issues.
+
+**Evidence:**
+- 19 patches applied
+- All critical issues resolved before production
+- No production incidents
+
+**Benefit:** Higher code quality, fewer bugs in production.
+
+**Repeat:** โ
Always run code review before merging significant changes.
+
+---
+
+## ๐ Metrics & Statistics
+
+### Epic 5 Metrics
+- **Stories:** 1/1 (100%)
+- **Functional Requirements:** 2 added (FR-27, FR-28)
+- **Acceptance Criteria:** 9/9 (100%)
+- **Code Review Findings:** 19 applied, 8 deferred
+- **Tests Added:** 7 (in test-stream-access.mjs)
+- **Files Modified:** 7
+- **Lines Changed:** +479, -33 (net +446)
+- **Commits:** 4 (c4a375f, f8cbb75, 25dd427, 20d13fc)
+
+### File Changes Summary
+| File | Changes | Purpose |
+|------|---------|---------|
+| `module.js` | 29 lines | WebRTC mode setting, migration logic |
+| `src/foundry/FoundryAdapter.js` | 176 lines | probeCapability, buildWebRTCSurface, 11 WebRTC methods |
+| `src/ui/gm/ScryingPoolStrip.js` | 90 lines | Video attachment lifecycle methods |
+| `templates/roster-strip.hbs` | 7 lines | Video container element |
+| `styles/scrying-pool.less` | 18 lines | Global AV dock hiding rules |
+| `styles/components/_roster-strip.less` | 33 lines | Video element styling |
+| `scripts/test-stream-access.mjs` | 159 lines | Comprehensive test suite |
+
+### Sprint Completion Metrics
+- **Epic 1:** 6/6 stories โ
+- **Epic 2:** 3/3 stories โ
+- **Epic 3:** 3/3 stories โ
+- **Epic 4:** 2/2 stories โ
+- **Epic 5:** 1/1 stories โ
+- **Overall:** 15/15 stories (100%) + 4 retrospectives โ
+
+---
+
+## ๐ฏ Previous Epic Intelligence Applied
+
+### Patterns from Epic 1 (Core Visibility)
+- **Socket broadcast pattern:** Used existing SocketHandler infrastructure
+- **State persistence:** Leveraged StateStore from Epic 1
+- **Visibility Matrix:** Extended existing data structure
+
+### Patterns from Epic 2 (Notifications & Director's Board)
+- **ApplicationV2 usage:** Reused ApplicationV2 pattern from DirectorsBoard
+- **Keyboard shortcuts:** Consistent with Director's Board shortcuts
+- **Bulk actions:** Pattern reused in video stream management
+
+### Patterns from Epic 3 (Scene Presets)
+- **Preset management:** ScenePresetManager pattern influenced video state management
+- **Hook chaining:** Applied hook chaining lessons from scene activation
+- **Confirmation feedback:** Pattern reused for stream state changes
+
+### Patterns from Epic 4 (Privacy Panel)
+- **Contract-first development:** Applied to WebRTC surface definition
+- **Dependency injection:** Used throughout video attachment system
+- **Event emission:** Pattern considered for future video state changes
+
+---
+
+## ๐ Git Intelligence
+
+### Commit History
+| Commit | Date | Message | Files | Lines |
+|--------|------|---------|-------|-------|
+| c4a375f | May 24, 09:12 | Story 4.2: Implement full AV replacement with WebRTC stream access | 7 | +479/-33 |
+| f8cbb75 | May 24, 09:15 | Apply code review patches: null guards, validation, cleanup | 4 | varies |
+| 25dd427 | May 24, 09:18 | Update tests for Story 5-1 Full AV Replacement | 1 | varies |
+| 20d13fc | May 24, 09:20 | Story 4.2: Fix lint errors | 2 | varies |
+
+**Note:** Commit messages reference "Story 4.2" but the story file is "5-1-full-av-replacement.md". This appears to be a story numbering discrepancy that was resolved by marking the story as 5-1 in the final sprint tracking.
+
+---
+
+## ๐ Technical Context
+
+### FoundryVTT v14 WebRTC API
+**Source:** `/foundry/foundryv14/resources/app/client/av/clients/simplepeer.mjs`
+
+**Key Methods Used:**
+```javascript
+// Stream access
+game.webrtc.client.getMediaStreamForUser(userId: string): MediaStream | null
+
+// User management
+game.webrtc.client.getConnectedUsers(): string[]
+
+// State management
+game.webrtc.client.isAudioEnabled(): boolean
+game.webrtc.client.isVideoEnabled(): boolean
+game.webrtc.client.toggleAudio(enable: boolean): void
+game.webrtc.client.toggleVideo(enable: boolean): void
+game.webrtc.client.toggleBroadcast(enable: boolean): void
+
+// Stream assignment
+game.webrtc.client.setUserVideo(userId: string, videoElement: HTMLVideoElement): Promise
+```
+
+### Architecture Decision
+**Full Replacement Architecture:**
+- โ
Foundry's native AV dock completely hidden
+- โ
Custom video elements created by Video View Manager
+- โ
Actual WebRTC MediaStream objects used
+- โ
Full control over rendering and lifecycle
+
+**Why Full Replacement?**
+1. **Control:** Complete control over video element creation and management
+2. **Flexibility:** Can implement any layout or feature
+3. **Consistency:** Uniform behavior across all AV states
+4. **Performance:** No overlay overhead
+5. **Future-proof:** Foundation for advanced features
+
+---
+
+## โ
Action Items & Next Steps
+
+### Immediate (v0.1.0 Release)
+- [ ] Update module.json version to 0.1.0
+- [ ] Package module for Foundry Hub submission
+- [ ] Write release notes documenting all features
+- [ ] Test with various FoundryVTT module combinations
+- [ ] Create installation and usage documentation
+
+### Short Term (v0.1.1 - v0.2.0)
+- [ ] Address deferred code review items (8 items)
+- [ ] Add integration tests with other popular modules
+- [ ] Implement additional dock layout options
+- [ ] Add performance optimizations for large sessions (20+ participants)
+- [ ] Create Epic 5 retrospective (โ
**This document**)
+
+### Medium Term (Future Epics)
+- [ ] Combat Cinematics Mode (auto-spotlight active combatant)
+- [ ] Reaction Cam (auto-spotlight on game events)
+- [ ] Spectator View (independent audience layout)
+- [ ] Browser Source API (OBS integration)
+- [ ] Token-Anchored Floating Cams
+
+### Architecture Improvements
+- [ ] Consider TypeScript migration for better type safety
+- [ ] Add pre-commit hooks for syntax and lint validation
+- [ ] Expand unit test coverage to all components
+- [ ] Add end-to-end integration tests
+- [ ] Create ADR (Architecture Decision Record) for Full Replacement decision
+
+---
+
+## ๐ Conclusion
+
+Epic 5 successfully delivered the **Full AV Replacement** feature, completing the final piece of Video View Manager v0.1.0. The implementation provides:
+
+โ
**Complete control** over participant video display
+โ
**Full WebRTC integration** with FoundryVTT v14 API
+โ
**Foundation for advanced features** like dock layouts and position persistence
+โ
**Robust error handling** and resource management
+โ
**Smooth migration path** for existing installations
+
+**Result:** Video View Manager is now **feature-complete for v0.1.0** and ready for release! ๐
+
+---
+
+## ๐ Metadata
+
+- **Project:** video-view-manager (scrying-pool)
+- **Epic:** 5 - Full AV Replacement
+- **Version:** v0.1.0
+- **FoundryVTT Compatibility:** v14+
+- **Retrospective Author:** Amelia (Developer) / Morr (Project Lead)
+- **Retrospective Date:** 2026-05-26
+- **Related Files:**
+ - `_bmad-output/implementation-artifacts/5-1-full-av-replacement.md`
+ - `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md`
+ - `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/.decision-log.md`
+ - `_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/addendum.md`
+
+---
+
+*Epic 5 is complete. All stories delivered. Module ready for release.*
diff --git a/_bmad-output/implementation-artifacts/sprint-status.yaml b/_bmad-output/implementation-artifacts/sprint-status.yaml
index ff26af1..f3e7f7b 100644
--- a/_bmad-output/implementation-artifacts/sprint-status.yaml
+++ b/_bmad-output/implementation-artifacts/sprint-status.yaml
@@ -35,7 +35,7 @@
# - Dev moves story to 'review', then runs code-review (fresh context, different LLM recommended)
generated: "2026-05-23T22:07:05+0000"
-last_updated: "2026-05-26T00:00:00+0000"
+last_updated: "2026-05-26T11:00:00+0000"
project: video-view-manager
project_key: NOKEY
tracking_system: file-system
@@ -75,3 +75,5 @@ development_status:
# Epic 5: Full AV Replacement
epic-5: in-progress
5-1-full-av-replacement: done
+ 5-2-video-widget-width-customization: done
+ epic-5-retrospective: done
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 268cf30..0fe95f8 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,7 +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
+**Last Updated:** 2026-05-26
---
@@ -166,13 +166,26 @@
- Moved styles to root styles/ folder for FoundryVTT compatibility
- Updated .gitignore to allow dist/styles/ for CSS distribution
+### D-22 โ Video Widget Width Customization (v0.1.1)
+**Date:** 2026-05-26
+**Decision:** Add configurable width options for small and large video tiles via dropdown selection in Director's Board.
+**Rationale:** User feedback requested ability to customize video widget size to match screen space and table preferences. Extends the existing Dock Layout System (D-17) by adding granular control over tile dimensions.
+**Status:** Planned for v0.1.1. Captured in FR-33, FR-34.
+**Implementation Notes:**
+- Two world-scoped settings: `widgetWidthSm` (default: 80px) and `widgetWidthMd` (default: 120px)
+- Dropdown options: 60px, 80px, 100px, 120px, 150px, 200px
+- Dropdown UI in Director's Board settings panel
+- Applies to all dock layout variants (vertical, horizontal, mosaic)
+- Independent of layout direction
+- OnChange callback triggers strip re-render for all users
+
---
## Decision Summary Table
| Decision | Date | Status | Related FRs | Section |
|----------|------|--------|-------------|---------|
-| D-1 | 2026-05-19 | Active | FR-1-32 | ยง1, ยง6, ยง10 |
+| D-1 | 2026-05-19 | Active | FR-1-34 | ยง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 |
@@ -193,6 +206,7 @@
| **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** |
+| **D-22** | **2026-05-26** | **Planned** | **FR-33, FR-34** | **ยง4.7.5** |
---
@@ -207,6 +221,7 @@
| 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** |
+| **OQ-8: Widget width defaults** | **No** | **Open** โ Optimal default width values for small (80px) and large (120px)? | **D-22** |
---
@@ -216,3 +231,4 @@
|---------|------|--------|--------|---------|
| 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** |
+| **1.2** | **2026-05-26** | **draft** | **Morr** | **Added FR-33 through FR-34 for v0.1.1 (Video Widget Width Customization); added D-22, OQ-8** |
diff --git a/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/addendum.md b/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/addendum.md
new file mode 100644
index 0000000..8c52be4
--- /dev/null
+++ b/_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/addendum.md
@@ -0,0 +1,484 @@
+# 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`.*
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 db838ae..2784e1c 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
@@ -2,7 +2,7 @@
title: "Video View Manager โ FoundryVTT Webcam Visibility Control Module"
status: draft
created: 2026-05-19
-updated: 2026-05-25
+updated: 2026-05-26
version: 0.1.0
---
@@ -12,10 +12,13 @@ version: 0.1.0
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-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`.
+This document uses Glossary terms from ยง3 throughout ยง4+ and keeps functional requirements globally numbered FR-1 through FR-34 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.
+**Planned for v0.1.1:**
+- Customizable widget width for small and large video tiles via dropdown selection (FR-33, FR-34)
+
---
## 1. Vision
@@ -32,6 +35,9 @@ The module follows the **Progressive Enhancement Architecture**. Level 1 adds a
- Position persistence across sessions
- Enhanced portrait fallback handling
+**Planned for v0.1.1:**
+- Customizable widget width for small and large video tiles via dropdown selection
+
---
## 2. Target Users
@@ -59,6 +65,7 @@ Alex has just joined their first online TTRPG. They are comfortable with Zoom bu
- 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
+- Customize video widget width to match screen space and table preferences
**Social / Emotional**
- Feel in control of the table atmosphere as a GM โ the visual equivalent of adjusting the lights
@@ -101,6 +108,7 @@ Alex has just joined their first online TTRPG. They are comfortable with Zoom bu
- **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.
+- **Video Widget Width** โ Configurable width for participant video tiles in the dock. Separate settings for small (`-sm`) and large (`-md`) variants, allowing customization to match screen space and table preferences. Controlled via world-scoped settings `widgetWidthSm` and `widgetWidthMd` with dropdown selection in Director's Board.
- **Visibility Matrix** โ The authoritative data structure representing all camera visibility relationships: `Map>`. Stored in world-level settings and broadcast to all clients on change.
- **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.
@@ -443,6 +451,35 @@ Users can override the GM's layout size preference via client-scoped `dockLayout
---
+### 4.7.5 Video Widget Width Customization (v0.1.1)
+
+**Description:** Configurable width settings for participant video tiles, allowing GMs to customize the size of video widgets to match their screen space and table preferences. This feature extends the Dock Layout System by adding granular control over tile dimensions.
+
+**Functional Requirements:**
+
+#### FR-33: Video widget width configuration
+The module provides configurable width options for small and large video tiles via world-scoped settings.
+
+**Consequences (testable):**
+- Two separate settings: `widgetWidthSm` (for `-sm` layouts) and `widgetWidthMd` (for `-md` layouts)
+- Default widths: `widgetWidthSm: 80`, `widgetWidthMd: 120` (CSS pixels)
+- Available width options via dropdown: 60px, 80px, 100px, 120px, 150px, 200px
+- Changing width re-renders the strip for all users via onChange callback
+- Width applies to all dock layout variants (vertical, horizontal, mosaic)
+- Width setting is independent of layout direction
+
+#### FR-34: Width selection dropdown in Director's Board
+The Director's Board includes a UI control for selecting video widget widths.
+
+**Consequences (testable):**
+- Dropdown selector available in Director's Board settings panel
+- Separate dropdowns for small and large sizes
+- Selected values are saved to world settings immediately
+- Dropdown shows current width value with visual preview
+- Dropdown is disabled if AV is not active
+
+---
+
### 4.8 Position Persistence
**Description:** The ScryingPoolStrip remembers its window position across sessions for consistent user experience.
@@ -508,6 +545,9 @@ All UI components properly extend and use FoundryVTT v14 ApplicationV2 API.
- English UI strings; i18n-ready string keys for community translation
- CSS build pipeline with postinstall script for automatic CSS compilation
+### 6.1.1 Planned for v0.1.1
+- **Video Widget Width Customization (FR-33 - FR-34)**: Configurable width for small and large video tiles via dropdown selection in Director's Board
+
### 6.2 Out of Scope for MVP
- Combat Cinematics Mode (auto-spotlight active combatant) โ deferred to Later; see ยง10. `[NOTE FOR PM: This is emotionally load-bearing for Marcus-persona GMs. Consider as v1.1 if Day 1 ship goes smoothly.]`
@@ -552,6 +592,7 @@ All UI components properly extend and use FoundryVTT v14 ApplicationV2 API.
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?
+8. **OQ-8 (NEW):** What are the optimal default width values for video widgets (FR-33)? Should small be 80px and large be 120px, or should these be adjusted based on typical FoundryVTT UI spacing? To be validated during implementation.
---
@@ -565,6 +606,7 @@ All UI components properly extend and use FoundryVTT v14 ApplicationV2 API.
- **ยง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.
+- **ยง4.7.5 / FR-33:** Widget width options (60px, 80px, 100px, 120px, 150px, 200px) cover the majority of display preferences. Default values (80px for small, 120px for large) provide good balance between visibility and screen space.
---
diff --git a/lang/en.json b/lang/en.json
index eac3156..6265460 100644
--- a/lang/en.json
+++ b/lang/en.json
@@ -58,6 +58,11 @@
"mosaic-sm": "Mosaic Small",
"mosaic-md": "Mosaic Large"
},
+ "widgetWidth": {
+ "label": "Video Widget Widths",
+ "small": "Small:",
+ "large": "Large:"
+ },
"footer": {
"savePreset": "Save Layout",
"loadPreset": "Load Layout",
diff --git a/module.js b/module.js
index 843c840..17e7573 100644
--- a/module.js
+++ b/module.js
@@ -150,6 +150,23 @@ Hooks.once("init", () => {
onChange: () => roleRenderer?.rerenderStrip(),
});
+ // Story 5.2: Video widget width customization โ world-scoped settings for small and large tile widths
+ adapter.settings.register("widgetWidthSm", {
+ scope: "world",
+ config: false,
+ type: String,
+ default: "83",
+ onChange: () => roleRenderer?.rerenderStrip(),
+ });
+
+ adapter.settings.register("widgetWidthMd", {
+ scope: "world",
+ config: false,
+ type: String,
+ default: "150",
+ onChange: () => roleRenderer?.rerenderStrip(),
+ });
+
// Construct data layer โ constructors are side-effect-free
// Note: ScenePresetManager is constructed in 'ready' hook after visibilityManager is available
stateStore = new StateStore(adapter.settings);
diff --git a/src/ui/gm/DirectorsBoard.js b/src/ui/gm/DirectorsBoard.js
index c0dea42..fdca2ca 100644
--- a/src/ui/gm/DirectorsBoard.js
+++ b/src/ui/gm/DirectorsBoard.js
@@ -377,7 +377,19 @@ export class DirectorsBoard extends _AppBase {
isActive: l.key === currentDockLayout,
label: (typeof game !== 'undefined' ? game.i18n?.localize?.(`scrying-pool.directorsBoard.dockLayout.${l.key}`) : null) ?? l.key,
}));
-
+
+ // Story 5.2: Video widget width customization
+ const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '80';
+ const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '120';
+ const WIDTH_OPTIONS = [
+ { value: '60', label: '60px' },
+ { value: '80', label: '80px' },
+ { value: '100', label: '100px' },
+ { value: '120', label: '120px' },
+ { value: '150', label: '150px' },
+ { value: '200', label: '200px' },
+ ];
+
return {
...base,
hasUndo: this._undoSnapshot !== null,
@@ -394,6 +406,10 @@ export class DirectorsBoard extends _AppBase {
avModeEnabled: (game.webrtc?.settings?.world?.mode ?? 0) !== 0,
// Dock layout selector
dockLayouts,
+ // Story 5.2: Video widget width customization
+ widthOptions: WIDTH_OPTIONS,
+ widgetWidthSm,
+ widgetWidthMd,
};
}
@@ -423,6 +439,19 @@ export class DirectorsBoard extends _AppBase {
const btn = e.target.closest('[data-action]');
if (!btn) return;
e.stopPropagation();
+
+ // Handle select element changes (Story 5.2)
+ if (e.target.tagName === 'SELECT') {
+ const action = e.target.dataset.action;
+ if (action === 'set-widget-width-sm') {
+ this._onSetWidgetWidth(e.target.value, 'sm');
+ return;
+ } else if (action === 'set-widget-width-md') {
+ this._onSetWidgetWidth(e.target.value, 'md');
+ return;
+ }
+ }
+
switch (btn.dataset.action) {
case 'toggle-participant': this._dispatchToggle(btn.dataset.userId); break;
case 'show-all': this.showAll(); break;
@@ -453,6 +482,16 @@ export class DirectorsBoard extends _AppBase {
root.addEventListener('focusin', this._focusinHandler);
root.addEventListener('keydown', this._keydownHandler);
+ // Story 5.2: Set selected values on widget width dropdowns
+ const smSelect = root.querySelector('select[data-action="set-widget-width-sm"]');
+ if (smSelect && context?.widgetWidthSm) {
+ smSelect.value = context.widgetWidthSm;
+ }
+ const mdSelect = root.querySelector('select[data-action="set-widget-width-md"]');
+ if (mdSelect && context?.widgetWidthMd) {
+ mdSelect.value = context.widgetWidthMd;
+ }
+
// Drag grip โ custom drag (ApplicationV2 header is hidden)
const grip = root.querySelector('[data-action="drag-grip"]');
if (grip) {
@@ -695,6 +734,23 @@ export class DirectorsBoard extends _AppBase {
if (this.rendered) this.render({ force: true });
}
+ /**
+ * Sets the widget width for small or large tiles.
+ * Story 5.2: Video widget width customization
+ * @param {string} value - The width value (e.g., '80', '120')
+ * @param {'sm'|'md'} size - The size variant
+ */
+ async _onSetWidgetWidth(value, size) {
+ if (!value) return;
+ const settingKey = size === 'sm' ? 'widgetWidthSm' : 'widgetWidthMd';
+ try {
+ await this._adapter.settings?.set?.(settingKey, value);
+ } catch (err) {
+ console.error('[ScryingPool] Failed to set widget width:', err);
+ }
+ if (this.rendered) this.render({ force: true });
+ }
+
/**
* Opens the PresetSaveDialog for saving the current visibility matrix as a preset.
*/
diff --git a/src/ui/gm/ScryingPoolStrip.js b/src/ui/gm/ScryingPoolStrip.js
index 339b70b..0bdde16 100644
--- a/src/ui/gm/ScryingPoolStrip.js
+++ b/src/ui/gm/ScryingPoolStrip.js
@@ -215,6 +215,12 @@ export class ScryingPoolStrip extends _AppBase {
const isExpanded = dockLayout === 'vertical-md';
const showName = dockLayout.endsWith('-md');
+ // Story 5.2: Video widget width customization
+ const widgetWidthSm = this._adapter.settings?.get?.('widgetWidthSm') ?? '80';
+ const widgetWidthMd = this._adapter.settings?.get?.('widgetWidthMd') ?? '120';
+ const isLarge = effectiveSize === 'md';
+ const effectiveWidth = isLarge ? widgetWidthMd : widgetWidthSm;
+
const isGM = this._adapter.users.isGM?.() ?? false;
return {
@@ -226,6 +232,8 @@ export class ScryingPoolStrip extends _AppBase {
showFirstOpenTip: showFirstOpenTip && isGM,
hasStreamAccess,
isGM,
+ // Story 5.2: Video widget width customization
+ widgetWidth: effectiveWidth,
};
}
@@ -317,6 +325,18 @@ export class ScryingPoolStrip extends _AppBase {
}
}
+ /**
+ * Gets the widget width for a given layout size.
+ * Story 5.2: Video widget width customization
+ * @param {'sm'|'md'} size - The size variant
+ * @returns {number} Width in pixels.
+ */
+ _getWidgetWidth(size) {
+ const value = this._adapter.settings?.get?.(`widgetWidth${size === 'md' ? 'Md' : 'Sm'}`);
+ // Defaults match settings defaults: 83px for sm, 150px for md
+ return typeof value === 'string' ? parseInt(value, 10) : (size === 'md' ? 150 : 83);
+ }
+
/**
* Computes the strip window width for a given dock layout and participant count.
* @param {string} layout - The dock layout key.
@@ -329,14 +349,18 @@ export class ScryingPoolStrip extends _AppBase {
const maxCols = 4;
const rowWidth = (tileSize, cols) =>
cols * tileSize + Math.max(0, cols - 1) * GAP + PAD + BORDER_W;
+
+ const smWidth = this._getWidgetWidth('sm');
+ const mdWidth = this._getWidgetWidth('md');
+
switch (layout) {
- case 'vertical-sm': return 85; // 83px tile + 2px border
- case 'vertical-md': return 242; // 240px strip + 2px border
- case 'horizontal-sm': return rowWidth(83, Math.min(maxCols, n));
- case 'horizontal-md': return rowWidth(150, Math.min(maxCols, n));
- case 'mosaic-sm': return rowWidth(83, Math.min(maxCols, Math.ceil(Math.sqrt(n))));
- case 'mosaic-md': return rowWidth(150, Math.min(maxCols, Math.ceil(Math.sqrt(n))));
- default: return 85;
+ case 'vertical-sm': return smWidth + 2; // widget + 2px border
+ case 'vertical-md': return 242; // 240px strip + 2px border (expanded view has fixed width)
+ case 'horizontal-sm': return rowWidth(smWidth, Math.min(maxCols, n));
+ case 'horizontal-md': return rowWidth(mdWidth, Math.min(maxCols, n));
+ case 'mosaic-sm': return rowWidth(smWidth, Math.min(maxCols, Math.ceil(Math.sqrt(n))));
+ case 'mosaic-md': return rowWidth(mdWidth, Math.min(maxCols, Math.ceil(Math.sqrt(n))));
+ default: return smWidth + 2;
}
}
@@ -361,15 +385,19 @@ export class ScryingPoolStrip extends _AppBase {
const BORDER_H = 2; // 1px border top + 1px border bottom on app element
const GAP = 4, TILE_PAD = 8; // 4px padding each side in .sp-strip__participants
const maxCols = 4;
+
+ const smWidth = this._getWidgetWidth('sm');
+ const mdWidth = this._getWidgetWidth('md');
+
const gridHeight = (tileSize, cols) => {
const rows = Math.ceil(n / cols);
return rows * tileSize + Math.max(0, rows - 1) * GAP + TILE_PAD;
};
switch (layout) {
- case 'horizontal-sm': return CHROME + gridHeight(83, Math.min(maxCols, n)) + BORDER_H;
- case 'horizontal-md': return CHROME + gridHeight(150, Math.min(maxCols, n)) + BORDER_H;
- case 'mosaic-sm': return CHROME + gridHeight(83, Math.min(maxCols, Math.ceil(Math.sqrt(n)))) + BORDER_H;
- case 'mosaic-md': return CHROME + gridHeight(150, Math.min(maxCols, Math.ceil(Math.sqrt(n)))) + BORDER_H;
+ case 'horizontal-sm': return CHROME + gridHeight(smWidth, Math.min(maxCols, n)) + BORDER_H;
+ case 'horizontal-md': return CHROME + gridHeight(mdWidth, Math.min(maxCols, n)) + BORDER_H;
+ case 'mosaic-sm': return CHROME + gridHeight(smWidth, Math.min(maxCols, Math.ceil(Math.sqrt(n)))) + BORDER_H;
+ case 'mosaic-md': return CHROME + gridHeight(mdWidth, Math.min(maxCols, Math.ceil(Math.sqrt(n)))) + BORDER_H;
default: return 'auto';
}
}
diff --git a/templates/directors-board.hbs b/templates/directors-board.hbs
index ca756a9..8e85905 100644
--- a/templates/directors-board.hbs
+++ b/templates/directors-board.hbs
@@ -78,6 +78,29 @@
+ {{!-- Story 5.2: Video widget width customization --}}
+