# Track Configuration API Implementation Plan

**Feature:** PUT /api/sessions/{id}/tracks - Track Configuration
**Date:** 2026-01-21
**Status:** Planning Phase

---

## 1. Backend API Specification (From Rails Analysis)

### Endpoint
```
PUT /api/sessions/{session_id}/tracks
```

### Purpose
Synchronizes participant's audio track configuration with the server. Updates:
- Active audio tracks (instrument, sound settings)
- Backing tracks
- Metronome open/closed state

### Request Payload (Expected Format)
```json
{
  "client_id": "client-uuid",
  "tracks": [
    {
      "instrument_id": "electric guitar",
      "sound": "stereo",
      "client_track_id": "client-track-guid",
      "client_resource_id": "resource-guid"
    }
  ],
  "backing_tracks": [
    {
      "filename": "backing-track.mp3",
      "client_track_id": "backing-track-guid",
      "client_resource_id": "backing-resource-guid"
    }
  ],
  "metronome_open": false
}
```

### Response
```json
{
  "tracks": [...],
  "backing_tracks": [...]
}
```

### Backend Behavior (Rails)
- Updates `Track` records in database
- Updates `BackingTrack` records
- Updates `Connection.metronome_open` flag
- Updates `MusicSessionUserHistory.instruments`
- Sends WebSocket notification to other participants: `tracks_changed`
- Returns updated track list

### When Called in Legacy
1. **First call:** ~1 second after joining session (initial track setup)
2. **Second call:** ~400ms later (track refinement)
3. **Third call:** ~5 seconds later (final configuration)
4. **Subsequent calls:** When user changes instruments, mixer settings, or opens/closes media

---

## 2. Current jam-ui State (What Exists)

### ✅ API Client Already Exists!

**File:** `src/helpers/rest.js:887-897`

```javascript
export const putTrackSyncChange = (options = {}) => {
  const { id, ...rest } = options;
  return new Promise((resolve, reject) => {
    apiFetch(`/sessions/${id}/tracks`, {
      method: 'PUT',
      body: JSON.stringify(rest)
    })
      .then(response => resolve(response))
      .catch(error => reject(error));
  });
}
```

**Status:** ✅ Already implemented, just not called anywhere!

### ✅ Redux State Management Ready

**Session State:** `src/store/features/activeSessionSlice.js`
- `userTracks` - Participant's tracks in session
- `backingTrackData` - Backing track state
- `jamTrackData` - Jam track state
- Actions: `setUserTracks()`, `setBackingTrackData()`, `setJamTrackData()`

**Mixer State:** `src/store/features/mixersSlice.js`
- Organized by `group_id` (MetronomeGroup: 16, BackingTrackGroup, JamTrackGroup, etc.)
- Actions: `setMetronomeTrackMixers()`, `setBackingTrackMixers()`, `setJamTrackMixers()`
- Selectors: `selectUserTracks`, `selectBackingTracks`, `selectJamTracks`

**Metronome State:** Recently integrated (commit 4d141c93c)
- `selectMetronome` - Metronome track state
- `selectMetronomeSettings` - Configuration

### ✅ Components That Use Tracks

1. **Session Screen:** `src/components/client/JKSessionScreen.js`
   - Main session UI
   - Manages track display and media players

2. **Track Components:**
   - `JKSessionMyTrack.js` - User's audio track
   - `JKSessionRemoteTracks.js` - Remote participant tracks
   - `JKSessionBackingTrackPlayer.js` - Backing track player
   - `JKSessionJamTrackPlayer.js` - Jam track player
   - `JKSessionMetronomePlayer.js` - Metronome player

3. **Hooks:**
   - `useMixerHelper.js` - Mixer management and categorization
   - `useMediaActions.js` - Media control actions
   - `useTrackHelpers.js` - Track information retrieval

---

## 3. What Needs to Be Implemented

### ❌ Missing: Track Sync Logic

**Problem:** The API wrapper exists but is never called.

**Need to implement:**

1. **Track state builder function** - Constructs payload from Redux state
2. **Sync trigger logic** - Determines when to call API
3. **Redux actions** - Update state after API success
4. **Error handling** - Handle API failures gracefully

### ❌ Missing: Integration Points

**Where to call the API:**

1. **On session join** - Initial track configuration
2. **On mixer changes** - When user adjusts audio settings
3. **On media open/close** - When backing track/jam track/metronome toggled
4. **On instrument change** - When user selects different instrument

---

## 4. Implementation Strategy

### Phase 1: Create Track Sync Service

**New file:** `src/services/trackSyncService.js`

**Purpose:**
- Build track sync payload from Redux state
- Call `putTrackSyncChange()` API
- Handle success/error responses
- Dispatch Redux updates

