# Backing Track Sync Research

**Researched:** 2026-02-25
**Confidence:** HIGH
**Issue:** Backing Track doesn't sync to session screen when opened in popup window

## Executive Summary

The issue is clear: **Backing Track never syncs to the session screen because the condition to display it is never satisfied.**

When a Backing Track is opened via popup:
1. File is opened through `jamClient.SessionOpenBackingTrackFile()`
2. `backingTrackData` is set in Redux (for popup display)
3. **BUT** `mixerHelper.backingTracks` (required for session screen display) is NOT populated

The condition `showBackingTrackPlayer && mixerHelper.backingTracks.length > 0` at line 1434 of JKSessionScreen.js is never true because `mixerHelper.backingTracks` remains empty.

Metronome works because it follows a different pattern with `metronomeState.isOpen` flag and `metronomeTrackMixers` from Redux.

## Key Findings

### 1. Backing Track Components

**JKSessionBackingTrackPlayer** (jam-ui/src/components/client/JKSessionBackingTrackPlayer.js)
- Popup player with controls (play/pause/stop/seek/loop)
- Opened in WindowPortal when user selects file
- Uses `jamClient` for playback control
- Does NOT trigger session screen sync

**JKSessionBackingTrack** (jam-ui/src/components/client/JKSessionBackingTrack.js)
- Session Mix area component (the visual track in session screen)
- Shows mixer controls (VU meter, gain, pan)
- Requires `backingTrack` and `mixers` props
- Only renders when `mixerHelper.backingTracks.length > 0`

### 2. How Metronome Successfully Syncs

**Popup Display:**
- `metronomeState.isOpen` flag (from GlobalContext)
- Renders JKSessionMetronomePlayer in WindowPortal
- Line 1727-1744 in JKSessionScreen.js

**Session Screen Display:**
- Condition: `metronomeState.isOpen && metronomeTrackMixers && metronomeTrackMixers.length > 0`
- Uses `metronomeTrackMixers` from Redux mixersSlice selector
- Line 1463-1488 in JKSessionScreen.js
- Renders JKSessionMetronome component

**Why it works:**
1. `openMetronome()` in useMediaActions.js (line 92):
   - Calls `jamClient.SessionOpenMetronome()`
   - Updates Redux: `updateMediaSummary({ metronomeOpen: true })`
   - Syncs tracks: `dispatch(syncTracksToServer(sessionId, jamClient))`
2. Track sync triggers server update
3. Session refresh fetches mixer data
4. Mixer data includes metronome mixers
5. `metronomeTrackMixers` selector returns populated array
6. JKSessionMetronome renders in Session Mix area

### 3. What's Missing for Backing Track

**Current Flow (BROKEN):**

```
User selects file
  ↓
handleBackingTrackSelected() in JKSessionScreen.js (line 1136)
  ↓
jamClient.SessionOpenBackingTrackFile(result.file, false)
  ↓
dispatch(setBackingTrackData({ backingTrack, session, currentUser }))
  ↓
dispatch(openModal('backingTrack'))
  ↓
WindowPortal renders JKSessionBackingTrackPlayer
  ↓
[STOPS HERE - mixerHelper.backingTracks NEVER POPULATED]
```

**Missing Steps:**
1. No track sync triggered after opening backing track
2. No session refresh to fetch mixer data
3. `mixerHelper.backingTracks` never populated from session data
4. Condition at line 1434 never satisfied

**Compare with Metronome (WORKING):**

```
User opens metronome
  ↓
openMetronome() in useMediaActions.js
  ↓
jamClient.SessionOpenMetronome()
  ↓
dispatch(updateMediaSummary({ metronomeOpen: true }))
  ↓
dispatch(syncTracksToServer(sessionId, jamClient))  ← KEY DIFFERENCE
  ↓
Server updates session with metronome mixer data
  ↓
Session refresh fetches mixers
  ↓
metronomeTrackMixers populated from Redux
  ↓
JKSessionMetronome renders in Session Mix
```

### 4. Redux State Structure

**activeSessionSlice.js:**
- `backingTrackData`: Stores popup player data (file path, session, user)
- Used for: Determining if popup should show
- NOT used for: Session Mix area display

**mediaSlice.js:**
- `backingTracks`: Array of backing track objects with mixers
- Updated by: `setBackingTracks` action from WebSocket or session refresh
- Used for: Session Mix area display via `mixerHelper.backingTracks`

**mixersSlice.js:**
- `backingTrackMixers`: Array of mixer objects for backing tracks
- `mediaSummary.backingTrackOpen`: Boolean flag
- Used for: Mixer controls in session screen

### 5. Data Flow for Session Screen Display

**How data gets to mixerHelper.backingTracks:**

