Files
scrying-pool/_bmad-output/implementation-artifacts/3-2-scene-auto-apply-and-confirmationbar.md
2026-05-23 18:23:48 +02:00

34 KiB

Story 3.2: Scene Auto-Apply & ConfirmationBar

Status: done

Epic: 3 - Scene-Aware Camera Automation (Scene Presets)

Story Key: 3-2-scene-auto-apply-and-confirmationbar

Created: 2026-05-23

Last Updated: 2026-05-24

Code Review: done (2026-05-24)


Story Header

Field Value
Epic 3 - Scene-Aware Camera Automation (Scene Presets)
Story ID 3.2
Story Key 3-2-scene-auto-apply-and-confirmationbar
Title Scene Auto-Apply & ConfirmationBar
Status done
Priority High
Assigned Agent DEV (Amelia)
Created 2026-05-23

📋 Story Requirements

User Story

As a GM, I want to have Scene Presets automatically apply when I activate a Scene, with immediate strip-local feedback and a one-click Undo, So that camera layouts change seamlessly with scene transitions without manual intervention.

Persona Alignment

  • Primary: Marcus (Veteran GM) - Needs seamless transitions between pre-configured scenes during play
  • Primary: Jake (Streamer) - Requires professional, automated scene transitions for broadcast production
  • Secondary: All GMs - Reduces cognitive load by eliminating repetitive manual setup

Acceptance Criteria (BDD Format)

AC-1: Auto-Apply on Scene Activation

Given a Scene has a preset association configured When the GM activates that Scene (triggering updateScene hook) Then the associated preset applies after the configured pre-delay (0-5000ms) And all clients receive "Scene changed: camera layout updated" via ui.notifications

AC-2: Configurable Pre-Delay

Given a Scene has a pre-delay of N ms configured When that Scene activates Then the preset applies exactly N ms after the updateScene hook fires

AC-3: ConfirmationBar Appearance

Given auto-apply fires for a Scene When the Visibility Matrix update is broadcast Then the ConfirmationBar appears in StripOverlayLayer at position: absolute; bottom: 0 showing "Preset applied — N hidden, N visible" And an "Undo" button is present and primary affordance

AC-4: One-Click Undo

Given the "Undo" button is clicked When the click is processed Then the Visibility Matrix immediately reverts to the state before the preset was applied And all clients receive the reverted state

AC-5: ConfirmationBar Auto-Dismiss

Given the ConfirmationBar is visible and idle When 8 seconds elapse (or 4 seconds if ≥2 presets applied within 60 seconds) Then the bar dismisses via opacity transition only (never height or max-height animation)

AC-6: Instant-Replace Rule

Given a second ConfirmationBar would appear while one is already visible When the second is triggered Then it instantly replaces the first with zero crossfade (instant-replace rule)

AC-7: Per-Scene Disable

Given auto-apply is disabled for a specific Scene When that Scene is activated Then no preset applies and no automation notification fires And the Director's Board manual override remains fully functional

AC-8: Global Disable

Given auto-apply is disabled globally in module settings When any Scene is activated Then no preset auto-applies regardless of Scene-level associations

AC-9: Partial-Fail Amber Variant

Given the partial-fail case (some participants unreachable) When the ConfirmationBar renders Then it uses the amber variant: "Preset applied — N hidden, N visible (some updates pending)"

Functional Requirements Covered

  • FR-17: Scene Preset auto-applies on FoundryVTT Scene activation via updateScene hook
  • FR-18: Scene Preset auto-apply can be disabled per-scene or globally via module settings

Success Criteria

  • All 9 acceptance criteria pass manual testing
  • All unit tests pass (target: +25-30 new tests for ConfirmationBar + ScenePresetManager auto-apply)
  • npm run lint exits 0 (ESLint import boundaries enforced)
  • npm run typecheck exits 0 (strict JSDoc compliance)
  • Code review passes with no critical findings
  • Integration test: Scene activation → preset apply → ConfirmationBar → Undo flow verified end-to-end

📝 Tasks / Subtasks

Task 1: Extend src/core/ScenePresetManager.js with Auto-Apply Logic

Files: src/core/ScenePresetManager.js, tests/unit/core/ScenePresetManager.test.js