**Functions:**
```javascript
// Build payload from current Redux state
export const buildTrackSyncPayload = (state, sessionId) => {
  const userTracks = selectUserTracks(state);
  const backingTracks = selectBackingTracks(state);
  const metronome = selectMetronome(state);
  const clientId = selectClientId(state);

  return {
    client_id: clientId,
    tracks: userTracks.map(track => ({
      instrument_id: track.instrument_id,
      sound: track.sound,
      client_track_id: track.client_track_id,
      client_resource_id: track.client_resource_id
    })),
    backing_tracks: backingTracks.map(bt => ({
      filename: bt.filename,
      client_track_id: bt.client_track_id,
      client_resource_id: bt.client_resource_id
    })),
    metronome_open: metronome?.isOpen || false
  };
};

// Sync tracks to server
export const syncTracksToServer = (sessionId) => async (dispatch, getState) => {
  const state = getState();
  const payload = buildTrackSyncPayload(state, sessionId);

  try {
    const response = await putTrackSyncChange({ id: sessionId, ...payload });

    // Update Redux with server response
    dispatch(setUserTracks(response.tracks));
    dispatch(setBackingTrackData(response.backing_tracks));

    return response;
  } catch (error) {
    console.error('Track sync failed:', error);
    dispatch(showError('Failed to sync tracks'));
    throw error;
  }
};
```

### Phase 2: Add Sync Triggers

**Location:** `src/hooks/useSessionModel.js` or create `src/hooks/useTrackSync.js`

**Trigger points:**

1. **On session join:**
```javascript
// In useSessionModel or JKSessionScreen
useEffect(() => {
  if (sessionJoined && clientId) {
    // Initial track sync after joining
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 1000);
  }
}, [sessionJoined, clientId]);
```

2. **On mixer changes:**
```javascript
// In useMixerHelper or mixer components
const handleMixerChange = (trackId, setting, value) => {
  // Update local mixer state
  updateMixer(trackId, setting, value);

  // Debounced sync to server
  debouncedTrackSync();
};
```

3. **On media toggle:**
```javascript
// In useMediaActions
export const toggleBackingTrack = (trackId, isOpen) => async (dispatch) => {
  // Update local state
  dispatch(setBackingTrackOpen({ trackId, isOpen }));

  // Sync to server
  await dispatch(syncTracksToServer(sessionId));
};
```

4. **On metronome toggle:**
```javascript
// In metronome components
const handleMetronomeToggle = () => {
  dispatch(toggleMetronome());
  dispatch(syncTracksToServer(sessionId));
};
```

### Phase 3: Debouncing and Optimization

**Problem:** Don't want to spam server with requests on every slider move.

**Solution:** Debounce track sync calls:

```javascript
// In trackSyncService.js
import { debounce } from 'lodash';

export const createDebouncedTrackSync = (dispatch, sessionId) => {
  return debounce(() => {
    dispatch(syncTracksToServer(sessionId));
  }, 500); // Wait 500ms after last change
};
```

**Usage:**
```javascript
// In component
const debouncedSync = useMemo(
  () => createDebouncedTrackSync(dispatch, sessionId),
  [dispatch, sessionId]
);

const handleMixerChange = (change) => {
  updateLocalState(change);
  debouncedSync(); // Will sync 500ms after last change
};
```

### Phase 4: Initial Sync Sequence (Match Legacy)

**Goal:** Replicate legacy's 3-call pattern on session join.

```javascript
// In useSessionModel.js or JKSessionScreen.js
useEffect(() => {
  if (sessionJoined && clientId) {
    // First sync: Initial setup (~1s after join)
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 1000);

    // Second sync: Refinement (~1.4s after join)
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 1400);

    // Third sync: Final config (~6s after join)
    setTimeout(() => {
      dispatch(syncTracksToServer(sessionId));
    }, 6000);
  }
}, [sessionJoined, clientId]);
```

**Note:** This mimics legacy behavior. May be optimized later to single call once we verify it works.

---

## 5. Data Flow Diagram

```
User Action (e.g., adjust mixer)
        ↓
Component Handler (e.g., handleMixerChange)
        ↓
Update Local Redux State (immediate UI feedback)
        ↓
Debounced Track Sync Trigger
        ↓
buildTrackSyncPayload(Redux state)
        ↓
putTrackSyncChange(sessionId, payload) → Rails API
        ↓
Rails: Update DB + Send WebSocket notification
        ↓
Response: Updated track data
        ↓
Update Redux with server response
        ↓
Components re-render with confirmed state
```

---

## 6. Files to Create/Modify

### New Files

