diff --git a/.planning/ROADMAP.md b/.planning/ROADMAP.md
index 8848b2aaa..80632d739 100644
--- a/.planning/ROADMAP.md
+++ b/.planning/ROADMAP.md
@@ -46,10 +46,10 @@ Plans:
**Success Criteria** (what must be TRUE):
1. Opening a backing track file shows the track in session screen (not just popup)
2. No "state update on unmounted component" warnings when closing backing track quickly
-**Plans**: TBD
+**Plans**: 1 plan
Plans:
-- [ ] 27-01: TBD
+- [ ] 27-01-PLAN.md - Use openBackingTrack action and add async cleanup
### Phase 28: Metronome Responsiveness
**Goal**: Metronome controls respond smoothly to user input
@@ -71,7 +71,7 @@ Plans:
| Phase | Milestone | Plans Complete | Status | Completed |
|-------|-----------|----------------|--------|-----------|
| 26. JamTrack Polish | v1.6 | 4/4 | Complete | 2026-02-25 |
-| 27. Backing Track Sync | v1.6 | 0/TBD | Not started | - |
+| 27. Backing Track Sync | v1.6 | 0/1 | Planned | - |
| 28. Metronome Responsiveness | v1.6 | 0/TBD | Not started | - |
---
diff --git a/.planning/phases/27-backing-track-sync/27-01-PLAN.md b/.planning/phases/27-backing-track-sync/27-01-PLAN.md
new file mode 100644
index 000000000..30d6cece2
--- /dev/null
+++ b/.planning/phases/27-backing-track-sync/27-01-PLAN.md
@@ -0,0 +1,266 @@
+---
+phase: 27-backing-track-sync
+plan: 01
+type: execute
+wave: 1
+depends_on: []
+files_modified:
+ - jam-ui/src/components/client/JKSessionScreen.js
+ - jam-ui/src/components/client/JKSessionBackingTrackPlayer.js
+autonomous: true
+
+must_haves:
+ truths:
+ - "Opening a backing track file shows the track in session screen (not just popup)"
+ - "No 'state update on unmounted component' warnings when closing backing track quickly"
+ artifacts:
+ - path: "jam-ui/src/components/client/JKSessionScreen.js"
+ provides: "handleBackingTrackSelected using openBackingTrack action"
+ contains: "openBackingTrack(result.file)"
+ - path: "jam-ui/src/components/client/JKSessionBackingTrackPlayer.js"
+ provides: "Duration fetch useEffect with cleanup"
+ contains: "let ignore = false"
+ key_links:
+ - from: "JKSessionScreen.js handleBackingTrackSelected"
+ to: "useMediaActions openBackingTrack"
+ via: "async await call"
+ pattern: "await openBackingTrack\\(result\\.file\\)"
+ - from: "JKSessionBackingTrackPlayer.js useEffect"
+ to: "cleanup function"
+ via: "return statement"
+ pattern: "return.*ignore = true"
+---
+
+
+Fix backing track sync to session screen and prevent unmounted component warnings.
+
+Purpose: When users open a backing track, it should appear in the session screen (like JamTrack does), and closing the popup quickly should not produce React warnings.
+
+Output: Two fixes in two files - use Redux action for track sync, add ignore flag for async cleanup.
+
+
+
+@/Users/nuwan/.claude/get-shit-done/workflows/execute-plan.md
+@/Users/nuwan/.claude/get-shit-done/templates/summary.md
+
+
+
+@.planning/PROJECT.md
+@.planning/ROADMAP.md
+@.planning/phases/27-backing-track-sync/27-RESEARCH.md
+
+# Source files to modify
+@jam-ui/src/components/client/JKSessionScreen.js
+@jam-ui/src/components/client/JKSessionBackingTrackPlayer.js
+
+# Reference for correct pattern (JamTrack uses similar flow)
+@jam-ui/src/hooks/useMediaActions.js
+
+
+
+
+
+ Task 1: Use openBackingTrack action in handleBackingTrackSelected
+ jam-ui/src/components/client/JKSessionScreen.js
+
+In the `handleBackingTrackSelected` function (around line 1137), replace the direct `jamClient.SessionOpenBackingTrackFile()` call with the `openBackingTrack()` action from useMediaActions.
+
+Current (incorrect):
+```javascript
+const handleBackingTrackSelected = async (result) => {
+ try {
+ await jamClient.SessionOpenBackingTrackFile(result.file, false);
+ dispatch(setBackingTrackData({...}));
+ dispatch(openModal('backingTrack'));
+ } catch (error) {
+ toast.error('Failed to open backing track');
+ }
+};
+```
+
+Change to (correct):
+```javascript
+const handleBackingTrackSelected = async (result) => {
+ try {
+ // Use the openBackingTrack action from useMediaActions (already imported at line 153)
+ // This handles: jamClient call, Redux state update, and server sync
+ await openBackingTrack(result.file);
+
+ // Set popup data (same as before)
+ dispatch(setBackingTrackData({
+ backingTrack: result.file,
+ session: currentSession,
+ currentUser: currentUser
+ }));
+ dispatch(openModal('backingTrack'));
+ } catch (error) {
+ toast.error('Failed to open backing track');
+ }
+};
+```
+
+The `openBackingTrack` is already destructured from `useMediaActions()` at line 153. This change ensures:
+1. jamClient.SessionOpenBackingTrackFile is called (via the thunk)
+2. mediaSummary.backingTrackOpen is set to true
+3. syncTracksToServer is called to sync track to session screen
+
+
+1. Grep for the change: `grep -n "openBackingTrack(result.file)" jam-ui/src/components/client/JKSessionScreen.js`
+2. Verify jamClient.SessionOpenBackingTrackFile is NOT called directly in handleBackingTrackSelected
+3. Run: `cd jam-ui && npm run build` - should complete without errors
+
+
+- handleBackingTrackSelected calls `await openBackingTrack(result.file)` instead of direct jamClient call
+- Build succeeds
+- No direct jamClient.SessionOpenBackingTrackFile call in handleBackingTrackSelected
+
+
+
+
+ Task 2: Add ignore flag to duration fetch useEffect
+ jam-ui/src/components/client/JKSessionBackingTrackPlayer.js
+
+In the duration fetch useEffect (around line 90-143), add an ignore flag to prevent state updates after unmount.
+
+Current structure (no cleanup):
+```javascript
+useEffect(() => {
+ const shouldInitialize = (isOpen || isPopup) && backingTrack && jamClient;
+ if (shouldInitialize) {
+ setIsPlaying(false);
+ setCurrentTime('0:00');
+ setCurrentPositionMs(0);
+
+ const fetchDuration = async () => {
+ setIsLoadingDuration(true);
+ try {
+ // ... async jamClient calls ...
+ setDurationMs(validDuration);
+ setDuration(formatTime(validDuration));
+ setIsLoadingDuration(false);
+ } catch (error) {
+ // ... state updates ...
+ }
+ };
+ fetchDuration();
+ }
+}, [deps]);
+```
+
+Change to (with ignore flag):
+```javascript
+useEffect(() => {
+ let ignore = false;
+
+ const shouldInitialize = (isOpen || isPopup) && backingTrack && jamClient;
+ if (shouldInitialize) {
+ setIsPlaying(false);
+ setCurrentTime('0:00');
+ setCurrentPositionMs(0);
+
+ const fetchDuration = async () => {
+ if (ignore) return;
+ setIsLoadingDuration(true);
+
+ try {
+ if (!jamClient) {
+ if (!ignore) {
+ handleError('general', 'Audio engine not available', null);
+ setIsLoadingDuration(false);
+ }
+ return;
+ }
+
+ const durationInMs = await jamClient.SessionGetTracksPlayDurationMs();
+
+ // Check ignore AFTER async operation
+ if (ignore) return;
+
+ const validDuration = parseInt(durationInMs, 10) || 0;
+ if (validDuration === 0 || isNaN(validDuration)) {
+ handleError('file', 'Failed to load track duration. The file may be invalid or unsupported.', null);
+ setDuration('0:00');
+ setDurationMs(0);
+ setIsLoadingDuration(false);
+ return;
+ }
+
+ setDurationMs(validDuration);
+ setDuration(formatTime(validDuration));
+ clearError();
+ setIsLoadingDuration(false);
+ } catch (error) {
+ if (!ignore) {
+ handleError('file', 'Failed to load track duration', error);
+ setDuration('0:00');
+ setDurationMs(0);
+ setIsLoadingDuration(false);
+ }
+ }
+ };
+
+ fetchDuration();
+ }
+
+ return () => {
+ ignore = true;
+ };
+}, [isOpen, isPopup, backingTrack, jamClient, handleError, formatTime, clearError]);
+```
+
+Key changes:
+1. Add `let ignore = false;` at start of useEffect
+2. Check `if (ignore) return;` before initial state updates in fetchDuration
+3. Check `if (ignore) return;` after the await call (the critical point)
+4. Wrap all state updates in catch block with `if (!ignore)`
+5. Add cleanup function `return () => { ignore = true; };`
+
+
+1. Grep for the pattern: `grep -n "let ignore = false" jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
+2. Grep for cleanup: `grep -n "ignore = true" jam-ui/src/components/client/JKSessionBackingTrackPlayer.js`
+3. Run: `cd jam-ui && npm run build` - should complete without errors
+
+
+- useEffect has `let ignore = false;` declaration
+- useEffect has cleanup function that sets `ignore = true`
+- All state updates after async operations are guarded with `if (!ignore)` or `if (ignore) return;`
+- Build succeeds
+
+
+
+
+
+
+After both tasks complete:
+
+1. **Build verification:**
+ ```bash
+ cd jam-ui && npm run build
+ ```
+ Should complete without errors.
+
+2. **Code verification:**
+ ```bash
+ # BT-01: Verify openBackingTrack is used
+ grep -n "openBackingTrack(result.file)" jam-ui/src/components/client/JKSessionScreen.js
+
+ # BT-02: Verify ignore flag pattern
+ grep -n "let ignore = false" jam-ui/src/components/client/JKSessionBackingTrackPlayer.js
+ grep -n "ignore = true" jam-ui/src/components/client/JKSessionBackingTrackPlayer.js
+ ```
+
+3. **No regressions:**
+ - Backing track popup should still open when selecting a file
+ - Existing functionality preserved
+
+
+
+1. `handleBackingTrackSelected` uses `await openBackingTrack(result.file)` - enables session screen sync
+2. Duration fetch useEffect has ignore flag with cleanup - prevents unmount warnings
+3. `npm run build` succeeds with no errors
+4. No removed functionality (popup still works, duration still displays)
+
+
+