Subtasks:

  • 1.1: Write TDD red tests for auto-apply methods — onSceneActivate, applyPreset with options, configureAutoApply
  • 1.2: Extend constructor to accept visibilityManager and socketHandler parameters
  • 1.3: Implement onSceneActivate(scene) — checks global enable, scene config, pre-delay, then applies preset
  • 1.4: Implement applyPreset(presetName, options) — applies preset matrix, emits socket message, returns result
  • 1.5: Implement configureAutoApply(scene, config) — updates scene flag with auto-apply settings
  • 1.6: Implement _getAutoApplyConfig(scene) — reads and validates auto-apply config from scene flag
  • 1.7: Implement _applyWithDelay(scene, presetName, delayMs) — sets timeout, clears on scene change
  • 1.8: Implement migration handler for missing autoApply field (defaults to disabled)
  • 1.9: Update existing tests to pass new constructor parameters
  • 1.10: Green all ScenePresetManager tests including new auto-apply tests

Acceptance Criteria: AC-1, AC-2, AC-7, AC-8


Task 2: Create src/ui/gm/ConfirmationBar.js

Files: src/ui/gm/ConfirmationBar.js, tests/unit/ui/gm/ConfirmationBar.test.js, templates/confirmation-bar.hbs, styles/components/_confirmation-bar.less

Subtasks:

  • 2.1: Write TDD red tests for ConfirmationBar — show, hide, undo, auto-dismiss, instant-replace
  • 2.2: Implement ConfirmationBar class with constructor (adapter, visibilityManager, socketHandler, stripOverlayLayer)
  • 2.3: Implement init() — registers hook listener for scrying-pool:presetApplied
  • 2.4: Implement teardown() — unregisters hooks, clears timers
  • 2.5: Implement show(payload) — renders bar, captures previous matrix, starts timer
  • 2.6: Implement hide() — removes bar, clears timer
  • 2.7: Implement _onUndo() — reverts to previous matrix via visibilityManager
  • 2.8: Implement _startDismissTimer() — 8000ms default, 4000ms if recent activity
  • 2.9: Implement _onPresetApplied(payload) — handler for hook event, determines variant (amber for partial)
  • 2.10: Implement _onNewPresetAppliedWhileVisible() — instant-replace logic, zero crossfade
  • 2.11: Create confirmation-bar.hbs template with message, undo button, variants
  • 2.12: Create _confirmation-bar.less with styles, animations, reduced-motion support
  • 2.13: Green all ConfirmationBar tests

Acceptance Criteria: AC-3, AC-4, AC-5, AC-6, AC-9


Task 3: Create src/ui/gm/ScenePresetPanel.js

Files: src/ui/gm/ScenePresetPanel.js, tests/unit/ui/gm/ScenePresetPanel.test.js, templates/scene-preset-panel.hbs, styles/components/_scene-preset-panel.less

Subtasks:

  • 3.1: Write TDD red tests for ScenePresetPanel — toggle, preset selection, delay config
  • 3.2: Implement ScenePresetPanel class for per-scene auto-apply configuration
  • 3.3: Implement async _prepareContext(scene) — returns { enabled, presetName, preDelay, presets }
  • 3.4: Implement _onToggleAutoApply(enabled) — updates scene flag via configureAutoApply
  • 3.5: Implement _onPresetSelected(presetName) — updates config for this scene
  • 3.6: Implement _onDelayChanged(delayMs) — validates 0-5000, updates config
  • 3.7: Implement accessibility: keyboard nav, focus trap, ARIA labels
  • 3.8: Create scene-preset-panel.hbs template with toggle, dropdown, slider
  • 3.9: Create _scene-preset-panel.less with styles matching DirectorsBoard theme
  • 3.10: Green all ScenePresetPanel tests

Acceptance Criteria: AC-7, AC-8


Task 4: Extend module.js with Scene Hook Registration

Files: module.js

Subtasks:

  • 4.1: Import ScenePresetManager, ConfirmationBar, ScenePresetPanel
  • 4.2: Construct ScenePresetManager with stateStore, adapter, visibilityManager, socketHandler
  • 4.3: Register updateScene hook via adapter.hooks.on('updateScene', scenePresetManager.onSceneActivate)
  • 4.4: Construct ConfirmationBar with adapter, visibilityManager, socketHandler, stripOverlayLayer
  • 4.5: Call confirmationBar.init() after construction
  • 4.6: For GM users, integrate ScenePresetPanel into DirectorsBoard
  • 4.7: Verify all injection order dependencies are satisfied

Acceptance Criteria: AC-1, AC-2


Task 5: Extend src/ui/shared/StripOverlayLayer.js for ConfirmationBar

