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

<objective>
Implement mixdown selection and download/sync UI for JamTrack player.

Purpose: Allow users to browse and select mixdowns (master/custom/stems), display download progress with 6-state sync machine UI, and handle download errors with retry capability.
Output: Complete mixdown picker and download UI with progress tracking and error handling.
</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-03-SUMMARY.md
@.planning/phases/04-jamtrack-research-design/COMPONENT_DESIGN.md
@.planning/phases/04-jamtrack-research-design/REDUX_DESIGN.md
@.planning/codebase/JAMTRACK_API.md
@.planning/codebase/JAMTRACK_LEGACY.md
@jam-ui/src/components/client/JKSessionJamTrackPlayer.js

**From Phase 4 design:**
- Mixdown types: master (single audio), custom-mix (merged stems), stem (single instrument)
- Mixdown picker shows hierarchy: master at top, then custom mixes, then stems
- Download/sync states: idle → checking → downloading → keying → synchronized → error
- pickMyPackage() logic filters by file_type == 'ogg', encrypt_type == 'jkz', sample_rate match
- Download progress: percentage (0-100%), step indicator (X of Y steps)
- Cancel button: calls JamTrackCancelDownload()
- Retry button: re-triggers downloadJamTrack thunk

**From legacy implementation:**
- Mixdown API: JamTrackGetMixdowns(jamTrackId) returns array
- Each mixdown has: id, name, type, packageId
- Master mixdown: type 'master', name usually 'Master Mix'
- Custom mixes: type 'custom', user-created
- Stems: type 'stem', individual instruments
</context>

<tasks>

<task type="auto">
  <name>Task 1: Fetch and organize mixdowns</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Add mixdown fetching logic to initialization:

**1. Fetch mixdowns in initializePlayer (after check sync):**
```javascript
// After checkJamTrackSync and before loadJamTrack...

// Fetch available mixdowns
try {
  const mixdowns = await jamClient.JamTrackGetMixdowns(jamTrack.id);

  if (mixdowns && mixdowns.length > 0) {
    // Organize into hierarchy: master, custom mixes, stems
    const organized = {
      master: mixdowns.find(m => m.type === 'master') || null,
      customMixes: mixdowns.filter(m => m.type === 'custom' || m.type === 'custom-mix'),
      stems: mixdowns.filter(m => m.type === 'stem')
    };

    dispatch(setAvailableMixdowns(mixdowns));

    // Set initial mixdown if not already selected
    if (!selectedMixdownId) {
      const defaultMixdown = organized.master || (mixdowns[0] || null);
      if (defaultMixdown) {
        setSelectedMixdownId(defaultMixdown.id);
        dispatch(setActiveMixdown(defaultMixdown));
      }
    } else {
      // Find and set the initially selected mixdown
      const mixdown = mixdowns.find(m => m.id === selectedMixdownId);
      if (mixdown) {
        dispatch(setActiveMixdown(mixdown));
      }
    }
  }
} catch (err) {
  console.error('[JamTrack] Failed to fetch mixdowns:', err);
  // Non-fatal - can still play default mixdown
}
```

**2. Add import:**
```javascript
import { setAvailableMixdowns, setActiveMixdown } from '../../store/features/activeSessionSlice';
```

**Follow patterns:**
- Fetch mixdowns during initialization (not on every render)
- Organize into master/customMixes/stems hierarchy
- Set default mixdown if none selected (prefer master)
- Non-fatal error (can still play without explicit selection)
- Store in Redux for shared access
  </action>
  <verify>
1. grep "JamTrackGetMixdowns" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (API call exists)
2. grep "organized = {" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (hierarchy created)
3. grep "dispatch(setAvailableMixdowns" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (stored in Redux)
4. grep "import.*setAvailableMixdowns" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (import added)
  </verify>
  <done>
- Mixdowns fetched via JamTrackGetMixdowns during initialization
- Organized into master/customMixes/stems hierarchy
- Default mixdown selected (prefer master, fallback to first available)
- availableMixdowns stored in Redux
- activeMixdown set in Redux
- Non-fatal error handling (can play without explicit selection)
  </done>