```
Session REST API response
  ↓
useSessionModel.js refreshCurrentSession() (line 695)
  ↓
Extract backing tracks from participants
  ↓
dispatch(setBackingTracks(extractedBackingTracks))
  ↓
mediaSlice.backingTracks updated
  ↓
useMixerHelper.js selects from mediaSlice (line 95)
  ↓
mixerHelper.backingTracks available to components
```

**Current problem:**
When backing track opened via popup, session is not refreshed, so backing tracks are never extracted from participants and `mixerHelper.backingTracks` remains empty.

### 6. Track Sync Service

**trackSyncService.js** (jam-ui/src/services/trackSyncService.js)
- `syncTracksToServer()`: Syncs track state to backend
- Builds payload with tracks, backing_tracks, metronome_open
- Called after opening metronome (line 107 in useMediaActions.js)
- NOT called after opening backing track

**Backend expectation:**
- PUT `/api/sessions/:id/tracks` with track sync payload
- Server updates session participants with backing track info
- Subsequent GET fetches updated mixer data

## Root Cause Analysis

### Why Backing Track Doesn't Sync

**Missing Integration Points:**

1. **No track sync call** in `handleBackingTrackSelected()` (JKSessionScreen.js line 1136)
   - Metronome calls: `dispatch(syncTracksToServer(sessionId, jamClient))`
   - Backing track: Missing this call

2. **Wrong Redux state used for display condition**
   - Uses: `showBackingTrackPlayer` (derived from `backingTrackData`)
   - Should use: `mediaSummary.backingTrackOpen` + `mixerHelper.backingTracks.length > 0`
   - Like metronome uses: `metronomeState.isOpen` + `metronomeTrackMixers.length > 0`

3. **No session refresh after opening**
   - Metronome: Track sync triggers backend update, then session refresh
   - Backing track: No sync, no refresh, no mixer data

4. **useMediaActions.openBackingTrack() exists but not used**
   - File: jam-ui/src/hooks/useMediaActions.js line 41
   - Already implements track sync: `dispatch(syncTracksToServer(sessionId, jamClient))`
   - Already updates media summary: `updateMediaSummary({ backingTrackOpen: true })`
   - **Not called from JKSessionScreen.js** - direct jamClient call used instead

## Recommended Fix Approach

### Option 1: Use Existing useMediaActions Pattern (RECOMMENDED)

**Change in JKSessionScreen.js:**

```javascript
// BEFORE (line 1136):
const handleBackingTrackSelected = async (result) => {
  try {
    await jamClient.SessionOpenBackingTrackFile(result.file, false);
    dispatch(setBackingTrackData({
      backingTrack: result.file,
      session: currentSession,
      currentUser: currentUser
    }));
    dispatch(openModal('backingTrack'));
  } catch (error) {
    toast.error('Failed to open backing track');
  }
};

// AFTER:
const handleBackingTrackSelected = async (result) => {
  try {
    // Use the media action that includes track sync
    await openBackingTrack(result.file);

    // Set up popup data
    dispatch(setBackingTrackData({
      backingTrack: result.file,
      session: currentSession,
      currentUser: currentUser
    }));
    dispatch(openModal('backingTrack'));
  } catch (error) {
    toast.error('Failed to open backing track');
  }
};
```

**Why this works:**
1. `openBackingTrack()` (useMediaActions.js line 41) already:
   - Opens file: `jamClient.SessionOpenBackingTrackFile()`
   - Updates media summary: `updateMediaSummary({ backingTrackOpen: true })`
   - Syncs tracks: `dispatch(syncTracksToServer(sessionId, jamClient))`
2. Track sync triggers backend update
3. Session refresh fetches mixer data
4. `mixerHelper.backingTracks` gets populated
5. Session screen condition satisfied

**Additional change needed in display condition:**

```javascript
// BEFORE (line 1434):
{showBackingTrackPlayer && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && (
  // ...
)}

// AFTER (align with metronome pattern):
{mediaSummary.backingTrackOpen && mixerHelper.backingTracks && mixerHelper.backingTracks.length > 0 && (
  // ...
)}
```

### Option 2: Add Track Sync to Current Flow

If Option 1 causes issues, manually add track sync:

```javascript
const handleBackingTrackSelected = async (result) => {
  try {
    await jamClient.SessionOpenBackingTrackFile(result.file, false);

    // Add track sync (like metronome does)
    if (sessionId && jamClient) {
      console.log('[Track Sync] Backing track opened, syncing tracks');
      dispatch(syncTracksToServer(sessionId, jamClient));
    }

    dispatch(setBackingTrackData({
      backingTrack: result.file,
      session: currentSession,
      currentUser: currentUser
    }));
    dispatch(openModal('backingTrack'));
  } catch (error) {
    toast.error('Failed to open backing track');
  }
};
```