Files: src/ui/shared/StripOverlayLayer.js, styles/components/_strip-overlay-layer.less, tests/unit/ui/shared/StripOverlayLayer.test.js

Subtasks:

  • 5.1: Create StripOverlayLayer class (was missing from Story 1.5) with constructor (adapter)
  • 5.2: Implement init(), get element(), render(), remove(), clearAll(), teardown()
  • 5.3: Create overlay DOM element with styles: position: absolute; inset: 0; pointer-events: none; overflow: visible
  • 5.4: Implement overlay tracking via Map for replacement support
  • 5.5: Create _strip-overlay-layer.less with scoped styles
  • 5.6: Verify pointer-events handling allows ConfirmationBar interaction

Acceptance Criteria: AC-3

Note: StripOverlayLayer was missing from Story 1.5 deliverables. Created complete implementation.


Task 6: Extend src/ui/gm/DirectorsBoard.js with ScenePresetPanel

Files: src/ui/gm/DirectorsBoard.js, tests/unit/ui/gm/DirectorsBoard.test.js

Subtasks:

  • 6.1: Import ScenePresetPanel
  • 6.2: Add _presetPanel field to constructor
  • 6.3: Extend _prepareContext() to include auto-apply status for current scene
  • 6.4: Update template to include ScenePresetPanel as collapsible drawer/tab
  • 6.5: Implement toggle handler for auto-apply panel visibility
  • 6.6: Update _onSceneChanged() or similar to refresh panel with new scene data
  • 6.7: Update tests for new panel integration

Acceptance Criteria: AC-7, AC-8


Task 7: Update Contracts and Fixtures

Files: src/contracts/socket-message.js, tests/fixtures/scene-preset.js

Subtasks:

  • 7.1: Verify PRESET_APPLY and PRESET_APPLIED constants exist in socket-message.js
  • 7.2: Extend payload schema validation for auto-apply fields (sceneId, autoApplied)
  • 7.3: Add new fixtures: SCENE_FLAG_WITH_AUTO_APPLY, SCENE_FLAG_WITHOUT_AUTO_APPLY, SCENE_FLAG_DISABLED_AUTO_APPLY
  • 7.4: Add fixture for partial-fail scenario
  • 7.5: Verify all fixtures are Object.freeze'd

Acceptance Criteria: All ACs (payload validation)


🎯 Developer Context Section

Epic Context

Epic 3: Scene-Aware Camera Automation (Scene Presets) completes the Level 3 Progressive Enhancement:

  • Story 3.1 (PREVIOUS - ready-for-dev): ScenePresetManager, save/load UI, Scene flag storage, socket broadcast
  • Story 3.2 (THIS STORY): Auto-apply on Scene activation, ConfirmationBar with Undo, per-scene/global disable toggles
  • Story 3.3 (NEXT - backlog): Preset import/export as JSON, merge/replace logic

This story delivers the automation magic - the ability for GMs to set up camera layouts once and have them apply automatically during sessions. The ConfirmationBar provides immediate, strip-local feedback with a safety net (Undo) for when things don't go as expected.

Cross-Epic Dependencies

Dependency Source Status Used In This Story
ScenePresetManager Story 3.1 ready-for-dev Extended with auto-apply logic
Scene flag storage Story 3.1 ready-for-dev Read preset associations
StateStore.setMatrix() Story 1.4 done Apply preset matrix
SocketHandler broadcast Story 1.3 done Broadcast preset apply
NotificationBus Story 2.1 done Optional fallback notification
DirectorsBoard Story 2.2 done Manual override always available
ConfirmationBar UX patterns UX-DR12 Specified Full implementation

Previous Story Intelligence (Story 3.1)

Critical Learnings from 3-1-save-and-load-scene-presets:

  1. Scene flag schema is frozen: { _version: 1, presets: { [name]: ScenePreset } } - DO NOT modify this structure
  2. ScenePreset structure is canonical: { _version: 1, name, matrix, createdAt, updatedAt }
  3. Socket events already defined in contracts: scrying-pool.preset.apply and scrying-pool.preset.applied
  4. Import boundary for ScenePresetManager: src/core/ScenePresetManager.js may ONLY import from src/contracts/ and src/utils/
  5. World settings for global config: Use scrying-pool.autoApplyEnabled (boolean, default: true)
  6. Test pattern: Fake timers for pre-delay testing; frozen fixtures for preset structures