</task>

<task type="auto">
  <name>Task 2: Implement mixdown picker UI</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Add mixdown picker UI and selection logic:

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

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

    // Find the new mixdown
    const mixdown = availableMixdowns.find(m => m.id === mixdownId);
    if (!mixdown) {
      throw new Error('Mixdown not found');
    }

    // Update local state
    setSelectedMixdownId(mixdownId);
    dispatch(setActiveMixdown(mixdown));

    // If currently playing, stop and restart with new mixdown
    if (jamTrackState.isPlaying || jamTrackState.isPaused) {
      await jamClient.JamTrackStop(fqIdRef.current);

      await dispatch(loadJamTrack({
        jamTrack,
        mixdownId,
        autoPlay: true,
        jamClient
      })).unwrap();
    }

  } catch (err) {
    console.error('[JamTrack] Mixdown change error:', err);
    setError({ type: 'playback', message: 'Failed to change mixdown' });
  } finally {
    if (mountedRef.current) {
      setIsOperating(false);
    }
  }
}, [isOperating, jamClient, jamTrack, jamTrackState, availableMixdowns, dispatch]);
```

**2. Add mixdown picker to render (after playback controls):**
```javascript
{availableMixdowns.length > 0 && (
  <div style={{ marginTop: '10px' }}>
    <label>Mixdown:</label>
    <select
      value={selectedMixdownId || ''}
      onChange={(e) => handleMixdownChange(parseInt(e.target.value, 10))}
      disabled={isOperating || isLoadingSync}
    >
      {availableMixdowns
        .slice() // Don't mutate original array
        .sort((a, b) => {
          // Sort order: master first, then custom mixes, then stems
          if (a.type === 'master') return -1;
          if (b.type === 'master') return 1;
          if (a.type === 'custom' || a.type === 'custom-mix') return -1;
          if (b.type === 'custom' || b.type === 'custom-mix') return 1;
          return a.name.localeCompare(b.name);
        })
        .map(mixdown => (
          <option key={mixdown.id} value={mixdown.id}>
            {mixdown.type === 'master' && '🎵 '}
            {(mixdown.type === 'custom' || mixdown.type === 'custom-mix') && '🎨 '}
            {mixdown.type === 'stem' && '🎸 '}
            {mixdown.name}
          </option>
        ))
      }
    </select>
  </div>
)}
```

**Follow patterns:**
- useCallback for handler
- Stop current playback before changing mixdown
- Restart playback with new mixdown if was playing
- Disabled during operations
- Sorted display: master → custom mixes → stems
- Visual indicators (emojis) for mixdown types
  </action>
  <verify>
1. grep "const handleMixdownChange" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists)
2. grep "<select" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (picker UI exists)
3. grep "sort((a, b)" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (hierarchy sorting)
4. grep "🎵" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (visual indicators)
  </verify>
  <done>
- handleMixdownChange implemented with useCallback
- Mixdown change stops current playback and restarts with new mixdown
- Mixdown picker dropdown rendered
- Sorted display: master → custom mixes → stems (alphabetical within types)
- Visual indicators for mixdown types (🎵 master, 🎨 custom, 🎸 stem)
- Disabled during operations and loading
- selectedMixdownId updated in local state
- activeMixdown updated in Redux
  </done>
</task>

<task type="auto">
  <name>Task 3: Implement download/sync UI</name>
  <files>jam-ui/src/components/client/JKSessionJamTrackPlayer.js</files>
  <action>
Add download/sync UI with 6-state machine visualization:

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

  try {
    await jamClient.JamTrackCancelDownload(fqIdRef.current);
    dispatch(setDownloadState({ state: 'idle', progress: 0 }));
  } catch (err) {
    console.error('[JamTrack] Cancel download error:', err);
    setError({ type: 'download', message: 'Failed to cancel download' });
  }
}, [jamClient, dispatch]);
```

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

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

    // Clear error state
    dispatch(setDownloadState({ state: 'idle', error: null }));

    // Retry load (will trigger download if not synced)
    await dispatch(loadJamTrack({
      jamTrack,
      mixdownId: selectedMixdownId,
      autoPlay: false,
      jamClient
    })).unwrap();

  } catch (err) {
    console.error('[JamTrack] Retry download error:', err);
    setError({ type: 'download', message: 'Retry failed' });
  } finally {
    if (mountedRef.current) {
      setIsOperating(false);
    }
  }
}, [isOperating, jamClient, jamTrack, selectedMixdownId, dispatch]);
```

**3. Add download UI to render (above playback controls):**
```javascript
{/* Download/Sync State Machine UI */}
{downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && (
  <div style={{ background: '#f0f8ff', padding: '15px', marginBottom: '10px', border: '1px solid #cce7ff' }}>
    <h4>
      {downloadState.state === 'checking' && 'Checking sync status...'}
      {downloadState.state === 'downloading' && 'Downloading JamTrack...'}
      {downloadState.state === 'keying' && 'Requesting decryption keys...'}
      {downloadState.state === 'error' && 'Download Failed'}
    </h4>

    {downloadState.state === 'downloading' && (
      <div>
        <progress
          value={downloadState.progress}
          max="100"
          style={{ width: '100%', height: '20px' }}
        />
        <p>{downloadState.progress}%</p>
        {downloadState.totalSteps > 0 && (
          <p>Step {downloadState.currentStep} of {downloadState.totalSteps}</p>
        )}
        <button onClick={handleCancelDownload}>Cancel</button>
      </div>
    )}

    {downloadState.state === 'keying' && (
      <div>
        <p>Finalizing download...</p>
      </div>
    )}

    {downloadState.state === 'error' && (
      <div style={{ color: '#d00' }}>
        <p>{downloadState.error?.message || 'Download failed'}</p>
        <button onClick={handleRetryDownload}>Retry</button>
      </div>
    )}
  </div>
)}
```

**Follow patterns:**
- 6 states visualized: idle (hidden), checking, downloading, keying, synchronized (hidden), error
- Progress bar for downloading state (0-100%)
- Step indicator when available (X of Y steps)
- Cancel button during download
- Retry button on error
- Clear visual feedback for each state
  </action>
  <verify>
1. grep "const handleCancelDownload" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists)
2. grep "const handleRetryDownload" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists)
3. grep "<progress" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (progress bar exists)
4. grep "downloadState.state === 'checking'" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (state machine UI)
  </verify>
  <done>
- 6-state sync machine UI implemented (checking, downloading, keying, error states shown)
- Progress bar shows download percentage (0-100%)
- Step indicator shows "Step X of Y" when available
- Cancel button calls JamTrackCancelDownload during download
- Retry button on error re-triggers loadJamTrack
- Clear visual feedback for each state
- Error state shows error message and retry option
- idle and synchronized states hidden (normal playback)
  </done>
</task>

</tasks>

<verification>
Before declaring plan complete:
- [ ] Mixdowns fetched and organized during initialization
- [ ] Mixdown picker displays master/custom/stems hierarchy
- [ ] Mixdown selection changes playback
- [ ] Download UI shows all 6 states correctly
- [ ] Progress bar and step indicator update during download
- [ ] Cancel and retry buttons work
</verification>

<success_criteria>

- Mixdowns fetched via JamTrackGetMixdowns and organized into hierarchy
- Mixdown picker dropdown with sorted display (master → custom → stems)
- handleMixdownChange stops/restarts playback with new mixdown
- Download/sync UI displays 6-state machine (checking, downloading, keying, error)
- Progress bar shows 0-100% during download
- Step indicator shows current/total steps
- Cancel button works during download
- Retry button works on error
- Visual indicators for mixdown types
- All handlers use useCallback
- Error handling for mixdown change, cancel, retry
</success_criteria>

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