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

<objective>
Implement 6 async thunks for JamTrack operations, add 3 WebSocket handlers, and create component skeleton.

Purpose: Build the async operation layer (download, sync, load, seek, close) with WebSocket real-time updates, then create the JKSessionJamTrackPlayer component shell with initialization/cleanup patterns.
Output: Complete async infrastructure + skeleton component ready for playback controls.
</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-01-SUMMARY.md
@.planning/phases/04-jamtrack-research-design/04-02-SUMMARY.md
@.planning/phases/04-jamtrack-research-design/REDUX_DESIGN.md
@.planning/phases/04-jamtrack-research-design/COMPONENT_DESIGN.md
@.planning/codebase/CONVENTIONS.md
@.planning/codebase/JAMTRACK_API.md
@.planning/codebase/JAMTRACK_REACT_PATTERNS.md
@jam-ui/src/store/features/mediaSlice.js
@jam-ui/src/components/client/JKSessionBackingTrackPlayer.js

**Established patterns:**
- jamClient methods return Promises (always async/await)
- jamClient returns strings for position/duration (always parseInt before math)
- fqId format: `{jamTrackId}-${sampleRate === 48 ? '48' : '44'}`
- Thunks use rejectWithValue for error handling
- WebSocket handlers dispatch Redux actions
- Component initialization checks sync state before playing
- Cleanup on unmount prevents stale state

**From Phase 4 design:**
- 6 async thunks: loadJamTrack (enhanced), downloadJamTrack, checkJamTrackSync, loadJMEP, seekJamTrack, closeJamTrack
- 3 WebSocket handlers: MIXER_CHANGES (extend), JAM_TRACK_CHANGES (new), MIXDOWN_CHANGES (new)
- JKSessionJamTrackPlayer props: isOpen, onClose, isPopup, jamTrack, jamClient, session, currentUser, initialMixdownId, autoPlay
- Initialization pattern: build fqId → check sync → download if needed → load JMEP → play
</context>

<tasks>

<task type="auto">
  <name>Task 1: Implement 6 async thunks in mediaSlice</name>
  <files>jam-ui/src/store/features/mediaSlice.js</files>
  <action>
Add 6 new thunks to mediaSlice.js (after existing thunks, before slice definition):

**1. Enhanced loadJamTrack (REPLACE existing):**
```javascript
export const loadJamTrack = createAsyncThunk(
  'media/loadJamTrack',
  async ({ jamTrack, mixdownId = null, autoPlay = false, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      // Build fqId
      const sampleRate = await jamClient.GetSampleRate();
      const fqId = `${jamTrack.id}-${sampleRate === 48 ? '48' : '44'}`;

      // Check sync state
      const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);

      // If not synchronized, trigger download
      if (!trackDetail || !trackDetail.key_state || trackDetail.key_state !== 'AVAILABLE') {
        await dispatch(downloadJamTrack({ jamTrack, mixdownId, fqId, jamClient })).unwrap();
      }

      // Load JMEP if present
      if (jamTrack.jmep) {
        await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
      }

      // Play with fqId (not jamTrack.id)
      const result = await jamClient.JamTrackPlay(fqId);
      if (!result) {
        throw new Error('Unable to play JamTrack');
      }

      return { jamTrack, fqId, result };
    } catch (error) {
      return rejectWithValue(error.message || error);
    }
  }
);
```

**2. downloadJamTrack:**
```javascript
export const downloadJamTrack = createAsyncThunk(
  'media/downloadJamTrack',
  async ({ jamTrack, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      dispatch(setDownloadState({
        jamTrackId: jamTrack.id,
        mixdownId,
        fqId,
        state: 'downloading',
        progress: 0
      }));

      // Get package ID (simplified - use first available package)
      // Real implementation would use pickMyPackage() logic from legacy
      const mixdowns = await jamClient.JamTrackGetMixdowns(jamTrack.id);
      const packageId = mixdowns && mixdowns.length > 0 ? mixdowns[0].packageId : null;

      if (!packageId) {
        throw new Error('No mixdown package available');
      }

      // Set up global callbacks for native client
      window.jamTrackDownloadProgress = (percent) => {
        dispatch(setDownloadState({ progress: percent }));
      };

      window.jamTrackDownloadSuccess = () => {
        dispatch(setDownloadState({ state: 'keying' }));
      };

      window.jamTrackDownloadFail = (error) => {
        dispatch(setDownloadState({ state: 'error', error: { type: 'download', message: error } }));
      };

      // Initiate download (callbacks as string names)
      await jamClient.JamTrackDownload(
        jamTrack.id,
        packageId,
        jamTrack.userId,
        'jamTrackDownloadProgress',
        'jamTrackDownloadSuccess',
        'jamTrackDownloadFail'
      );

      // Request keys
      await jamClient.JamTrackKeysRequest();

      dispatch(setDownloadState({ state: 'synchronized' }));
      return { jamTrackId: jamTrack.id, fqId };
    } catch (error) {
      dispatch(setDownloadState({ state: 'error', error: { type: 'download', message: error.message } }));
      return rejectWithValue(error.message || error);
    }
  }
);
```