1. **`src/services/trackSyncService.js`** (~150 lines)
   - Track sync business logic
   - Payload builder
   - Redux thunk actions

2. **`src/hooks/useTrackSync.js`** (~80 lines)
   - Hook to provide track sync functionality
   - Debouncing logic
   - Effect hooks for auto-sync

### Modified Files

1. **`src/components/client/JKSessionScreen.js`**
   - Add initial track sync on session join
   - Import and use `useTrackSync` hook

2. **`src/hooks/useMixerHelper.js`**
   - Add track sync calls when mixer changes
   - Debounce sync to avoid spam

3. **`src/hooks/useMediaActions.js`**
   - Add track sync after media toggle
   - Sync when backing track/jam track opened/closed

4. **`src/components/client/JKSessionMetronomePlayer.js`**
   - Add track sync when metronome toggled

5. **`src/store/features/activeSessionSlice.js`** (maybe)
   - Add `setTrackSyncStatus` action (syncing, success, error)
   - Track last sync timestamp

---

## 7. Testing Strategy

### Unit Tests

**Test file:** `src/services/trackSyncService.test.js`

```javascript
describe('trackSyncService', () => {
  test('buildTrackSyncPayload constructs correct payload', () => {
    const mockState = {
      activeSession: {
        userTracks: [{ instrument_id: 'guitar', sound: 'stereo' }],
        backingTrackData: [],
        metronome: { isOpen: false }
      }
    };

    const payload = buildTrackSyncPayload(mockState, 'session-123');

    expect(payload.tracks).toHaveLength(1);
    expect(payload.tracks[0].instrument_id).toBe('guitar');
    expect(payload.metronome_open).toBe(false);
  });

  test('syncTracksToServer calls API and updates Redux', async () => {
    // Mock API call
    // Mock dispatch
    // Assert correct flow
  });
});
```

### Integration Tests (Playwright)

**Test file:** `test/track-sync/track-configuration.spec.ts`

```javascript
test.describe('Track Configuration API', () => {
  test('syncs tracks when user joins session', async ({ page }) => {
    const apiInterceptor = new APIInterceptor();
    apiInterceptor.intercept(page);

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

    // Wait for track sync calls
    await page.waitForTimeout(7000);

    const trackSyncCalls = apiInterceptor.getCallsByPath('/api/sessions/*/tracks');
    const putCalls = trackSyncCalls.filter(c => c.method === 'PUT');

    // Should have at least 1 track sync call
    expect(putCalls.length).toBeGreaterThanOrEqual(1);

    // Validate payload structure
    const firstCall = putCalls[0];
    expect(firstCall.requestBody).toHaveProperty('client_id');
    expect(firstCall.requestBody).toHaveProperty('tracks');
    expect(firstCall.requestBody).toHaveProperty('backing_tracks');
    expect(firstCall.requestBody).toHaveProperty('metronome_open');

    // Validate response
    expect(firstCall.responseStatus).toBe(200);
  });

  test('syncs tracks when metronome toggled', async ({ page }) => {
    // Join session
    // Click metronome toggle
    // Assert PUT /tracks called with metronome_open=true
  });

  test('syncs tracks when backing track opened', async ({ page }) => {
    // Join session
    // Open backing track
    // Assert PUT /tracks called with backing_tracks array
  });
});
```

### Manual Testing Checklist

- [ ] Join session → verify 1-3 PUT /tracks calls in DevTools Network tab
- [ ] Toggle metronome → verify PUT /tracks called with metronome_open=true
- [ ] Open backing track → verify PUT /tracks called with backing_tracks populated
- [ ] Adjust mixer → verify debounced PUT /tracks call
- [ ] Change instrument → verify PUT /tracks called with new instrument_id
- [ ] Network failure → verify error handling (toast/alert shown)
- [ ] Verify other participants see track changes via WebSocket

---

## 8. Implementation Steps (Order)

1. **Create `trackSyncService.js`**
   - Implement `buildTrackSyncPayload()`
   - Implement `syncTracksToServer()` thunk
   - Add error handling

2. **Create `useTrackSync.js` hook**
   - Wrap `syncTracksToServer` for component use
   - Add debouncing logic
   - Return `{ syncTracks, isSyncing, lastSyncTime, syncError }`

3. **Add initial sync to `JKSessionScreen.js`**
   - Import `useTrackSync`
   - Add useEffect to sync on session join (3 calls at 1s, 1.4s, 6s)
   - Test with manual join

4. **Add sync to metronome toggle**
   - Modify `JKSessionMetronomePlayer.js`
   - Call `syncTracks()` when metronome toggled
   - Test metronome toggle

5. **Add sync to media actions**
   - Modify `useMediaActions.js`
   - Add sync after `toggleBackingTrack`, `toggleJamTrack`
   - Test opening/closing media