## Pattern Comparison: Metronome vs Backing Track

| Aspect | Metronome (WORKS) | Backing Track (BROKEN) |
|--------|------------------|----------------------|
| Open Action | `openMetronome()` in useMediaActions | Direct `jamClient.SessionOpenBackingTrackFile()` |
| Track Sync | ✅ Called in useMediaActions | ❌ Not called |
| Media Summary Update | ✅ `metronomeOpen: true` | ❌ Not updated |
| Popup State | `metronomeState.isOpen` (GlobalContext) | `backingTrackData` (Redux) |
| Session Screen State | `metronomeTrackMixers` (Redux selector) | `mixerHelper.backingTracks` (never populated) |
| Display Condition | `metronomeState.isOpen && metronomeTrackMixers.length > 0` | `showBackingTrackPlayer && mixerHelper.backingTracks.length > 0` |
| Result | ✅ Shows in both popup and session screen | ❌ Shows only in popup |

## File Locations

### Components
- **Backing Track Player (popup):** `jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
- **Backing Track (session screen):** `jam-ui/src/components/client/JKSessionBackingTrack.js`
- **Metronome Player (popup):** `jam-ui/src/components/client/JKSessionMetronomePlayer.js`
- **Metronome (session screen):** `jam-ui/src/components/client/JKSessionMetronome.js`
- **Session Screen:** `jam-ui/src/components/client/JKSessionScreen.js`

### State Management
- **Media Actions:** `jam-ui/src/hooks/useMediaActions.js`
- **Track Sync Service:** `jam-ui/src/services/trackSyncService.js`
- **Active Session Slice:** `jam-ui/src/store/features/activeSessionSlice.js`
- **Media Slice:** `jam-ui/src/store/features/mediaSlice.js`
- **Mixers Slice:** `jam-ui/src/store/features/mixersSlice.js`
- **Session Model:** `jam-ui/src/hooks/useSessionModel.js`
- **Mixer Helper:** `jam-ui/src/hooks/useMixerHelper.js`

### Key Line Numbers
- **Backing track handler:** JKSessionScreen.js line 1136-1163
- **Backing track display condition:** JKSessionScreen.js line 1434
- **Metronome display condition:** JKSessionScreen.js line 1463
- **openBackingTrack() action:** useMediaActions.js line 41-60
- **syncTracksToServer() service:** trackSyncService.js
- **Session refresh with backing tracks:** useSessionModel.js line 685-696

## Testing Strategy

### 1. Verify Current Behavior (Failing Test)

```javascript
// test/backing-track/backing-track-sync.spec.ts
test('backing track appears in session screen after opening popup', async ({ page }) => {
  await loginToJamUI(page);
  await createAndJoinSession(page);

  // Open backing track from Open menu
  await page.click('[data-testid="open-menu-button"]');
  await page.click('[data-testid="open-backing-track"]');
  await selectBackingTrackFile(page, 'test-audio.mp3');

  // Verify popup appears
  await expect(page.locator('.metronome-player-popup')).toBeVisible();

  // Verify track appears in session screen
  await expect(page.locator('.backingTrack')).toBeVisible();
  await expect(page.locator('.backingTrack .session-track')).toBeVisible();
});
```

### 2. Verify Track Sync Called

```javascript
test('track sync triggered when backing track opened', async ({ page }) => {
  const interceptor = new APIInterceptor();
  interceptor.intercept(page);

  await loginToJamUI(page);
  await createAndJoinSession(page);

  await openBackingTrack(page, 'test-audio.mp3');

  // Verify track sync API called
  const trackSyncCalls = interceptor.getCallsByPath('/api/sessions/*/tracks');
  expect(trackSyncCalls.length).toBeGreaterThan(0);
});
```

### 3. Verify Mixer Data Populated

```javascript
test('backing track mixers populated after sync', async ({ page }) => {
  await loginToJamUI(page);
  const sessionId = await createAndJoinSession(page);

  await openBackingTrack(page, 'test-audio.mp3');

  // Wait for session refresh
  await page.waitForTimeout(1000);

  // Verify mixer data in Redux
  const mixers = await page.evaluate(() => {
    return window.__REDUX_DEVTOOLS_EXTENSION__.store.getState().mixers.backingTrackMixers;
  });

  expect(mixers.length).toBeGreaterThan(0);
});
```

## Conclusion

The fix is straightforward:
1. Use `openBackingTrack()` from useMediaActions instead of direct jamClient call
2. This automatically triggers track sync (like metronome does)
3. Track sync updates backend, session refresh populates mixer data
4. Session screen condition satisfied, backing track appears

**Estimated effort:** 1-2 hours (simple refactor + testing)
**Risk:** Low (following existing working pattern from metronome)
**Impact:** HIGH (fixes critical missing feature)
