---
phase: 05-jamtrack-implementation
plan: 03
type: execute
---

<objective>
Implement playback controls, visibility-aware polling, and seek slider for JamTrack player.

Purpose: Add play/pause/stop controls with jamClient integration, implement 500ms/2000ms polling pattern from Phase 3, and wire seek slider with UAT-003 fix pattern.
Output: Functional playback controls with real-time position updates and smooth seeking.
</objective>

<execution_context>
@./.claude/get-shit-done/workflows/execute-phase.md
@./.claude/get-shit-done/templates/summary.md
</execution_context>

<context>
@.planning/PROJECT.md
@.planning/ROADMAP.md
@.planning/STATE.md
@.planning/phases/05-jamtrack-implementation/05-CONTEXT.md
@.planning/phases/05-jamtrack-implementation/05-02-SUMMARY.md
@.planning/phases/04-jamtrack-research-design/COMPONENT_DESIGN.md
@.planning/codebase/JAMTRACK_API.md
@jam-ui/src/components/client/JKSessionBackingTrackPlayer.js
@jam-ui/src/components/client/JKSessionJamTrackPlayer.js

**Established patterns from Phase 3:**
- Visibility-aware polling: 500ms visible, 2000ms hidden
- useCallback for all handlers (prevent re-renders)
- Conditional state updates in polling (only update if values changed)
- UAT-003 fix: pending seek while paused, apply on resume
- End-of-track handling: stop and reset when position >= duration
- isOperating flag prevents rapid clicks during async operations

**From Phase 4 design:**
- Playback controls: handlePlay, handlePause, handleStop
- Polling: JamTrackGetPositionMs, JamTrackGetDurationMs
- Seek: handleSeek dispatches seekJamTrack thunk
- End-of-track: detect currentPositionMs >= durationMs
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement playback control handlers</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Add playback control handlers to JKSessionJamTrackPlayer:

**1. handlePlay:**
```javascript
const handlePlay = useCallback(async () => {
  if (isOperating || !jamClient || !fqIdRef.current) return;

  try {
    setIsOperating(true);
    setError(null);

    // If not playing yet, load JamTrack
    if (!jamTrackState.isPlaying) {
      await dispatch(loadJamTrack({
        jamTrack,
        mixdownId: selectedMixdownId,
        autoPlay: true,
        jamClient
      })).unwrap();
    } else if (jamTrackState.isPaused) {
      // Resume from pause
      await jamClient.JamTrackResume(fqIdRef.current);

      // Apply pending seek if exists (UAT-003 fix)
      if (pendingSeekRef.current !== null) {
        await jamClient.JamTrackSeekMs(fqIdRef.current, pendingSeekRef.current);
        pendingSeekRef.current = null;
      }

      dispatch(setJamTrackState({ isPaused: false, isPlaying: true }));
    }
  } catch (err) {
    console.error('[JamTrack] Play error:', err);
    setError({ type: 'playback', message: 'Failed to play JamTrack' });
  } finally {
    if (mountedRef.current) {
      setIsOperating(false);
    }
  }
}, [isOperating, jamClient, jamTrack, jamTrackState, selectedMixdownId, dispatch]);
```

**2. handlePause:**
```javascript
const handlePause = useCallback(async () => {
  if (isOperating || !jamClient || !fqIdRef.current) return;

  try {
    setIsOperating(true);
    setError(null);

    await jamClient.JamTrackPause(fqIdRef.current);
    dispatch(setJamTrackState({ isPaused: true, isPlaying: false }));
  } catch (err) {
    console.error('[JamTrack] Pause error:', err);
    setError({ type: 'playback', message: 'Failed to pause JamTrack' });
  } finally {
    if (mountedRef.current) {
      setIsOperating(false);
    }
  }
}, [isOperating, jamClient, dispatch]);
```

**3. handleStop:**
```javascript
const handleStop = useCallback(async () => {
  if (isOperating || !jamClient || !fqIdRef.current) return;

  try {
    setIsOperating(true);
    setError(null);

    await jamClient.JamTrackStop(fqIdRef.current);
    dispatch(setJamTrackState({
      isPlaying: false,
      isPaused: false,
      currentPositionMs: 0
    }));
  } catch (err) {
    console.error('[JamTrack] Stop error:', err);
    setError({ type: 'playback', message: 'Failed to stop JamTrack' });
  } finally {
    if (mountedRef.current) {
      setIsOperating(false);
    }
  }
}, [isOperating, jamClient, dispatch]);
```

