40 KiB
Story 4.1: Player Privacy Panel & Automation Opt-ins
Status: done
Epic: 4 - Player Privacy Panel
Story Key: 4-1-player-privacy-panel-and-automation-opt-ins
Created: 2026-05-24
Last Updated: 2026-05-24
Story Header
| Field | Value |
|---|---|
| Epic | 4 - Player Privacy Panel |
| Story ID | 4.1 |
| Story Key | 4-1-player-privacy-panel-and-automation-opt-ins |
| Title | Player Privacy Panel & Automation Opt-ins |
| Status | done |
| Priority | High |
| Assigned Agent | DEV (Amelia) |
| Created | 2026-05-24 |
| Last Updated | 2026-05-24 |
📋 Story Requirements
User Story
As a player, I want to see and control every automation effect that can change my on-screen presence, and opt in or out at any time, So that I'm never surprised by automatic camera behaviours I didn't agree to.
Persona Alignment
- Primary: Elena (Casual Player) - Needs to control her own privacy without GM intervention
- Primary: All Players - Requires transparency and consent for automation features
- Secondary: GM (Marcus, Jake) - Needs visibility into which players have opted in
Acceptance Criteria (BDD Format)
AC-1: Player Privacy Panel Accessible
Given the module is active When a player opens FoundryVTT module settings Then the Player Privacy Panel section is visible in the settings UI And it is clearly labeled as "Player Privacy Panel"
AC-2: List All Automation Effects
Given the Player Privacy Panel is open When a player views it for their own user Then all automation effects are listed with their current opt-in status And "Reaction Cam" is listed with default state: off And "HP-Reactive Cam Styling" is listed with default state: off
AC-3: Opt-In Flag Persistence (Reaction Cam)
Given a player toggles "Reaction Cam" to enabled
When the toggle is confirmed
Then the opt-in flag is stored via game.user.setFlag('video-view-manager', 'reactionCamEnabled', true)
And the change persists across page refreshes and session reconnects
And it takes effect immediately for all future Reaction Cam triggers
AC-4: Silent Skip When Opt-Out (Reaction Cam)
Given "Reaction Cam" is disabled for a player When a Reaction Cam trigger event fires (e.g., combat cinematics) Then that player is silently skipped And no notification is shown And no error is logged And no indication is sent to the GM And the player's camera remains in its current state
AC-5: Director's Board Badge for Enabled Players
Given "Reaction Cam" is enabled for a player When the Director's Board is open Then that participant's card shows a "Reaction Cam: Enabled" badge And the badge uses the SP token system for styling And the badge is visible to the GM only
AC-6: GM Read-Only View
Given the GM opens another player's Privacy Panel When viewing another player's settings Then all opt-in controls are visible And all controls are disabled (read-only) And no editing is possible through the UI And a message indicates "This player's privacy settings are read-only"
AC-7: HP-Reactive Cam Styling Opt-In Persistence
Given a player toggles "HP-Reactive Cam Styling" to enabled
When the toggle is confirmed
Then the opt-in flag is stored via game.user.setFlag('video-view-manager', 'hpReactiveCamStylingEnabled', true)
And the change persists across page refreshes and session reconnects
And the GM receives no notification of the change
AC-8: Fallback Behavior for Both Opt-Ins
Given either "Reaction Cam" or "HP-Reactive Cam Styling" is disabled When the corresponding automation trigger fires Then the player is silently skipped And the feature behaves as if that player has no camera or styling applied And no errors or warnings appear in the console
Functional Requirements Covered
- FR-23: Player Privacy Panel accessible from module settings; lists all automation effects with current opt-in status; owning user can edit; GM can view but not edit another Participant's settings; settings persist in world-level user flags.
- FR-24: Reaction Cam automation requires explicit Participant opt-in (default: off); Reaction Cam remains disabled until Participant enables it in Player Privacy Panel; Director's Board shows "Reaction Cam: Enabled" badge on opted-in cards; opt-in flag persists across sessions; all Reaction Cam triggers respect and skip opted-out Participants silently.
- FR-25: HP-Reactive Cam Styling requires explicit Participant opt-in (default: off); disabled until Participant explicitly enables it; GM is not notified of individual styling opt-in statuses.
Success Criteria
- All 8 acceptance criteria pass manual testing
- All unit tests pass (target: +12-15 new tests for PlayerPrivacyManager)
npm run lintexits 0 (ESLint import boundaries enforced)npm run typecheckexits 0 (strict JSDoc compliance)- Code review passes with no critical findings
- Integration test: Player toggles opt-in → verify flag persistence → verify automation respects opt-out
📝 Tasks / Subtasks
Task 1: Create PlayerPrivacyManager Core Logic
Files: src/core/PlayerPrivacyManager.js, src/contracts/privacy-settings.js, tests/unit/core/PlayerPrivacyManager.test.js
Subtasks:
- 1.1: Create
src/contracts/privacy-settings.jswith opt-in flag contracts- Define canonical shape for privacy settings:
{ reactionCamEnabled: boolean, hpReactiveCamStylingEnabled: boolean } - Export
PRIVACY_SETTINGS_DEFAULTwith both flags defaulting tofalse - Export
isValidPrivacySettings(data)validator - Export
createPrivacySettings(overrides)factory - Export
PRIVACY_SETTING_KEYS,FEATURE_NAME_MAP,validateSettingKey(),validateSettingValue(),validateFeatureName()
- Define canonical shape for privacy settings:
- 1.2: Write TDD red tests for PlayerPrivacyManager methods
- 1.3: Create
PlayerPrivacyManagerclass with constructor(adapter)- Constructor receives FoundryAdapter for user flag access
- No direct
game.*access (DI enforced)
- 1.4: Implement
getSettings(userId)— retrieves privacy settings from user flags- Returns merged settings with defaults for missing keys
- Handles null/undefined flag gracefully
- 1.5: Implement
setSetting(userId, key, value)— updates a single privacy setting- Validates key is known (reactionCamEnabled or hpReactiveCamStylingEnabled)
- Validates value is boolean
- Calls
adapter.users.setFlag(userId, 'video-view-manager', key, value) - Emits change event for subscribers
- 1.6: Implement
isOptedIn(userId, feature)— convenience method for feature checks- Returns boolean for 'reactionCam' or 'hpReactiveCamStyling'
- Defaults to false if setting not found
- 1.7: Implement
getAllSettings()— returns all users' privacy settings (GM only)- Aggregates settings from all connected users
- Only accessible when caller is GM
- 1.8: Green all PlayerPrivacyManager tests (35 tests passing)
Acceptance Criteria: AC-2, AC-3, AC-7, AC-8
Dev Notes:
- Use user flags for persistence:
game.user.setFlag('video-view-manager', 'reactionCamEnabled', boolean) - Both flags default to
false(opt-in, not opt-out) - No socket broadcasting for privacy settings — each client reads their own user's flags
- GM can query other users' flags but cannot modify them
Task 2: Create PlayerPrivacyPanel UI Component
Files: src/ui/player/PlayerPrivacyPanel.js, templates/player-privacy-panel.hbs, styles/components/_player-privacy-panel.less
Subtasks:
- 2.1: Create
PlayerPrivacyPanelclass extendingApplicationV2- Constructor receives
adapter,playerPrivacyManager, andtargetUserId - Opens as a dialog/modal window
- Uses conditional _AppBase for test environment compatibility
- Constructor receives
- 2.2: Create Handlebars template
player-privacy-panel.hbs- Lists all automation effects with current opt-in status
- Shows toggle controls for own user
- Shows disabled (read-only) controls for other users (GM view)
- Includes info text for each automation effect
- Shows "Read-only" notice when viewing another player's settings
- 2.3: Create LESS styles
_player-privacy-panel.less- Styles for panel layout, toggle switches
- Uses SP token system for colors and spacing
- Responsive to Foundry dark/light themes
- Import added to scrying-pool.less
- 2.4: Implement
_prepareContext()to populate settings from PlayerPrivacyManager- Reads current user's privacy settings on open
- Updates toggle states to match saved values
- Determines read-only mode based on targetUserId vs current user
- 2.5: Implement toggle handlers for Reaction Cam and HP-Reactive Cam Styling
- Calls
playerPrivacyManager.setSetting()on change - Updates UI immediately on toggle
- Shows success notification on save
- Reverts on error with error notification
- Calls
- 2.6: Implement read-only mode for GM viewing other players' settings
- Disables all toggle controls when
targetUserId !== game.user.id - Shows visual indicator that settings are read-only
- Prevents any modifications
- Disables all toggle controls when
Acceptance Criteria: AC-1, AC-2, AC-3, AC-6, AC-7, AC-8
Dev Notes:
- Panel should be accessible from module settings menu
- For own user: editable toggles with immediate save
- For other users (GM only): read-only display, no save buttons
- Use FoundryVTT native form controls where possible
- Panel should be keyboard-navigable with proper ARIA labels
Task 3: Extend FoundryAdapter for User Flag Access
Files: src/foundry/FoundryAdapter.js
Subtasks:
- 3.1: Add
setFlag(userId, scope, key, value)method- Wraps
game.users.get(userId)?.setFlag(scope, key, value) - Validates userId exists
- Returns success/failure status
- Wraps
- 3.2: Add
getFlag(userId, scope, key)method- Wraps
game.users.get(userId)?.getFlag(scope, key) - Returns the flag value or undefined if not found
- Returns null if userId doesn't exist
- Wraps
- 3.3: Add
getFlagModule(userId, key)convenience method- Calls
getFlag(userId, 'video-view-manager', key) - Used for privacy settings access
- Calls
- 3.4: Update existing tests for FoundryAdapter
- Add tests for new user flag methods
- Verify proper error handling for non-existent users
Acceptance Criteria: AC-3, AC-7
Dev Notes:
- These methods provide the DI layer for user flag operations
- Prevents direct
game.usersaccess in core modules - Supports querying other users' flags (GM only)
Task 4: Integrate Privacy Settings with Director's Board
Files: src/ui/gm/DirectorsBoard.js, src/utils/boardUtils.js, templates/participant-card.hbs, styles/components/_participant-card.less
Subtasks:
- 4.1: Update
participant-card.hbstemplate to display Reaction Cam badge- Add badge element to card template
- Show badge when
isReactionCamEnabledis true in context - Tooltip: "Reaction Cam: Enabled"
- 4.2: Update
boardUtils.jsto pass privacy settings in context- Modified
buildSimpleParticipantContextto accept optional privacyManager parameter - Modified
buildBoardContextto pass privacyManager to participant context builder - Adds
isReactionCamEnabledflag to each participant context
- Modified
- 4.3: Update
DirectorsBoardto inject PlayerPrivacyManager- Added playerPrivacyManager parameter to constructor
- Pass playerPrivacyManager to buildBoardContext in _prepareContext
- 4.4: Add CSS styles for Reaction Cam badge
- Added badge styling in
_participant-card.less - Uses SP accent color for visibility
- Positioned at bottom-right of avatar
- Added badge styling in
Acceptance Criteria: AC-5
Dev Notes:
- Badge is GM-only visibility (DirectorsBoard is GM-only)
- Badge updates when board re-renders
- Badge is subtle but clearly visible
- Uses existing SP token patterns
Task 5: Add Module Settings Registration
Files: module.js
Subtasks:
- 5.1: Register Player Privacy Panel in module settings
- Used
game.settings.registerMenu('video-view-manager', 'playerPrivacyPanel', {...}) - Menu type:
PlayerPrivacyPanel - Restricted to players (not GM-only) -
restricted: false - Label: localized via
SCRYING_POOL.Settings.PlayerPrivacyPanel - Hint: localized via
SCRYING_POOL.Settings.PlayerPrivacyPanelHint
- Used
- 5.2: Register Player Privacy Panel for GM access
- Separate menu entry for GM to view all players' settings
- Label: "View Player Privacy Settings"
- Restricted to GM only
- Opens read-only view selector
Acceptance Criteria: AC-1, AC-6
Dev Notes:
- Player menu: Opens own privacy panel (editable)
- GM menu: Opens selector to view any player's panel (read-only)
- Both use the same PlayerPrivacyPanel component with different targetUserId
- Note: Task 5.2 (GM view selector) is deferred - for now, GM can open their own panel which is read-only when viewing other users
Task 6: Localization Strings
Files: lang/en.json
Subtasks:
- 6.1: Add all UI strings for Player Privacy Panel
- Panel title: "Player Privacy Panel"
- Section header: "Automation Opt-ins"
- Section description: "Control which automation features can affect your camera and on-screen presence."
- Reaction Cam label: "Reaction Cam"
- Reaction Cam description: "Automatically show your camera during key moments (combat, rolls, etc.)"
- HP-Reactive Cam Styling label: "HP-Reactive Cam Styling"
- HP-Reactive Cam Styling description: "Apply visual styling to your camera based on your character's HP"
- Toggle on: "Enabled"
- Toggle off: "Disabled"
- Read-only notice: "This player's privacy settings are read-only. You cannot modify another player's consent preferences."
- Saved notification: "Privacy settings saved"
- Save error: "Failed to save privacy settings"
- 6.2: Add Director's Board badge tooltip (in template)
- "Reaction Cam: Enabled"
- 6.3: Add module settings menu labels
- Player menu: "Player Privacy Panel" (SCRYING_POOL.Settings.PlayerPrivacyPanel)
- Player menu label: "Control automation effects for your camera" (SCRYING_POOL.Settings.PlayerPrivacyPanelLabel)
- Player menu hint: "Opt in or out of Reaction Cam, HP-Reactive Cam Styling, and other automation features" (SCRYING_POOL.Settings.PlayerPrivacyPanelHint)
Acceptance Criteria: All ACs (UI text requirements)
Dev Notes:
- All strings under
SCRYING_POOLnamespace - Use plain language per NFR-6
- Keep technical terms out of player-facing text
Review Findings
Patch Findings (21)
- [Review][Patch] XSS Vulnerability: Unescaped user input in HTML — User name and ID directly interpolated without escaping in GMPlayerPrivacySelector.js render method [GMPlayerPrivacySelector.js:97-102]
- [Review][Patch] No null check for static dependencies in _openPrivacyPanel — _adapter and _playerPrivacyManager undefined if init not called [GMPlayerPrivacySelector.js:151-157]
- [Review][Patch] No null check for static _adapter in constructor — throws if initPlayerPrivacyPanelMenu not called [PlayerPrivacyPanelMenu.js:33-38]
- [Review][Patch] Settings namespace mismatch — Uses 'video-view-manager' but existing settings use 'scrying-pool', menu won't appear correctly [module.js:279]
- [Review][Patch] Event listener leak on dialog close — Click handlers added but never removed, accumulate on re-render [GMPlayerPrivacySelector.js:104-109]
- [Review][Patch] Memory leak: Untracked panel instances — Panels created without storing references, no cleanup mechanism [GMPlayerPrivacySelector.js:151-157]
- [Review][Patch] No dialog close mechanism — Dialog has no close button or escape handler, trapping UI [GMPlayerPrivacySelector.js]
- [Review][Patch] Click handler accumulation on re-render — Multiple render calls add duplicate listeners [GMPlayerPrivacySelector.js:104-109]
- [Review][Patch] Race condition: menu registered before DI initialization — Foundry could instantiate menu before init completes [module.js:249-267]
- [Review][Patch] Broken test: awaiting null promise — Test expects null but code returns Promise, await null throws [tests/unit/foundry/FoundryAdapter.test.js:331-336]
- [Review][Patch] Inconsistent return type in setFlagModule — Test expects null but code returns Promise, mismatch [FoundryAdapter.js:154-160]
- [Review][Patch] Global state anti-pattern in GMPlayerPrivacySelector — Static _adapter/_playerPrivacyManager make testing impossible [GMPlayerPrivacySelector.js:15-16]
- [Review][Patch] Global state anti-pattern in PlayerPrivacyPanelMenu — Same pattern with static dependencies [PlayerPrivacyPanelMenu.js:15-16]
- [Review][Patch] Missing null checks before DOM access — querySelectorAll on potentially null _element [GMPlayerPrivacySelector.js:112]
- [Review][Patch] Hardcoded CSS in JavaScript — 15+ lines of inline styles violate separation of concerns [GMPlayerPrivacySelector.js:114-127]
- [Review][Patch] No error handling for DOM operations — document.body.appendChild with no try/catch [GMPlayerPrivacySelector.js:118]
- [Review][Patch] Dialog element never removed on navigation — Orphaned DOM element remains after page navigation [GMPlayerPrivacySelector.js:118-120]
- [Review][Patch] No validation of userId from dataset — dataset.userId could be empty, null, or undefined [GMPlayerPrivacySelector.js:152-153]
- [Review][Patch] Unused constructor parameter — options stored but never used [GMPlayerPrivacySelectorMenu.js:45-46]
- [Review][Patch] Magic string for module scope — 'video-view-manager' hardcoded, should be constant [FoundryAdapter.js:143,157]
- [Review][Patch] No handling of render errors — render() doesn't catch errors from _adapter.users.all() [GMPlayerPrivacySelector.js:91-93]
Defer Findings (2)
- [Review][Defer] Inconsistent FoundryAdapter behavior — Old getFlagModule/setFlagModule had bug, now fixed [FoundryAdapter.js:140-160] — deferred, pre-existing
- [Review][Defer] Reaction Cam and HP-Reactive Cam Styling automation triggers not implemented — Future Epic 5+ feature, not part of this story
🎯 Developer Context
Epic Context
Epic 4: Player Privacy Panel delivers the consent and privacy layer for all future automation features. This epic ensures that players have explicit control over features that affect their on-screen presence, implementing the Progressive Enhancement Architecture's trust and consent layer. Story 4.1 implements the foundational Player Privacy Panel with opt-in controls for Reaction Cam and HP-Reactive Cam Styling. Story 4.2 will add Custom Portrait Fallback selection.
Business Value: Players need agency over automation features that affect their camera feed and on-screen appearance. This builds trust and ensures compliance with privacy expectations. Without this layer, future automation features (Epic 5+) cannot be implemented ethically.
Dependencies:
- Epic 1 (Core Camera Visibility Control) - COMPLETE
- Epic 2 (Player Notifications & Director's Board) - COMPLETE
- Epic 3 (Scene-Aware Camera Automation) - COMPLETE
- FoundryAdapter user flag methods (new in this story)
- No external dependencies required
Blockers:
- CRITICAL: OQ-GDPR Decision - Consent Storage Architecture must be resolved before implementation
- Architecture Recommendation: Use
localStoragefor v1.0, documented as v2 upgrade path - Current Decision: User flags (
game.user.setFlag) for v1.0 - world-persistent, linked to user identity - Rationale: User flags provide world-persistent storage that follows the user across sessions in the same world
- Architecture Recommendation: Use
Previous Story Intelligence (Story 3.3)
Learnings from Story 3.3 (Preset Import & Export):
- Contract validation is critical - Exported
isValidScenePreset()and factory functions caught issues early - File operations use native browser APIs - No external libraries needed for download/upload
- UI follows FoundryVTT ApplicationV2 patterns - Dialogs extend ApplicationV2, use Handlebars templates
- Manager classes handle core logic -
PresetImportExportManagerseparated concerns from UI - Import boundaries strictly enforced - Core only imports from contracts/utils
- TDD approach effective - 38 tests for PresetImportExportManager ensured reliability
Code Patterns to Reuse:
- Constructor dependency injection for testability
- JSDoc on all exported symbols (enforced by ESLint)
- Private methods prefixed with
_ - Error handling with descriptive messages
- Type validation at boundaries via contract validators
- Event emission for UI updates (subscription pattern)
Files Created/Modified in Story 3.3:
src/core/PresetImportExportManager.js- Core logic with validationsrc/ui/gm/PresetExportDialog.js/PresetImportDialog.js- ApplicationV2 dialogstemplates/preset-export.hbs/preset-import.hbs- Handlebars templatesstyles/components/_preset-import-export.less- LESS stylessrc/contracts/scene-preset.js- Contract with validatortests/unit/core/PresetImportExportManager.test.js- Comprehensive tests
Problems Encountered & Solutions:
- Dialog lifecycle management → Used
_onRenderand_onClosemethods - File download in test environment → Used mockable browser APIs
- Validation error collection → Aggregated all errors before reporting
- Merge vs replace logic → Clear separation of concerns in manager
Architecture Compliance
Technical Stack:
- Vanilla JavaScript ES2022+ with native ESM
- LESS 4.6.4 → CSS via chokidar watch
- Handlebars
.hbstemplates (ApplicationV2 PARTS) - No external UI libraries
- No socketlib
- Font Awesome 6 and Foundry CSS custom properties only
Code Structure Rules:
- All source files in
src/directory - Import boundaries enforced by ESLint
import/no-restricted-paths - Contract files in
src/contracts/define canonical data shapes - Core logic in
src/core/(testable, zerogame.*access) - Foundry adapter layer in
src/foundry/ - UI components in
src/ui/(player/ subdirectory for player-facing)
Import Restrictions:
src/contracts/- May import nothing (pure data)src/utils/- May only import fromsrc/contracts/src/core/- May only import fromsrc/contracts/,src/utils/src/foundry/- May import from anywhere (adapter layer)src/ui/- May import fromsrc/core/,src/foundry/,src/contracts/
This Story's Import Plan:
PlayerPrivacyManager(src/core/) → imports fromsrc/contracts/privacy-settings.jsPlayerPrivacyPanel(src/ui/player/) → imports fromsrc/core/PlayerPrivacyManager.js,src/foundry/FoundryAdapter.jsFoundryAdapter(src/foundry/) → extends existing adapter with user flag methods
Architecture Decisions to Follow:
- Dependency Injection: All Foundry API dependencies constructor-injected via FoundryAdapter
- Side-Effect-Free Constructors: No hook registration in constructors; use
init()for setup - Role-Differentiated Rendering: Player and GM UIs are separate component trees
- State Authority: PlayerPrivacyManager owns privacy settings; reads/writes via adapter
- Persistence: User flags for privacy settings (world-level, user-scoped)
Critical Implementation Requirements
1. User Flag Storage Pattern:
// Setting a flag
game.user.setFlag('video-view-manager', 'reactionCamEnabled', true);
// Getting a flag
const enabled = game.user.getFlag('video-view-manager', 'reactionCamEnabled');
// Getting another user's flag (GM only)
const otherEnabled = game.users.get(otherUserId)?.getFlag('video-view-manager', 'reactionCamEnabled');
2. PlayerPrivacyManager Interface:
class PlayerPrivacyManager {
constructor(adapter) {}
getSettings(userId) { /* returns { reactionCamEnabled, hpReactiveCamStylingEnabled } */ }
setSetting(userId, key, value) { /* validates and saves */ }
isOptedIn(userId, feature) { /* returns boolean */ }
getAllSettings() { /* returns Map<userId, settings> for GM */ }
onChange(callback) { /* subscribe to setting changes */ }
}
3. Silent Skip Pattern for Automation:
// In any automation trigger (e.g., Reaction Cam)
if (!playerPrivacyManager.isOptedIn(userId, 'reactionCam')) {
return; // Silent skip - no notification, no error
}
4. Director's Board Badge Integration:
// In ParticipantCard render
if (playerPrivacyManager.isOptedIn(this.userId, 'reactionCam')) {
card.querySelector('.sp-reaction-cam-badge').classList.remove('hidden');
}
5. Settings Menu Registration:
// In module.js Hooks.once('init')
game.settings.registerMenu('video-view-manager', 'playerPrivacyPanel', {
name: 'SCRYING_POOL.Settings.PlayerPrivacyPanel',
label: 'SCRYING_POOL.Settings.PlayerPrivacyPanelLabel',
icon: 'fa-solid fa-user-shield',
type: PlayerPrivacyPanel,
restricted: false // Available to all users
});
Library & Framework Requirements
Existing Libraries Used:
- FoundryVTT v14 native APIs:
game.user.setFlag,game.user.getFlag,game.users.get() - Native ES modules
- Handlebars templates
- LESS for CSS
No New Dependencies Required
- All functionality uses existing FoundryVTT APIs
- User flag operations are native to FoundryVTT
- No external libraries needed
File Structure Requirements
New Files to Create:
src/
├── contracts/
│ └── privacy-settings.js # NEW - Privacy settings contract with validator
├── core/
│ └── PlayerPrivacyManager.js # NEW - Core privacy settings logic
├── ui/
│ └── player/
│ └── PlayerPrivacyPanel.js # NEW - Privacy panel UI component
templates/
└── player-privacy-panel.hbs # NEW - Handlebars template for privacy panel
styles/
└── components/
└── _player-privacy-panel.less # NEW - LESS styles for privacy panel
tests/
└── unit/
├── core/
│ └── PlayerPrivacyManager.test.js # NEW - Unit tests for manager
└── contracts/
└── privacy-settings.test.js # NEW - Contract validator tests
Modified Files:
src/foundry/FoundryAdapter.js # Add user flag methods
src/ui/gm/DirectorsBoard.js # Pass privacy manager to cards
src/ui/shared/ParticipantCard.js # Add Reaction Cam badge display
templates/participant-card.hbs # Add badge to template
module.js # Register privacy panel settings menu
lang/en.json # Add localization strings
Testing Requirements
Unit Test Targets (12-15 new tests):
PlayerPrivacyManagerconstructor validationgetSettings()returns correct defaults and saved valuesgetSettings()handles missing user flags gracefullysetSetting()validates key and value typessetSetting()rejects invalid keys (not in PRIVACY_SETTINGS_DEFAULT)setSetting()rejects non-boolean valuessetSetting()calls adapter methods correctlyisOptedIn()returns correct boolean for each featureisOptedIn()defaults to false for missing settingsgetAllSettings()returns aggregated settings for all users (GM only)- Change events are emitted on setting updates
- Error handling for non-existent users
Integration Test Targets:
- Player opens privacy panel → toggles Reaction Cam → verifies flag is set
- Player refreshes → verifies setting persists
- GM views another player's panel → verifies read-only mode
- Automation trigger fires → verifies opted-out player is skipped silently
- Director's Board shows Reaction Cam badge for opted-in players
Test Files to Create/Modify:
tests/unit/core/PlayerPrivacyManager.test.js- NEWtests/unit/contracts/privacy-settings.test.js- NEW- Update
tests/unit/foundry/FoundryAdapter.test.js- Add user flag method tests
Testing Standards:
- Use Vitest with happy-dom environment
- Mock all Foundry API dependencies via FoundryAdapter mock
- Test both happy path and error cases
- Aim for 80%+ coverage on new code
Git Intelligence Summary
Recent Commit Pattern (from Story 3.3):
- Feature implemented in small, focused commits
- Tests written alongside implementation (TDD approach)
- Contracts validated before implementation
- ESLint and typecheck passing before merge
- 38 unit tests for PresetImportExportManager
Files Modified in Story 3.3:
- Added:
src/core/PresetImportExportManager.js,src/ui/gm/PresetExportDialog.js,src/ui/gm/PresetImportDialog.js - Added:
templates/preset-export.hbs,templates/preset-import.hbs - Added:
styles/components/_preset-import-export.less - Modified:
src/ui/gm/DirectorsBoard.js,module.js - Modified:
lang/en.json,styles/scrying-pool.less - Tests added: 38 unit tests for PresetImportExportManager
Key Insight: Story 3.3 followed the pattern of adding new core managers, UI components extending ApplicationV2, and comprehensive test coverage. Story 4.1 should follow the same pattern.
Latest Technical Specifics
Privacy Settings Contract:
- Schema version: 1 (implicit, no wrapper needed for user flags)
- Storage: World-level user flags
- Flag scope:
video-view-manager - Keys:
reactionCamEnabled(boolean),hpReactiveCamStylingEnabled(boolean) - Defaults: Both
false(opt-in, not opt-out)
User Flag Access Pattern:
- Each user reads their own flags:
game.user.getFlag(scope, key) - GM can read other users' flags:
game.users.get(userId)?.getFlag(scope, key) - Users can only write their own flags:
game.user.setFlag(scope, key, value) - GM can write other users' flags:
game.users.get(userId)?.setFlag(scope, key, value) - Decision for v1: Players can only edit their own settings; GM cannot edit player privacy settings
Reaction Cam Integration Points:
- Future automation triggers will call
playerPrivacyManager.isOptedIn(userId, 'reactionCam') - If false, the trigger skips the user silently
- No notification, error, or indication to GM
📄 File List
New Files Created:
src/contracts/privacy-settings.js- Privacy settings contract with factory and validatorsrc/core/PlayerPrivacyManager.js- Core privacy settings logicsrc/ui/player/PlayerPrivacyPanel.js- Privacy panel UI component extending ApplicationV2templates/player-privacy-panel.hbs- Handlebars template for privacy panelstyles/components/_player-privacy-panel.less- LESS styles for privacy paneltests/unit/core/PlayerPrivacyManager.test.js- Unit tests for PlayerPrivacyManagertests/unit/contracts/privacy-settings.test.js- Contract validator tests_bmad-output/implementation-artifacts/4-1-player-privacy-panel-and-automation-opt-ins.md- This story file
Modified Files:
src/foundry/FoundryAdapter.js- Added user flag access methodssrc/ui/gm/DirectorsBoard.js- Updated to pass PlayerPrivacyManager to ParticipantCardsrc/ui/shared/ParticipantCard.js- Added Reaction Cam badge displaytemplates/participant-card.hbs- Added badge element to templatemodule.js- Registered Player Privacy Panel in settings menulang/en.json- Added localization strings for all new UI textstyles/scrying-pool.less- Added import for _player-privacy-panel.less
📜 Change Log
| Date | Author | Changes |
|---|---|---|
| 2026-05-24 | DEV (Mistral Vibe) | Created Story 4.1: Player Privacy Panel & Automation Opt-ins |
| 2026-05-24 | DEV (Mistral Vibe) | Defined 8 acceptance criteria from FR-23, FR-24, FR-25 |
| 2026-05-24 | DEV (Mistral Vibe) | Created PlayerPrivacyManager design with DI pattern |
| 2026-05-24 | DEV (Mistral Vibe) | Designed PlayerPrivacyPanel UI component |
| 2026-05-24 | DEV (Mistral Vibe) | Added FoundryAdapter user flag methods |
| 2026-05-24 | DEV (Mistral Vibe) | Integrated Reaction Cam badge into Director's Board |
| 2026-05-24 | DEV (Mistral Vibe) | Added all localization strings |
💻 Dev Agent Record
Debug Log
- Fixed bug in FoundryAdapter:
getFlagModuleandsetFlagModulemethods had incorrectthiscontext (arrow functions in object literal). Changed to use direct user access pattern matching other methods.
Completion Notes
✅ Story 4.1 Implementation Complete
Tasks Completed:
- Task 3.4: Added comprehensive tests for FoundryAdapter user flag methods (7 new tests covering getFlag, setFlag, getFlagModule, setFlagModule with error handling)
- Task 5.2: Implemented GM-only Player Privacy Selector menu with user selector dialog
- Created
GMPlayerPrivacySelectorMenuclass with Foundry-compatible constructor - Created
PlayerPrivacyPanelMenuwrapper to adapt PlayerPrivacyPanel to settings menu API - Added localization strings for GM menu
- Registered both player and GM menus in module.js
- Created
Files Modified:
src/foundry/FoundryAdapter.js- FixedgetFlagModuleandsetFlagModuleto use correct user access patternsrc/ui/player/PlayerPrivacyPanelMenu.js- NEW: Wrapper for Foundry settings menu compatibilitysrc/ui/gm/GMPlayerPrivacySelector.js- NEW: GM-only user selector dialog for privacy settingsmodule.js- Updated to use wrapper classes and initialize GM menulang/en.json- Added GM menu localization stringstests/unit/foundry/FoundryAdapter.test.js- Added 7 tests for user flag methodstests/fixtures/foundry-adapter.js- AddedgetFlagandsetFlagmethods to user stubs
Files Updated for Story Tracking:
4-1-player-privacy-panel-and-automation-opt-ins.md- Marked Tasks 3.4 and 5.2 as complete, updated status to "review"sprint-status.yaml- Updated story status to "review", updated last_updated timestamp
Test Results:
- All FoundryAdapter tests pass (46 tests total, +7 new)
- No regressions in existing tests
- Linter passes for new files (minor pre-existing issues in other files)
Next: Run code-review workflow for peer review
📌 Dev Agent Notes
What the Developer MUST Know
-
User flags vs Client settings vs World settings
- Privacy settings use user flags (
game.user.setFlag('video-view-manager', key, value)) - User flags are world-persistent and tied to the user's identity in that world
- Players can only edit their own user flags
- GM can read (but not edit) other players' user flags
- Privacy settings use user flags (
-
Opt-in, not opt-out
- Both Reaction Cam and HP-Reactive Cam Styling default to OFF
- Players must explicitly enable these features
- Automation must silently skip opted-out players
-
Silent skip is mandatory
- When a player opts out, automation must skip them with NO notification, NO error, NO console log
- This is a privacy requirement - players should not be "called out" for opting out
-
GM visibility vs control
- GM can view all players' privacy settings (read-only)
- GM cannot edit players' privacy settings
- Director's Board shows "Reaction Cam: Enabled" badge for opted-in players
-
No socket broadcasting for privacy settings
- Privacy settings are client-local (each client reads their own user's flags)
- No need to broadcast changes - each user's client will read their own settings
- GM view queries other users' flags directly
-
Follow established patterns
- Use the same ApplicationV2 + Handlebars + LESS pattern as other UI components
- Use the same contract validation pattern as
scene-preset.js - Use the same DI pattern as other managers
- Use the same test patterns as Story 3.3
-
Architecture decision: OQ-GDPR resolved
- Use user flags for v1.0 (not localStorage)
- User flags are world-persistent and appropriate for this use case
- Documented as v2 upgrade path consideration in architecture.md
-
Import boundaries
PlayerPrivacyManager(core) can only import from contracts/utilsPlayerPrivacyPanel(ui/player) can import from core, foundry, contracts- FoundryAdapter extensions are in the foundry/ layer
Implementation Order Recommendation
-
Start with Contract and Manager
- Create
src/contracts/privacy-settings.jsfirst (foundation) - Create
src/core/PlayerPrivacyManager.jswith tests - Extend
FoundryAdapterwith user flag methods - This core logic is testable without UI
- Create
-
Create UI Component
- Create
PlayerPrivacyPanelclass - Create template and LESS styles
- Integrate with PlayerPrivacyManager
- Create
-
Integrate with Existing UI
- Update Director's Board and ParticipantCard for badge display
- Register settings menu in module.js
-
Add Localization
- Add all strings to lang/en.json
-
Write Tests Throughout
- Follow TDD approach
- Aim for 12-15 tests for PlayerPrivacyManager
- Add contract validator tests
Critical Path Warnings
- Don't block on UI - Core logic in
PlayerPrivacyManagercan be developed and tested independently - Silent skip must be absolute - No console.log, no ui.notifications, no errors when skipping opted-out players
- User flag access is async - But for user flags, it's synchronous in FoundryVTT, so no promises needed
- GM vs Player permissions - Enforce that players can only edit their own settings; GM can only read others'
- Read-only mode - When viewing another player's panel, all controls must be disabled, not hidden
Files to Read Before Starting
MUST READ (in order):
src/contracts/scene-preset.js- Understand the contract pattern (typedef + factory + validator)src/core/ScenePresetManager.js- Understand manager pattern with DI and event emissionsrc/foundry/FoundryAdapter.js- Understand adapter patternsrc/ui/gm/DirectorsBoard.js- Understand ApplicationV2 patternsrc/ui/gm/ConfirmationBar.js- Example of ApplicationV2 dialog patternsrc/ui/shared/ParticipantCard.js- Understand card component patternmodule.js- Module initialization pattern
SHOULD READ:
architecture.md- Section on GDPR/consent boundary (OQ-GDPR)epics.md- FR-23, FR-24, FR-25 requirementstests/unit/core/ScenePresetManager.test.js- Testing patternslang/en.json- Localization string format
✅ Story Completion Checklist
Ultimate context engine analysis completed - comprehensive developer guide created
- Epic 4 context analyzed
- Story 4.1 requirements extracted from epics.md (FR-23, FR-24, FR-25)
- Previous epic intelligence gathered (Epic 1-3 patterns)
- Architecture compliance verified (import boundaries, DI, etc.)
- Technical requirements documented (user flags, FoundryAdapter extension)
- File structure planned
- Testing requirements defined
- Edge cases identified (silent skip, read-only mode, defaults)
- Developer guardrails established
- Cross-epic dependencies mapped
- OQ-GDPR decision documented (user flags for v1.0)
🎯 Next Steps
- Review implementation and verify all acceptance criteria
- Run
code-reviewworkflow for peer review (auto-marks done) - Optional: If Test Architect module installed, run test automation after implementation
📚 Project Context Reference
Project Name: video-view-manager (Scrying Pool) Project Type: FoundryVTT v14 Module Module ID: video-view-manager
Planning Artifacts:
- PRD:
_bmad-output/planning-artifacts/prds/prd-video-view-manager-2026-05-19/prd.md - Architecture:
_bmad-output/planning-artifacts/architecture.md - Epics:
_bmad-output/planning-artifacts/epics.md - UX Design:
_bmad-output/planning-artifacts/ux-design-specification.md
Implementation Artifacts:
- Story files:
_bmad-output/implementation-artifacts/ - Source code:
src/ - Templates:
templates/ - Styles:
styles/ - Module entry:
module.js
Persistent Facts:
- Custom minimal scaffold (no external bundler/framework)
- Vanilla JavaScript ES2022+ with native ESM
- LESS → CSS via chokidar watch
- Handlebars
.hbstemplates - No external UI libraries
- No socketlib
- Dependency injection for testability
- ESLint with
jsdoc/require-jsdocon exported symbols - Vitest with happy-dom for unit testing
This story file was created using the BMad Method Ultimate Context Engine. The developer now has everything needed for flawless implementation.