Files Created in 3.1 (DO NOT RECREATE):

  • src/core/ScenePresetManager.js - Extend this, don't replace it
  • src/ui/gm/PresetSaveDialog.js - Already exists
  • src/ui/gm/PresetLoadDialog.js - Already exists
  • src/contracts/scene-preset.js - Already exists, has validators

Patterns Established in 3.1:

  • Scene flag access via adapter.scenes.current().getFlag() and .setFlag()
  • Matrix serialization/deserialization in ScenePresetManager
  • Error handling: clear user-facing messages, no stack traces

Git Intelligence

Recent commits in Epic 3:

  • 3-1-save-and-load-scene-presets.md created 2026-05-23 09:51 - Full ScenePresetManager spec
  • Architecture established for Scene flag storage with versioning
  • Socket contract for preset events pre-defined

Actionable Insights:

  • The preset storage mechanism is solid and tested
  • Socket infrastructure for preset apply is ready but not wired to Scene hooks
  • Need to add updateScene hook registration in module.js
  • ConfirmationBar is NEW - no existing implementation to build on

🏗️ Technical Requirements

Core Components to Create/Extend

Component File Action Purpose
ScenePresetManager src/core/ScenePresetManager.js EXTEND Add auto-apply logic, per-scene config
ConfirmationBar src/ui/gm/ConfirmationBar.js NEW Strip-local feedback with Undo
StripOverlayLayer src/ui/shared/StripOverlayLayer.js EXTEND Add ConfirmationBar container
ScenePresetPanel src/ui/gm/ScenePresetPanel.js NEW Per-scene auto-apply toggle UI

Data Flow - Auto-Apply Sequence

Hooks.on('updateScene', scene)
  ↓
ScenePresetManager.onSceneActivate(scene)
  ↓ [Check: auto-apply enabled globally?]
  ↓ [Check: scene has preset association?]
  ↓ [Check: scene has auto-apply enabled?]
  ↓ [Wait: configured pre-delay (0-5000ms)]
  ↓
ScenePresetManager.applyPreset(presetName)
  ↓
VisibilityManager.applyMatrix(preset.matrix)
  ↓
StateStore.setMatrix(preset.matrix)
  ↓
SocketHandler.emit('scrying-pool.preset.apply', payload)
  ↓
[Broadcast to all clients]
  ↓
Hooks.callAll('scrying-pool:presetApplied', { presetName, sceneId, matrix })
  ↓
ConfirmationBar.show({ presetName, hiddenCount, visibleCount, partialFail })
  ↓
[8s or 4s timer starts]
  ↓ (User clicks Undo)
ConfirmationBar.onUndo() → StateStore.setMatrix(previousMatrix)

New/Extended Socket Messages

Already defined in src/contracts/socket-message.js (from Story 3.1):

  • PRESET_APPLY: "scrying-pool.preset.apply" - Intent (GM only)
  • PRESET_APPLIED: "scrying-pool.preset.applied" - Authoritative echo

New for this story:

  • Payload extension for PRESET_APPLY: Add { sceneId, preDelay, autoApplied: true }
  • New hook: scrying-pool:presetApplied for ConfirmationBar subscription

World Settings (New)

Setting Key Type Default Scope Description
scrying-pool.autoApplyEnabled boolean true world Global toggle for auto-apply feature
scrying-pool.confirmationBarDuration number 8000 world Default bar duration in ms
scrying-pool.shortConfirmationBarDuration number 4000 world Short duration when ≥2 presets in 60s

Note: Per-scene auto-apply toggle stored in Scene flag alongside preset association.

Scene Flag Structure Extension

Existing (from Story 3.1):

{
  _version: 1,
  presets: { [name: string]: ScenePreset }
}

Extended for Story 3.2:

{
  _version: 1,
  presets: { [name: string]: ScenePreset },
  autoApply: {
    enabled: boolean,      // Per-scene toggle
    presetName: string,    // Which preset to auto-apply
    preDelay: number       // 0-5000 ms delay
  }
}

Migration: If autoApply field missing, defaults to { enabled: false, presetName: null, preDelay: 0 }


🏛️ Architecture Compliance

Import Boundary Rules (HARD - ESLint Enforced)

NEW FILES:

src/ui/gm/ConfirmationBar.js
  → may import: src/core/, src/contracts/, src/utils/ ONLY
  ❌ FORBIDDEN: src/foundry/, src/ui/gm/PresetSaveDialog.js, etc.