**Add refs:**
```javascript
const pendingSeekRef = useRef(null);
```

**Add buttons to render (replace placeholder):**
```javascript
return (
  <div className="jamtrack-player">
    {error && (
      <div style={{ background: error.type === 'file' || error.type === 'network' ? '#fee' : '#ffd', padding: '10px' }}>
        {error.message}
        <button onClick={() => setError(null)}>Dismiss</button>
      </div>
    )}

    {downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && (
      <div>
        <p>Download: {downloadState.state} ({downloadState.progress}%)</p>
      </div>
    )}

    <div>
      <button onClick={handlePlay} disabled={isOperating || isLoadingSync}>
        {jamTrackState.isPaused ? 'Resume' : 'Play'}
      </button>
      <button onClick={handlePause} disabled={isOperating || !jamTrackState.isPlaying}>
        Pause
      </button>
      <button onClick={handleStop} disabled={isOperating || (!jamTrackState.isPlaying && !jamTrackState.isPaused)}>
        Stop
      </button>
    </div>

    <div>
      <p>Position: {jamTrackState.currentPositionMs}ms / Duration: {jamTrackState.durationMs}ms</p>
    </div>
  </div>
);
```

**Follow Phase 3 patterns:**
- isOperating flag prevents rapid clicks
- useCallback for all handlers
- try/catch with user-friendly error messages
- mountedRef check before setState
- Disabled buttons during operations
  </action>
  <verify>
1. grep "const handlePlay" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists)
2. grep "const handlePause" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists)
3. grep "pendingSeekRef" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (UAT-003 fix ref exists)
4. grep "disabled={isOperating" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (buttons disabled during operations)
  </verify>
  <done>
- 3 playback handlers implemented (handlePlay, handlePause, handleStop)
- All handlers use useCallback for performance
- UAT-003 fix pattern applied (pendingSeekRef for pause-seek-resume)
- Error handling with typed errors (file/network red, playback yellow)
- Buttons disabled correctly (isOperating, isLoadingSync, playback state)
- Buttons rendered in UI with proper labels
  </done>
</task>

<task type="auto">
  <name>Task 2: Implement visibility-aware polling</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Add polling logic following Phase 3 Backing Track pattern:

**1. Add polling effect:**
```javascript
// Polling for position and duration
useEffect(() => {
  if (!jamClient || !fqIdRef.current || !jamTrackState.isPlaying) return;

  // Check if tab is visible
  const isVisible = document.visibilityState === 'visible';
  const pollInterval = isVisible ? 500 : 2000;

  const intervalId = setInterval(async () => {
    try {
      // Fetch position and duration
      const positionStr = await jamClient.JamTrackGetPositionMs(fqIdRef.current);
      const durationStr = await jamClient.JamTrackGetDurationMs(fqIdRef.current);

      const position = parseInt(positionStr, 10);
      const duration = parseInt(durationStr, 10);

      // Conditional update: only update if values changed
      if (
        position !== jamTrackState.currentPositionMs ||
        duration !== jamTrackState.durationMs
      ) {
        dispatch(setJamTrackState({
          currentPositionMs: position,
          durationMs: duration,
          lastUpdate: Date.now()
        }));
      }

      // End-of-track handling
      if (duration > 0 && position >= duration) {
        await jamClient.JamTrackStop(fqIdRef.current);
        dispatch(setJamTrackState({
          isPlaying: false,
          isPaused: false,
          currentPositionMs: 0
        }));
      }
    } catch (err) {
      console.error('[JamTrack] Polling error:', err);
      // Don't setError - polling errors are non-critical
    }
  }, pollInterval);

  return () => clearInterval(intervalId);
}, [jamClient, jamTrackState.isPlaying, jamTrackState.currentPositionMs, jamTrackState.durationMs, dispatch]);

// Listen for visibility changes to recreate interval
useEffect(() => {
  const handleVisibilityChange = () => {
    // Trigger polling effect to restart with new interval
    // This is handled by the dependency array
  };

  document.addEventListener('visibilitychange', handleVisibilityChange);
  return () => document.removeEventListener('visibilitychange', handleVisibilityChange);
}, []);
```

**2. Add formatTime utility:**
```javascript
// Helper: Format milliseconds to MM:SS
const formatTime = (ms) => {
  if (!ms || isNaN(ms)) return '00:00';
  const totalSeconds = Math.floor(ms / 1000);
  const minutes = Math.floor(totalSeconds / 60);
  const seconds = totalSeconds % 60;
  return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`;
};
```

**3. Update time display in render:**
```javascript
<p>
  {formatTime(jamTrackState.currentPositionMs)} / {formatTime(jamTrackState.durationMs)}