6. **Add sync to mixer changes** (optional, can defer)
   - Modify `useMixerHelper.js`
   - Debounced sync on mixer adjustments
   - Test mixer slider changes

7. **Write Playwright tests**
   - Create `test/track-sync/track-configuration.spec.ts`
   - Test session join sync
   - Test metronome sync
   - Test media sync

8. **Manual testing and refinement**
   - Test all flows in browser
   - Verify WebSocket notifications to other participants
   - Check DevTools for correct API calls
   - Optimize timing and debouncing

---

## 9. Edge Cases and Error Handling

### Edge Cases

1. **Session not yet joined**
   - Don't call sync if `sessionId` is null
   - Wait for `sessionJoined` flag

2. **Client ID not available**
   - Don't call sync if `clientId` is null
   - Wait for native client initialization

3. **No tracks to sync**
   - Still call API with empty arrays (server needs metronome state)

4. **Rapid consecutive changes**
   - Debounce to avoid API spam
   - Cancel pending requests if new one triggered

5. **User leaves session mid-sync**
   - Cancel pending requests
   - Clear sync timers

### Error Handling

1. **Network failure**
   - Show user-friendly error toast
   - Don't update local state
   - Retry with exponential backoff?

2. **401 Unauthorized**
   - Session expired, redirect to login

3. **404 Session not found**
   - Session was deleted, show error and redirect

4. **422 Validation error**
   - Log error, show to user
   - Check payload format

---

## 10. Success Criteria

### Must Have ✅

- [ ] PUT /api/sessions/{id}/tracks called on session join
- [ ] Payload includes client_id, tracks, backing_tracks, metronome_open
- [ ] Response updates Redux state
- [ ] Metronome toggle triggers sync
- [ ] Backing track open/close triggers sync
- [ ] Errors handled gracefully with user feedback
- [ ] Playwright tests pass for all scenarios

### Should Have 📋

- [ ] Debouncing prevents API spam
- [ ] Sync status shown in UI (loading spinner?)
- [ ] Automatic retry on network failure
- [ ] Legacy-like 3-call pattern on session join (for compatibility)

### Could Have 💡

- [ ] Optimistic updates (update UI before server confirms)
- [ ] Sync queue for offline resilience
- [ ] Analytics tracking for sync success/failure rates
- [ ] Reduce to single call on join (after verifying works)

---

## 11. Rollout Plan

### Phase 1: Development
- Implement in feature branch: `feature/track-sync-api`
- Local testing with Rails backend
- Unit tests passing

### Phase 2: Integration Testing
- Playwright tests passing
- Manual QA with multiple participants
- Verify WebSocket notifications work

### Phase 3: Staging Deployment
- Deploy to staging environment
- Monitor error rates and API performance
- Test with beta users

### Phase 4: Production Rollout
- Deploy to production
- Monitor API calls (should see 3 PUT /tracks per session join)
- Monitor error rates
- Verify no performance degradation

---

## 12. Risks and Mitigations

| Risk | Impact | Mitigation |
|------|--------|------------|
| API spam from debouncing bugs | High server load | Implement rate limiting, test debouncing thoroughly |
| Wrong payload format breaks Rails | 422 errors, broken sync | Validate payload against Rails specs, add schema validation |
| Sync failures silent | Users unaware of issues | Add error toasts, log to analytics |
| Race conditions with WebSocket updates | Inconsistent state | Implement proper Redux action ordering |
| Breaking changes to legacy app | Backward compatibility issues | Test with both legacy and jam-ui simultaneously |

---

## 13. Questions to Resolve Before Implementation

1. **Should we match legacy's 3-call pattern exactly?**
   - Pro: Ensures compatibility
   - Con: Seems redundant, could optimize to 1 call
   - Decision: Start with 3 calls, optimize later

2. **What happens if native client not initialized yet?**
   - Need to wait for `clientId` before calling API
   - Add guard: `if (!clientId) return;`

3. **Should mixer changes sync immediately or debounced?**
   - Debounced (500ms) to avoid spam
   - User gets immediate UI feedback from local state

4. **How to handle tracks from native client?**
   - Native client provides track list via `jamClient` API
   - Need to map native client tracks to API format
   - Check if `jamClient.getTracks()` method exists

5. **What about instrument selection?**
   - Need UI for user to select instrument
   - Sync after instrument change
   - Is this already implemented?

---

**Next Steps:**
1. Review this plan with team
2. Answer open questions
3. Get approval on approach
4. Start implementation with Phase 1 (trackSyncService)

---

**Generated:** 2026-01-21
**Status:** ✅ Ready for Implementation