src/ui/gm/ScenePresetPanel.js
  → may import: src/core/, src/contracts/, src/utils/ ONLY
  ❌ FORBIDDEN: src/notifications/, direct game.* access

EXTENDED FILES:

src/core/ScenePresetManager.js (EXTEND from Story 3.1)
  → EXISTING: may import src/contracts/, src/utils/ ONLY
  → NEW: may ALSO import src/core/VisibilityManager.js (for applyMatrix)
  ❌ STILL FORBIDDEN: src/foundry/, src/ui/, direct game.*

src/ui/shared/StripOverlayLayer.js (EXTEND from Story 1.5)
  → EXISTING: may import src/core/, src/contracts/, src/utils/
  → NEW: may import src/ui/gm/ConfirmationBar.js

Constructor Pattern (Side-Effect-Free)

ConfirmationBar:

// ✅ CORRECT
export class ConfirmationBar {
  constructor(adapter, visibilityManager, socketHandler) {
    this._adapter = adapter;
    this._visibilityManager = visibilityManager;
    this._socketHandler = socketHandler;
    this._previousMatrix = null;
  }

  init() {
    // Lifecycle registration here, NOT in constructor
    this._adapter.hooks.on('scrying-pool:presetApplied', (payload) => this._onPresetApplied(payload));
  }

  teardown() {
    this._adapter.hooks.off('scrying-pool:presetApplied', this._onPresetApplied);
  }
}

ScenePresetManager Extension:

// EXTEND existing class from Story 3.1
export class ScenePresetManager {
  // Existing methods: save(), load(), delete(), list()
  
  // NEW: Auto-apply methods
  onSceneActivate(scene) { /* ... */ }
  applyPreset(presetName, options = { autoApplied: false }) { /* ... */ }
  configureAutoApply(scene, { enabled, presetName, preDelay }) { /* ... */ }
}

Dependency Injection

All new components follow FoundryAdapter pattern:

  • ConfirmationBar receives adapter via constructor
  • ScenePresetPanel receives adapter via constructor
  • NO direct game.* access in any new file
  • All Foundry API calls go through adapter.scenes, adapter.hooks, etc.

Hook Registration Order

Critical: Module.js wiring order for Story 3.2 components:

// In module.js, inside Hooks.once('ready', () => { ... })

// EXISTING (from previous stories) - order preserved:
const visibilityManager = new VisibilityManager(stateStore, adapter);
socketHandler.setReady(visibilityManager);
const notificationBus = new NotificationBus(adapter);
const roleRenderer = new RoleRenderer(visibilityManager, adapter);
const rosterStrip = new RosterStrip(visibilityManager, roleRenderer);

// NEW for Story 3.2:
const scenePresetManager = new ScenePresetManager(
  stateStore, 
  adapter,
  visibilityManager,  // NEW: for applyMatrix
  socketHandler        // NEW: for preset broadcast
);

// Register updateScene hook for auto-apply
adapter.hooks.on('updateScene', (scene) => {
  scenePresetManager.onSceneActivate(scene);
});

// NEW: StripOverlayLayer gets ConfirmationBar support
const stripOverlayLayer = new StripOverlayLayer(adapter);
const confirmationBar = new ConfirmationBar(
  adapter,
  visibilityManager,
  socketHandler,
  stripOverlayLayer
);
confirmationBar.init();

// If GM, register DirectorsBoard with extended preset panel
if (adapter.users.isGM()) {
  const directorsBoard = new DirectorsBoard(visibilityManager, socketHandler, adapter);
  // DirectorsBoard now includes ScenePresetPanel as embedded component
}

📁 File Structure Requirements

Files to CREATE

File Location Purpose AC Blocking
ConfirmationBar.js src/ui/gm/ConfirmationBar.js Strip-local feedback component
ConfirmationBar.test.js tests/unit/ui/gm/ConfirmationBar.test.js Unit tests
ScenePresetPanel.js src/ui/gm/ScenePresetPanel.js Per-scene auto-apply config UI
ScenePresetPanel.test.js tests/unit/ui/gm/ScenePresetPanel.test.js Unit tests
_confirmation-bar.less styles/components/_confirmation-bar.less ConfirmationBar styles
confirmation-bar.hbs templates/confirmation-bar.hbs Handlebars template

Files to EXTEND

File Changes From Story
ScenePresetManager.js Add auto-apply methods 3.1
module.js Wire updateScene hook, inject dependencies Story 0
StripOverlayLayer.js Add ConfirmationBar rendering 1.5
DirectorsBoard.js Integrate ScenePresetPanel 2.2