</p>
```

**Follow Phase 3 patterns:**
- 500ms when visible, 2000ms when hidden
- parseInt before using position/duration strings
- Conditional update (only if values changed)
- End-of-track detection and reset
- No error state for polling failures (non-critical)
  </action>
  <verify>
1. grep "pollInterval = isVisible ? 500 : 2000" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (visibility-aware)
2. grep "parseInt(positionStr, 10)" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (parseInt before use)
3. grep "position !== jamTrackState.currentPositionMs" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (conditional update)
4. grep "formatTime" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (utility exists)
  </verify>
  <done>
- Visibility-aware polling implemented (500ms visible, 2000ms hidden)
- Polling fetches position and duration via jamClient
- parseInt applied before using string values
- Conditional state updates (only when values change)
- End-of-track handling (stop and reset)
- formatTime utility for MM:SS display
- Time display updated in render
- visibilitychange listener updates polling interval
  </done>
</task>

<task type="auto">
  <name>Task 3: Implement seek slider</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Add seek slider following Phase 3 Backing Track pattern:

**1. handleSeek:**
```javascript
const handleSeek = useCallback(async (newPositionMs) => {
  if (isOperating || !jamClient || !fqIdRef.current) return;

  try {
    setError(null);

    // UAT-003 fix: if paused, store pending seek
    if (jamTrackState.isPaused) {
      pendingSeekRef.current = newPositionMs;
      dispatch(setJamTrackState({ currentPositionMs: newPositionMs }));
      return;
    }

    // If playing, seek immediately
    await jamClient.JamTrackSeekMs(fqIdRef.current, newPositionMs);
    dispatch(setJamTrackState({ currentPositionMs: newPositionMs }));
  } catch (err) {
    console.error('[JamTrack] Seek error:', err);
    setError({ type: 'playback', message: 'Failed to seek' });
  }
}, [isOperating, jamClient, jamTrackState.isPaused, dispatch]);
```

**2. Add slider to render:**
```javascript
<div>
  <input
    type="range"
    min="0"
    max={jamTrackState.durationMs || 100}
    value={jamTrackState.currentPositionMs || 0}
    onChange={(e) => handleSeek(parseInt(e.target.value, 10))}
    disabled={isOperating || !jamTrackState.durationMs}
    style={{ width: '300px' }}
  />
</div>
```

**UAT-003 fix pattern:**
- If paused: store in pendingSeekRef, update UI immediately
- On resume (handlePlay): apply pendingSeekRef if set, then clear
- If playing: seek immediately via jamClient

**Follow Phase 3 patterns:**
- handleSeek wrapped in useCallback
- Disabled during operations and before duration loaded
- parseInt on slider value
- Error handling with user feedback
  </action>
  <verify>
1. grep "const handleSeek" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists)
2. grep "pendingSeekRef.current = newPositionMs" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (UAT-003 fix)
3. grep "type=\"range\"" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (slider exists)
4. grep "disabled={isOperating" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (slider disabled correctly)
  </verify>
  <done>
- handleSeek implemented with useCallback
- UAT-003 fix applied (pending seek while paused, apply on resume)
- Seek slider rendered with correct min/max/value
- Slider disabled during operations and before duration loaded
- onChange parses int before passing to handler
- Error handling for seek failures
- Works while playing (immediate seek) and while paused (pending seek)
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] All 3 playback handlers implemented (play/pause/stop)
- [ ] Visibility-aware polling working (500ms/2000ms)
- [ ] Position and duration update in real-time
- [ ] Seek slider functional with UAT-003 fix
- [ ] No errors when using controls
</verification>

<success_criteria>

- Playback controls complete: play/pause/stop with jamClient integration
- Visibility-aware polling: 500ms visible, 2000ms hidden, conditional updates
- Seek slider functional with UAT-003 fix (pending seek while paused)
- End-of-track handling (stop and reset when position >= duration)
- formatTime utility for MM:SS display
- All handlers use useCallback for performance
- Error handling with user-friendly messages
- Buttons and slider disabled appropriately
- Player matches Backing Track feel and quality
</success_criteria>

<output>
After completion, create `.planning/phases/05-jamtrack-implementation/05-03-SUMMARY.md`
</output>