**3. checkJamTrackSync:**
```javascript
export const checkJamTrackSync = createAsyncThunk(
  'media/checkJamTrackSync',
  async ({ jamTrack, jamClient }, { rejectWithValue }) => {
    try {
      const sampleRate = await jamClient.GetSampleRate();
      const fqId = `${jamTrack.id}-${sampleRate === 48 ? '48' : '44'}`;
      const trackDetail = await jamClient.JamTrackGetTrackDetail(fqId);
      return { fqId, trackDetail, isSynchronized: trackDetail && trackDetail.key_state === 'AVAILABLE' };
    } catch (error) {
      return rejectWithValue(error.message || error);
    }
  }
);
```

**4. loadJMEP:**
```javascript
export const loadJMEP = createAsyncThunk(
  'media/loadJMEP',
  async ({ jamTrack, fqId, jamClient }, { rejectWithValue }) => {
    try {
      if (!jamTrack.jmep) {
        return { loaded: false };
      }
      await jamClient.JamTrackLoadJmep(fqId, jamTrack.jmep);
      return { loaded: true, fqId };
    } catch (error) {
      return rejectWithValue(error.message || error);
    }
  }
);
```

**5. seekJamTrack:**
```javascript
export const seekJamTrack = createAsyncThunk(
  'media/seekJamTrack',
  async ({ fqId, positionMs, jamClient }, { getState, rejectWithValue }) => {
    try {
      // Apply UAT-003 fix pattern: if paused, store pending seek
      const { media } = getState();
      if (media.jamTrackState.isPaused) {
        // Store pending seek, will be applied on resume
        return { pendingSeek: positionMs };
      }

      await jamClient.JamTrackSeekMs(fqId, positionMs);
      return { positionMs };
    } catch (error) {
      return rejectWithValue(error.message || error);
    }
  }
);
```

**6. closeJamTrack:**
```javascript
export const closeJamTrack = createAsyncThunk(
  'media/closeJamTrack',
  async ({ fqId, jamClient }, { dispatch, rejectWithValue }) => {
    try {
      // Stop playback if playing
      const positionStr = await jamClient.JamTrackGetPositionMs(fqId);
      const position = parseInt(positionStr, 10);
      if (position > 0) {
        await jamClient.JamTrackStop(fqId);
      }

      // Clear Redux state
      dispatch(clearJamTrackState());
      dispatch(clearDownloadState());

      return { closed: true };
    } catch (error) {
      return rejectWithValue(error.message || error);
    }
  }
);
```

**In extraReducers, add handlers for these thunks** (pending/fulfilled/rejected for loading states).

**Import setDownloadState, clearJamTrackState, clearDownloadState** from reducers section.
  </action>
  <verify>
1. grep "export const downloadJamTrack" jam-ui/src/store/features/mediaSlice.js (thunk exists)
2. grep "export const seekJamTrack" jam-ui/src/store/features/mediaSlice.js (thunk exists)
3. grep "jamTrackDownloadProgress" jam-ui/src/store/features/mediaSlice.js (callback setup exists)
4. Count thunks: should have loadJamTrack + downloadJamTrack + checkJamTrackSync + loadJMEP + seekJamTrack + closeJamTrack = 6 new thunks
  </verify>
  <done>
- 6 async thunks implemented with correct signatures
- All thunks use fqId format (not jamTrack.id)
- downloadJamTrack sets up global callbacks for native client
- seekJamTrack applies UAT-003 fix pattern (pending seek while paused)
- closeJamTrack cleans up state properly
- extraReducers handle loading states for all thunks
- No TypeScript/linter errors
  </done>