File Boundaries

ConfirmationBar owns:

  • Display logic for preset apply feedback
  • Undo button click handler
  • Auto-dismiss timer management
  • Instant-replace logic for consecutive bar displays

ScenePresetManager owns (NEW):

  • Auto-apply configuration per-scene
  • Pre-delay timer management
  • Scene activation detection
  • Preset application trigger

ScenePresetPanel owns:

  • Per-scene auto-apply toggle UI
  • Pre-delay configuration (0-5000ms slider)
  • Preset selection for auto-apply

🧪 Testing Requirements

Unit Test Coverage Targets

Component Test File Coverage Target
ConfirmationBar ConfirmationBar.test.js 100% branch coverage
ScenePresetManager (new methods) Extend ScenePresetManager.test.js +25 new tests
ScenePresetPanel ScenePresetPanel.test.js 100% statement coverage

Test Scenarios (MUST INCLUDE)

ConfirmationBar:

// Fake timers required for duration testing
vi.useFakeTimers();

// Test 1: Shows on preset applied event
// Test 2: Undo clicks revert to previous matrix
// Test 3: Auto-dismisses after 8000ms
// Test 4: Auto-dismisses after 4000ms when ≥2 presets in 60s
// Test 5: Instant-replace when new event during visible bar
// Test 6: Shows amber variant on partial fail
// Test 7: Clears timer on manual dismiss
// Test 8: accessibility: focus trap, keyboard navigation

ScenePresetManager (auto-apply):

// Test 1: onSceneActivate does nothing when no preset association
// Test 2: onSceneActivate applies preset after pre-delay
// Test 3: onSceneActivate respects global disable
// Test 4: onSceneActivate respects per-scene disable
// Test 5: Pre-delay timer cleared on scene change
// Test 6: applyPreset with autoApplied=true emits correct socket event
// Test 7: configureAutoApply updates Scene flag correctly
// Test 8: Migration: missing autoApply field gets defaults

Integration Tests:

// Test 1: Full flow: Scene activation → preset apply → ConfirmationBar → Undo
// Test 2: Partial fail: Some participants offline → amber bar
// Test 3: Global disable → no auto-apply on any scene
// Test 4: Per-scene disable → no auto-apply on that scene only

Fixtures to Add/Update

New fixtures in tests/fixtures/scene-preset.js:

export const SCENE_FLAG_WITH_AUTO_APPLY = Object.freeze({
  _version: 1,
  presets: { /* ... */ },
  autoApply: { enabled: true, presetName: 'combat', preDelay: 1000 }
});

export const SCENE_FLAG_WITHOUT_AUTO_APPLY = Object.freeze({
  _version: 1,
  presets: { /* ... */ }
  // autoApply missing - should default to disabled
});

export const SCENE_FLAG_DISABLED_AUTO_APPLY = Object.freeze({
  _version: 1,
  presets: { /* ... */ },
  autoApply: { enabled: false, presetName: null, preDelay: 0 }
});

🎨 UX Design Requirements

ConfirmationBar Specification (UX-DR12)

Location: StripOverlayLayer at position: absolute; bottom: 0

Visual:

  • Background: --sp-surface (semantic token)
  • Text: --sp-text-primary
  • Border: 1px solid --sp-border
  • Padding: 12px 16px
  • Border-radius: 4px
  • Box-shadow: 0 2px 8px rgba(0,0,0,0.3)

Content:

  • Message: "Preset applied — N hidden, N visible"
  • Undo button: Primary CTA, left side
  • Duration indicator: Optional subtle progress bar

Variants:

  • Default: Green accent for success
  • Amber: Orange accent when partial fail ("some updates pending")

Animations:

  • In: Slide up from bottom + fade (200ms ease-out)
  • Out: Slide down to bottom + fade (200ms ease-in)
  • CRITICAL: Only opacity transitions for height changes - NEVER height or max-height animation
  • Gated under @media (prefers-reduced-motion: no-preference)

Behavior:

  • Auto-dismiss: 8000ms default, 4000ms if ≥2 presets applied within 60000ms window
  • Instant-replace: New bar replaces existing with 0ms crossfade
  • Click Undo: Reverts matrix, dismisses bar immediately
  • Click outside: No dismiss (bar is in StripOverlayLayer, which has pointer-events: none on parent)
  • Click on bar: No action (bar is informational, only Undo is interactive)

Accessibility:

  • role="status" on bar container
  • aria-live="polite" for message updates
  • aria-label="Preset [name] applied. Undo available." on bar
  • Undo button: role="button", aria-label="Undo preset apply"
  • Focus: Undo button receives focus when bar appears
  • Keyboard: Space/Enter on Undo button triggers revert

ScenePresetPanel Specification

Location: Embedded in DirectorsBoard as collapsible drawer/tab

Controls:

  • Toggle: "Auto-apply preset on scene activation" (checkbox)
  • Preset selector: Dropdown of available presets for this scene
  • Pre-delay: Slider 0-5000ms with value display
  • Global settings link: "Configure global auto-apply settings"

Accessibility:

  • All interactive elements keyboard-navigable
  • ARIA labels on all controls
  • Focus trap within panel

🔒 Security & Performance Requirements

Security

  • No data transmission: Scene flag data stays in FoundryVTT world
  • Permission check: Only GM can configure auto-apply settings
  • Validation: All Scene flag inputs validated before saving
  • Sanitization: Preset names sanitized (no HTML, max length 100 chars)

Performance

  • Pre-delay max: 5000ms - enforced at validation level
  • Timer cleanup: All timers cleared on module teardown
  • Debounce: If multiple scene activations in quick succession, only last one processed
  • Memory: ConfirmationBar holds previous matrix reference only while visible
  • No blocking: All operations async; no synchronous waits

📂 Project Structure Notes

Alignment with Unified Structure

All new files follow the established pattern:

  • src/ui/gm/ - GM-only UI components
  • src/core/ - Pure logic, testable
  • styles/components/ - Component-specific LESS
  • templates/ - Handlebars templates
  • tests/unit/{mirror-path}/ - One spec per source file

Detected Conflicts / Variances

None detected. The architecture established in Stories 0, 1.x, and 2.x fully supports this implementation.

Previous Work Patterns to Follow

  1. Constructor injection (from Story 1.3): All Foundry deps via FoundryAdapter
  2. Import boundaries (from Story 0): ESLint no-restricted-paths enforced
  3. Test patterns (from Story 1.3): Fake timers, frozen fixtures, canonical mocks
  4. CSS architecture (from Story 0): LESS partials, semantic tokens, scoped selectors
  5. Socket patterns (from Story 1.3): Intent/echo cycle, PendingOp lifecycle

🔗 References

Source Documents

Reference Path Section
Story 3.2 ACs _bmad-output/planning-artifacts/epics.md Story 3.2: Scene Auto-Apply & ConfirmationBar
FR-17 _bmad-output/planning-artifacts/epics.md FR-17: Scene Preset auto-applies on Scene activation
FR-18 _bmad-output/planning-artifacts/epics.md FR-18: Disable auto-apply per-scene or globally
UX-DR12 _bmad-output/planning-artifacts/epics.md UX-DR12: ConfirmationBar specification
Architecture _bmad-output/planning-artifacts/architecture.md Full project architecture
ScenePresetManager _bmad-output/implementation-artifacts/3-1-save-and-load-scene-presets.md Story 3.1 implementation
Socket contracts src/contracts/socket-message.js PRESET_APPLY, PRESET_APPLIED
Scene flag schema src/contracts/scene-preset.js Versioned wrapper pattern

Previous Story Files (Critical Context)

Story File Relevance
3.1 3-1-save-and-load-scene-presets.md MUST READ - ScenePresetManager foundation
2.3 2-3-directors-board-bulk-actions-spotlight-and-keyboard-shortcuts.md Bulk action patterns, Undo concept
1.5 1-5-gm-control-ui-scryingpoolstrip-actionpopover-and-av-tile-integration.md StripOverlayLayer patterns
1.4 1-4-core-logic-scryingpoolcontroller-and-visibilitymanager.md Matrix application, state management

🤖 Dev Agent Record

Agent Model Used

DEV (Amelia) - Senior software engineer for story execution

Debug Log References

Critical debugging checkpoints:

  1. Hooks.on('updateScene') registration - added in module.js (Task 4)
  2. Scene flag read/write - implemented in ScenePresetManager (Task 1)
  3. ConfirmationBar timer management - implemented (Task 2)
  4. Undo matrix storage - implemented in ConfirmationBar (Task 2)
  5. Socket payload validation - verified in src/contracts/scene-preset.js (Task 7)

Code Review Fixes Applied