</task>

<task type="auto">
  <name>Task 2: Add 3 WebSocket handlers</name>
  <files>jam-ui/src/hooks/useWebSocketSubscription.js (or equivalent WebSocket handler file)</files>
  <action>
Find the WebSocket handler file where MIXER_CHANGES and other message handlers are registered. Look for files in:
- jam-ui/src/hooks/useWebSocketSubscription.js
- jam-ui/src/hooks/useWebSocket.js
- jam-ui/src/contexts/WebSocketContext.js
- jam-ui/src/store/websocketMiddleware.js

**Once found, add/extend 3 handlers:**

**1. Extend MIXER_CHANGES handler:**
```javascript
case 'MIXER_CHANGES':
  // Existing backing track mixer logic...

  // NEW: Handle JamTrack mixdown changes
  if (message.jamTrackMixdowns) {
    dispatch(setAvailableMixdowns(message.jamTrackMixdowns));
  }
  break;
```

**2. Add JAM_TRACK_CHANGES handler (new):**
```javascript
case 'JAM_TRACK_CHANGES':
  dispatch(updateJamTrackState({
    isPlaying: message.jamTrackState?.isPlaying || false,
    isPaused: message.jamTrackState?.isPaused || false,
    currentPositionMs: parseInt(message.jamTrackState?.currentPositionMs || '0', 10),
    lastUpdate: Date.now()
  }));
  break;
```

**3. Add MIXDOWN_CHANGES handler (new):**
```javascript
case 'MIXDOWN_CHANGES':
  if (message.mixdownPackage) {
    dispatch(setDownloadState({
      currentStep: message.mixdownPackage.current_packaging_step || 0,
      totalSteps: message.mixdownPackage.packaging_steps || 0
    }));
  }
  break;
```

**Import actions at top:**
```javascript
import { updateJamTrackState, setDownloadState } from '../store/features/mediaSlice';
import { setAvailableMixdowns } from '../store/features/activeSessionSlice';
```

**If WebSocket file not found,** document in commit message that handlers need to be added when WebSocket integration point is identified.
  </action>
  <verify>
1. grep "JAM_TRACK_CHANGES" (handler added)
2. grep "MIXDOWN_CHANGES" (handler added)
3. grep "updateJamTrackState" (dispatch call exists)
4. grep "import.*updateJamTrackState" (import added)
  </verify>
  <done>
- 3 WebSocket handlers added/extended (MIXER_CHANGES, JAM_TRACK_CHANGES, MIXDOWN_CHANGES)
- Handlers dispatch correct Redux actions
- All imports added
- Handlers parse message data correctly (parseInt for numbers, handle nulls)
  </done>
</task>

<task type="auto">
  <name>Task 3: Create JKSessionJamTrackPlayer component skeleton</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Create new file following JKSessionBackingTrackPlayer.js patterns:

```javascript
import React, { useState, useEffect, useCallback, useRef } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {
  loadJamTrack,
  checkJamTrackSync,
  closeJamTrack,
  setJamTrackState
} from '../../store/features/mediaSlice';
import { setOpenJamTrack, clearOpenJamTrack } from '../../store/features/sessionUISlice';

const JKSessionJamTrackPlayer = ({
  isOpen,
  onClose,
  isPopup = false,
  jamTrack,
  jamClient,
  session,
  currentUser,
  initialMixdownId = null,
  autoPlay = false
}) => {
  // Local state
  const [error, setError] = useState(null);
  const [isLoadingSync, setIsLoadingSync] = useState(false);
  const [isOperating, setIsOperating] = useState(false);
  const [selectedMixdownId, setSelectedMixdownId] = useState(initialMixdownId);

  // Redux state
  const dispatch = useDispatch();
  const jamTrackState = useSelector(state => state.media.jamTrackState);
  const downloadState = useSelector(state => state.media.downloadState);
  const availableMixdowns = useSelector(state => state.activeSession.availableMixdowns);

  // Refs
  const fqIdRef = useRef(null);
  const mountedRef = useRef(true);

  // Helper: Build fqId
  const buildFqId = useCallback(async () => {
    if (!jamClient || !jamTrack) return null;
    const sampleRate = await jamClient.GetSampleRate();
    return `${jamTrack.id}-${sampleRate === 48 ? '48' : '44'}`;
  }, [jamClient, jamTrack]);

  // Initialization
  useEffect(() => {
    if (!isOpen && !isPopup) return;
    if (!jamClient || !jamTrack) return;

    const initializePlayer = async () => {
      try {
        setIsLoadingSync(true);
        setError(null);

        // Build fqId
        const fqId = await buildFqId();
        fqIdRef.current = fqId;

        // Check sync state
        const syncResult = await dispatch(checkJamTrackSync({ jamTrack, jamClient })).unwrap();

        if (!syncResult.isSynchronized) {
          // Download flow will be triggered by loadJamTrack
          console.log('[JamTrack] Not synchronized, will download');
        }

        // Set open in Redux
        dispatch(setOpenJamTrack(jamTrack.id));

        // Load and play if autoPlay
        if (autoPlay) {
          await dispatch(loadJamTrack({
            jamTrack,
            mixdownId: selectedMixdownId,
            autoPlay: true,
            jamClient
          })).unwrap();
        }

      } catch (err) {
        console.error('[JamTrack] Initialization error:', err);
        setError({ type: 'initialization', message: err.message || 'Failed to initialize JamTrack' });
      } finally {
        if (mountedRef.current) {
          setIsLoadingSync(false);
        }
      }
    };

    initializePlayer();
  }, [isOpen, isPopup, jamTrack, jamClient, autoPlay, selectedMixdownId, dispatch, buildFqId]);

  // Cleanup on unmount
  useEffect(() => {
    return () => {
      mountedRef.current = false;

      if (fqIdRef.current && jamClient) {
        dispatch(closeJamTrack({ fqId: fqIdRef.current, jamClient }));
        dispatch(clearOpenJamTrack());
      }
    };
  }, [dispatch, jamClient]);

  // Placeholder render (will be filled in Plan 3)
  if (!isOpen && !isPopup) return null;

  return (
    <div className="jamtrack-player">
      <h3>JamTrack Player (Skeleton)</h3>
      {isLoadingSync && <p>Checking sync status...</p>}
      {error && <p style={{ color: 'red' }}>{error.message}</p>}
      {downloadState.state !== 'idle' && (
        <p>Download State: {downloadState.state} ({downloadState.progress}%)</p>
      )}
      <p>Ready for playback controls (Plan 3)</p>
    </div>
  );
};

export default JKSessionJamTrackPlayer;
```

**Follow conventions:**
- JK prefix (matches JKSessionBackingTrackPlayer)
- Located in jam-ui/src/components/client/
- Uses Redux Toolkit hooks (useDispatch, useSelector)
- Props interface matches Phase 4 design
- Initialization pattern: build fqId → check sync → load if autoPlay
- Cleanup on unmount
- mountedRef prevents state updates after unmount
  </action>
  <verify>
1. ls jam-ui/src/components/client/JKSessionJamTrackPlayer.js (file exists)
2. grep "const JKSessionJamTrackPlayer" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (component defined)
3. grep "buildFqId" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (helper exists)
4. grep "closeJamTrack" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (cleanup logic)
  </verify>
  <done>
- JKSessionJamTrackPlayer.js created with correct structure
- Props interface matches Phase 4 design (11 props)
- Initialization pattern implemented (build fqId → check sync → autoPlay if requested)
- Cleanup on unmount prevents stale state
- Local state for UI (error, isLoadingSync, isOperating, selectedMixdownId)
- Redux state connected (jamTrackState, downloadState, availableMixdowns)
- Placeholder render (will be enhanced in Plan 3)
- No TypeScript/linter errors
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] All 6 async thunks implemented and exported
- [ ] All thunks use fqId format correctly
- [ ] 3 WebSocket handlers added/extended
- [ ] JKSessionJamTrackPlayer component created with initialization/cleanup
- [ ] No errors when importing component or slices
</verification>

<success_criteria>

- 6 async thunks complete: loadJamTrack (enhanced), downloadJamTrack, checkJamTrackSync, loadJMEP, seekJamTrack, closeJamTrack
- 3 WebSocket handlers: MIXER_CHANGES (extended), JAM_TRACK_CHANGES (new), MIXDOWN_CHANGES (new)
- JKSessionJamTrackPlayer component skeleton created with props interface, initialization pattern, cleanup logic
- All code follows Backing Track patterns and Phase 4 design
- Component ready for playback controls (Plan 3)
</success_criteria>

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