Fix 1: module.js wiring (HIGH)

  • Added imports for ConfirmationBar and StripOverlayLayer
  • Constructed ScenePresetManager with visibilityManager in ready hook
  • Created StripOverlayLayer and ConfirmationBar instances
  • Registered updateScene hook to trigger scenePresetManager.onSceneActivate()
  • Registered autoApplyEnabled world setting

Fix 2: Settings key namespace (HIGH)

  • Changed scrying-pool.autoApplyEnabled to video-view-manager.autoApplyEnabled in ScenePresetManager.onSceneActivate()
  • Settings are now properly namespaced under the module's registered namespace

Fix 3: Socket loop prevention (HIGH)

  • Added emitSocket option parameter to load() method (default: true)
  • load() now only emits PRESET_APPLIED when emitSocket is true
  • module.js socket handler passes { emitSocket: false } to prevent loop
  • PresetLoadDialog continues to emit (manual GM action)

Fix 4: Auto-apply config preservation (MEDIUM)

  • Modified _saveScenePresets() to read existing autoApply config from scene flag
  • Preserves autoApply setting when saving presets
  • Prevents loss of per-scene auto-apply configuration

Fix 5: Scene timer cleanup (MEDIUM)

  • Added _clearAllTimers() method to clear ALL pending timers
  • onSceneActivate() now calls _clearAllTimers() before applying new preset
  • Prevents old scene's timer from firing after switching scenes

Task 1 Implementation Notes:

  • Extended ScenePresetManager constructor to accept optional visibilityManager parameter
  • Added constants: MAX_PREDELAY_MS (5000), MIN_PREDELAY_MS (0)
  • Implemented onSceneActivate(scene) with full auto-apply logic chain
  • Implemented applyPreset(presetName, options) supporting autoApplied flag
  • Implemented configureAutoApply(scene, config) with validation
  • Implemented _getAutoApplyConfig(flagData) with default fallback
  • Implemented _applyWithDelay(scene, presetName, delayMs) with timer storage
  • Implemented _clearSceneTimer(scene) for cleanup
  • Implemented _getSceneFlagData(scene) for safe flag access
  • All 61 tests passing including 15 new Story 3.2 tests

Completion Notes List

Before marking done, verify:

  • ConfirmationBar appears and disappears correctly
  • Undo works and reverts to exact previous state
  • Auto-apply respects both global and per-scene settings
  • Pre-delay is configurable and accurate
  • Partial fail shows amber variant
  • Instant-replace works for consecutive bars
  • All timers cleaned up on module teardown
  • No memory leaks from event listeners
  • Accessibility: keyboard nav, ARIA labels, focus management
  • All ESLint import boundaries pass

File List

NEW FILES (9):

  1. src/ui/gm/ConfirmationBar.js
  2. src/ui/gm/ScenePresetPanel.js
  3. src/ui/shared/StripOverlayLayer.js
  4. tests/unit/ui/gm/ConfirmationBar.test.js
  5. tests/unit/ui/gm/ScenePresetPanel.test.js
  6. styles/components/_confirmation-bar.less
  7. styles/components/_strip-overlay-layer.less
  8. templates/confirmation-bar.hbs
  9. tests/fixtures/scene-preset.js (updated with auto-apply fixtures)

MODIFIED FILES (5):

  1. src/core/ScenePresetManager.js (extended with auto-apply methods, constructor updated)
  2. tests/unit/core/ScenePresetManager.test.js (added 15 new Story 3.2 tests)
  3. module.js (to wire updateScene hook, inject visibilityManager)
  4. src/ui/shared/StripOverlayLayer.js (to add ConfirmationBar support)
  5. src/ui/gm/DirectorsBoard.js (to integrate ScenePresetPanel)

CONTRACT FILES (verify, don't modify):

  • src/contracts/socket-message.js (already has preset events)
  • src/contracts/scene-preset.js (already has validators)

Story Completion Status

Status: done

Code Review: done (2026-05-24)

Ultimate context engine analysis completed - comprehensive developer guide created with:

  • Complete epic and cross-story context
  • Exhaustive architecture compliance requirements
  • Previous story intelligence and patterns
  • Git history insights
  • Specific file structure and boundaries
  • Testing requirements and fixtures
  • UX design specifications
  • Security and performance constraints
  • Actionable dev agent guardrails

The developer now has everything needed for flawless implementation!


Generated for {project_name} by BMad Method Story Context Engine Agent: DEV (Amelia) Date: 2026-05-23