From cc32abcabac2883d04fae35894e822494425e993 Mon Sep 17 00:00:00 2001 From: Nuwan Date: Wed, 14 Jan 2026 23:48:22 +0530 Subject: [PATCH] docs(05): create phase plan Phase 5: JamTrack Implementation - 5 plans created - 14 total tasks defined - Ready for execution Plan breakdown: - 05-01: Redux Infrastructure & Bug Fixes (2 tasks) - 05-02: Async Thunks & Component Core (3 tasks) - 05-03: Playback Controls & Polling (3 tasks) - 05-04: Mixdown Selection & Download UI (3 tasks) - 05-05: Error Handling & Final UAT (3 tasks, includes UAT checkpoint) Scope: Download + play JamTracks + select existing mixdowns Quality bar: Match Backing Track player feel and reliability Estimated complexity: 2.5-3x Phase 3 effort Co-Authored-By: Claude Sonnet 4.5 --- .../05-jamtrack-implementation/05-01-PLAN.md | 219 ++++++++ .../05-jamtrack-implementation/05-02-PLAN.md | 506 ++++++++++++++++++ .../05-jamtrack-implementation/05-03-PLAN.md | 416 ++++++++++++++ .../05-jamtrack-implementation/05-04-PLAN.md | 381 +++++++++++++ .../05-jamtrack-implementation/05-05-PLAN.md | 423 +++++++++++++++ 5 files changed, 1945 insertions(+) create mode 100644 .planning/phases/05-jamtrack-implementation/05-01-PLAN.md create mode 100644 .planning/phases/05-jamtrack-implementation/05-02-PLAN.md create mode 100644 .planning/phases/05-jamtrack-implementation/05-03-PLAN.md create mode 100644 .planning/phases/05-jamtrack-implementation/05-04-PLAN.md create mode 100644 .planning/phases/05-jamtrack-implementation/05-05-PLAN.md diff --git a/.planning/phases/05-jamtrack-implementation/05-01-PLAN.md b/.planning/phases/05-jamtrack-implementation/05-01-PLAN.md new file mode 100644 index 000000000..639677021 --- /dev/null +++ b/.planning/phases/05-jamtrack-implementation/05-01-PLAN.md @@ -0,0 +1,219 @@ +--- +phase: 05-jamtrack-implementation +plan: 01 +type: execute +--- + + +Establish Redux infrastructure for JamTrack with critical bug fix and state structure extensions. + +Purpose: Fix existing fqId bug that blocks all JamTrack functionality, then add Redux state structure needed for download/sync machine, mixdown selection, and UI preferences. +Output: Redux slices extended with JamTrack state, bug fixed, ready for async thunks. + + + +@./.claude/get-shit-done/workflows/execute-phase.md +@./.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/05-jamtrack-implementation/05-CONTEXT.md +@.planning/phases/04-jamtrack-research-design/04-02-SUMMARY.md +@.planning/phases/04-jamtrack-research-design/REDUX_DESIGN.md +@.planning/codebase/CONVENTIONS.md +@.planning/codebase/JAMTRACK_API.md +@jam-ui/src/store/features/mediaSlice.js +@jam-ui/src/store/features/sessionUISlice.js + +**Tech stack available:** +- React 16.13.1 +- Redux Toolkit 1.6.1 +- jamClient (C++ native client proxy via QWebChannel) +- WebSocket Protocol Buffer messaging + +**Established patterns from Phase 3 & 4:** +- jamClient passed as prop (non-serializable, never in Redux) +- jamClient returns Promises (always async/await) +- jamClient returns strings for position/duration (always parseInt) +- fqId format mandatory: `{jamTrackId}-{sampleRate}` for all JamTrack calls +- Cleanup on unmount required (prevent stale state) + +**Constraining decisions:** +- Phase 4: Redux state split: jamTrackState (playback), downloadState (sync machine), availableMixdowns (mixdown cache) +- Phase 4: 6 async thunks needed: loadJamTrack (enhanced), downloadJamTrack, checkJamTrackSync, loadJMEP, seekJamTrack, closeJamTrack +- Phase 4: Enhanced loadJamTrack fixes existing bug: uses fqId format instead of jamTrack.id +- Phase 5 Context: All three pillars equally important (download UX, playback reliability, mixdown selection) +- Phase 5 Context: Match Backing Track player feel and quality + +**Critical bug to fix:** +- mediaSlice.js line 29: `JamTrackPlay(jamTrack.id)` should be `JamTrackPlay(fqId)` +- This bug blocks ALL JamTrack playback - must fix in Task 1 + + + + + + Task 1: Fix loadJamTrack fqId bug and extend mediaSlice + jam-ui/src/store/features/mediaSlice.js + +**CRITICAL BUG FIX (line 29):** +Change `await jamClient.JamTrackPlay(jamTrack.id)` to `await jamClient.JamTrackPlay(fqId)` where fqId is already built on line 24. + +**Then extend mediaSlice initialState:** +1. Replace `jamTrackState: {}` with full structure: +```javascript +jamTrackState: { + isPlaying: false, + isPaused: false, + currentPositionMs: 0, + durationMs: 0, + selectedMixdownId: null, + playbackMode: null, // 'master' | 'custom-mix' | 'stem' + lastUpdate: null +} +``` + +2. Replace `downloadingJamTrack: false` with full downloadState: +```javascript +downloadState: { + jamTrackId: null, + mixdownId: null, + fqId: null, + state: 'idle', // 'idle' | 'checking' | 'downloading' | 'keying' | 'synchronized' | 'error' + progress: 0, + currentStep: 0, + totalSteps: 0, + error: null +} +``` + +**Add new reducers:** +- setJamTrackState(state, action) - Sets full jamTrackState from action.payload +- setDownloadState(state, action) - Sets full downloadState from action.payload +- clearDownloadState(state) - Resets downloadState to initial values + +**Keep existing:** +- updateJamTrackState (for partial updates from WebSocket) +- clearJamTrackState (for cleanup) + +**Do NOT add thunks yet** - that's Plan 2. Only fix bug and add state structure. + + +1. grep "JamTrackPlay(fqId)" jam-ui/src/store/features/mediaSlice.js (not jamTrack.id) +2. grep "state: 'idle'" jam-ui/src/store/features/mediaSlice.js (downloadState exists) +3. grep "setDownloadState" jam-ui/src/store/features/mediaSlice.js (new reducer exists) + + +- loadJamTrack bug fixed (uses fqId not jamTrack.id) +- jamTrackState extended with 7 fields (isPlaying, isPaused, currentPositionMs, durationMs, selectedMixdownId, playbackMode, lastUpdate) +- downloadState added with 8 fields (jamTrackId, mixdownId, fqId, state, progress, currentStep, totalSteps, error) +- 3 new reducers added (setJamTrackState, setDownloadState, clearDownloadState) +- No TypeScript/linter errors introduced + + + + + Task 2: Extend activeSessionSlice and sessionUISlice + jam-ui/src/store/features/activeSessionSlice.js, jam-ui/src/store/features/sessionUISlice.js + +**In activeSessionSlice.js:** + +Add to initialState: +```javascript +availableMixdowns: [], // Array of mixdown objects: { id, type: 'master'|'custom-mix'|'stem', name, jamTrackId, packageId } +activeMixdown: null, // Currently selected mixdown object +mixdownCache: {} // Map of packageId -> { metadata, sampleRate, fileType, encryptType } +``` + +Add reducers: +- setAvailableMixdowns(state, action) - Sets availableMixdowns array +- setActiveMixdown(state, action) - Sets activeMixdown object +- cacheMixdownPackage(state, action) - Adds/updates entry in mixdownCache: `state.mixdownCache[action.payload.packageId] = action.payload.metadata` +- clearMixdowns(state) - Resets all three to initial values + +**In sessionUISlice.js:** + +Add to initialState: +```javascript +openJamTrack: null, // Currently open JamTrack ID or null +jamTrackUI: { + lastUsedMixdownId: null, // User's last selected mixdown for this session + volume: 100 // Last used volume (0-100) +} +``` + +Add reducers: +- setOpenJamTrack(state, action) - Sets openJamTrack ID +- updateJamTrackUI(state, action) - Merges action.payload into jamTrackUI: `state.jamTrackUI = { ...state.jamTrackUI, ...action.payload }` +- clearOpenJamTrack(state) - Sets openJamTrack to null + +**Do NOT add thunks** - only state structure and reducers. + + +1. grep "availableMixdowns" jam-ui/src/store/features/activeSessionSlice.js (exists in state and reducer) +2. grep "openJamTrack" jam-ui/src/store/features/sessionUISlice.js (exists in state and reducer) +3. grep "setActiveMixdown" jam-ui/src/store/features/activeSessionSlice.js (reducer exists) + + +- activeSessionSlice extended with availableMixdowns, activeMixdown, mixdownCache state + 4 reducers +- sessionUISlice extended with openJamTrack, jamTrackUI state + 3 reducers +- All reducers follow Redux Toolkit patterns (immutable updates) +- No TypeScript/linter errors introduced + + + + + + +Before declaring plan complete: +- [ ] Bug fix verified: loadJamTrack uses fqId not jamTrack.id +- [ ] All state structures added with correct field names and types +- [ ] All reducers added and follow Redux Toolkit patterns +- [ ] No errors when importing slices + + + + +- loadJamTrack fqId bug fixed (critical blocker removed) +- mediaSlice extended: jamTrackState (7 fields), downloadState (8 fields), 3 new reducers +- activeSessionSlice extended: availableMixdowns, activeMixdown, mixdownCache, 4 new reducers +- sessionUISlice extended: openJamTrack, jamTrackUI, 3 new reducers +- All code follows existing patterns and conventions +- Redux state foundation ready for async thunks (Plan 2) + + + +After completion, create `.planning/phases/05-jamtrack-implementation/05-01-SUMMARY.md`: + +# Phase 5 Plan 1: Redux Infrastructure & Bug Fixes Summary + +**[Substantive one-liner - what shipped]** + +## Accomplishments + +- Fixed critical loadJamTrack fqId bug +- Extended mediaSlice with jamTrackState and downloadState +- Extended activeSessionSlice with mixdown management state +- Extended sessionUISlice with JamTrack UI preferences + +## Files Created/Modified + +- `jam-ui/src/store/features/mediaSlice.js` - Bug fix + state extensions +- `jam-ui/src/store/features/activeSessionSlice.js` - Mixdown state +- `jam-ui/src/store/features/sessionUISlice.js` - UI preferences + +## Decisions Made + +[Key decisions and rationale] + +## Issues Encountered + +[Problems and resolutions, or "None"] + +## Next Step + +Ready for Plan 2: Async Thunks & Component Core + diff --git a/.planning/phases/05-jamtrack-implementation/05-02-PLAN.md b/.planning/phases/05-jamtrack-implementation/05-02-PLAN.md new file mode 100644 index 000000000..ac05fb542 --- /dev/null +++ b/.planning/phases/05-jamtrack-implementation/05-02-PLAN.md @@ -0,0 +1,506 @@ +--- +phase: 05-jamtrack-implementation +plan: 02 +type: execute +--- + + +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. + + + +@./.claude/get-shit-done/workflows/execute-phase.md +@./.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Implement 6 async thunks in mediaSlice + jam-ui/src/store/features/mediaSlice.js + +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. + + +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 + + +- 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 + + + + + Task 2: Add 3 WebSocket handlers + jam-ui/src/hooks/useWebSocketSubscription.js (or equivalent WebSocket handler file) + +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. + + +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) + + +- 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) + + + + + Task 3: Create JKSessionJamTrackPlayer component skeleton + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +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 ( +
+

JamTrack Player (Skeleton)

+ {isLoadingSync &&

Checking sync status...

} + {error &&

{error.message}

} + {downloadState.state !== 'idle' && ( +

Download State: {downloadState.state} ({downloadState.progress}%)

+ )} +

Ready for playback controls (Plan 3)

+
+ ); +}; + +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 +
+ +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) + + +- 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 + +
+ +
+ + +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 + + + + +- 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) + + + +After completion, create `.planning/phases/05-jamtrack-implementation/05-02-SUMMARY.md` + diff --git a/.planning/phases/05-jamtrack-implementation/05-03-PLAN.md b/.planning/phases/05-jamtrack-implementation/05-03-PLAN.md new file mode 100644 index 000000000..a7c27335d --- /dev/null +++ b/.planning/phases/05-jamtrack-implementation/05-03-PLAN.md @@ -0,0 +1,416 @@ +--- +phase: 05-jamtrack-implementation +plan: 03 +type: execute +--- + + +Implement playback controls, visibility-aware polling, and seek slider for JamTrack player. + +Purpose: Add play/pause/stop controls with jamClient integration, implement 500ms/2000ms polling pattern from Phase 3, and wire seek slider with UAT-003 fix pattern. +Output: Functional playback controls with real-time position updates and smooth seeking. + + + +@./.claude/get-shit-done/workflows/execute-phase.md +@./.claude/get-shit-done/templates/summary.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/05-jamtrack-implementation/05-CONTEXT.md +@.planning/phases/05-jamtrack-implementation/05-02-SUMMARY.md +@.planning/phases/04-jamtrack-research-design/COMPONENT_DESIGN.md +@.planning/codebase/JAMTRACK_API.md +@jam-ui/src/components/client/JKSessionBackingTrackPlayer.js +@jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +**Established patterns from Phase 3:** +- Visibility-aware polling: 500ms visible, 2000ms hidden +- useCallback for all handlers (prevent re-renders) +- Conditional state updates in polling (only update if values changed) +- UAT-003 fix: pending seek while paused, apply on resume +- End-of-track handling: stop and reset when position >= duration +- isOperating flag prevents rapid clicks during async operations + +**From Phase 4 design:** +- Playback controls: handlePlay, handlePause, handleStop +- Polling: JamTrackGetPositionMs, JamTrackGetDurationMs +- Seek: handleSeek dispatches seekJamTrack thunk +- End-of-track: detect currentPositionMs >= durationMs + + + + + + Task 1: Implement playback control handlers + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +Add playback control handlers to JKSessionJamTrackPlayer: + +**1. handlePlay:** +```javascript +const handlePlay = useCallback(async () => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setIsOperating(true); + setError(null); + + // If not playing yet, load JamTrack + if (!jamTrackState.isPlaying) { + await dispatch(loadJamTrack({ + jamTrack, + mixdownId: selectedMixdownId, + autoPlay: true, + jamClient + })).unwrap(); + } else if (jamTrackState.isPaused) { + // Resume from pause + await jamClient.JamTrackResume(fqIdRef.current); + + // Apply pending seek if exists (UAT-003 fix) + if (pendingSeekRef.current !== null) { + await jamClient.JamTrackSeekMs(fqIdRef.current, pendingSeekRef.current); + pendingSeekRef.current = null; + } + + dispatch(setJamTrackState({ isPaused: false, isPlaying: true })); + } + } catch (err) { + console.error('[JamTrack] Play error:', err); + setError({ type: 'playback', message: 'Failed to play JamTrack' }); + } finally { + if (mountedRef.current) { + setIsOperating(false); + } + } +}, [isOperating, jamClient, jamTrack, jamTrackState, selectedMixdownId, dispatch]); +``` + +**2. handlePause:** +```javascript +const handlePause = useCallback(async () => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setIsOperating(true); + setError(null); + + await jamClient.JamTrackPause(fqIdRef.current); + dispatch(setJamTrackState({ isPaused: true, isPlaying: false })); + } catch (err) { + console.error('[JamTrack] Pause error:', err); + setError({ type: 'playback', message: 'Failed to pause JamTrack' }); + } finally { + if (mountedRef.current) { + setIsOperating(false); + } + } +}, [isOperating, jamClient, dispatch]); +``` + +**3. handleStop:** +```javascript +const handleStop = useCallback(async () => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setIsOperating(true); + setError(null); + + await jamClient.JamTrackStop(fqIdRef.current); + dispatch(setJamTrackState({ + isPlaying: false, + isPaused: false, + currentPositionMs: 0 + })); + } catch (err) { + console.error('[JamTrack] Stop error:', err); + setError({ type: 'playback', message: 'Failed to stop JamTrack' }); + } finally { + if (mountedRef.current) { + setIsOperating(false); + } + } +}, [isOperating, jamClient, dispatch]); +``` + +**Add refs:** +```javascript +const pendingSeekRef = useRef(null); +``` + +**Add buttons to render (replace placeholder):** +```javascript +return ( +
+ {error && ( +
+ {error.message} + +
+ )} + + {downloadState.state !== 'idle' && downloadState.state !== 'synchronized' && ( +
+

Download: {downloadState.state} ({downloadState.progress}%)

+
+ )} + +
+ + + +
+ +
+

Position: {jamTrackState.currentPositionMs}ms / Duration: {jamTrackState.durationMs}ms

+
+
+); +``` + +**Follow Phase 3 patterns:** +- isOperating flag prevents rapid clicks +- useCallback for all handlers +- try/catch with user-friendly error messages +- mountedRef check before setState +- Disabled buttons during operations +
+ +1. grep "const handlePlay" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists) +2. grep "const handlePause" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists) +3. grep "pendingSeekRef" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (UAT-003 fix ref exists) +4. grep "disabled={isOperating" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (buttons disabled during operations) + + +- 3 playback handlers implemented (handlePlay, handlePause, handleStop) +- All handlers use useCallback for performance +- UAT-003 fix pattern applied (pendingSeekRef for pause-seek-resume) +- Error handling with typed errors (file/network red, playback yellow) +- Buttons disabled correctly (isOperating, isLoadingSync, playback state) +- Buttons rendered in UI with proper labels + +
+ + + Task 2: Implement visibility-aware polling + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +Add polling logic following Phase 3 Backing Track pattern: + +**1. Add polling effect:** +```javascript +// Polling for position and duration +useEffect(() => { + if (!jamClient || !fqIdRef.current || !jamTrackState.isPlaying) return; + + // Check if tab is visible + const isVisible = document.visibilityState === 'visible'; + const pollInterval = isVisible ? 500 : 2000; + + const intervalId = setInterval(async () => { + try { + // Fetch position and duration + const positionStr = await jamClient.JamTrackGetPositionMs(fqIdRef.current); + const durationStr = await jamClient.JamTrackGetDurationMs(fqIdRef.current); + + const position = parseInt(positionStr, 10); + const duration = parseInt(durationStr, 10); + + // Conditional update: only update if values changed + if ( + position !== jamTrackState.currentPositionMs || + duration !== jamTrackState.durationMs + ) { + dispatch(setJamTrackState({ + currentPositionMs: position, + durationMs: duration, + lastUpdate: Date.now() + })); + } + + // End-of-track handling + if (duration > 0 && position >= duration) { + await jamClient.JamTrackStop(fqIdRef.current); + dispatch(setJamTrackState({ + isPlaying: false, + isPaused: false, + currentPositionMs: 0 + })); + } + } catch (err) { + console.error('[JamTrack] Polling error:', err); + // Don't setError - polling errors are non-critical + } + }, pollInterval); + + return () => clearInterval(intervalId); +}, [jamClient, jamTrackState.isPlaying, jamTrackState.currentPositionMs, jamTrackState.durationMs, dispatch]); + +// Listen for visibility changes to recreate interval +useEffect(() => { + const handleVisibilityChange = () => { + // Trigger polling effect to restart with new interval + // This is handled by the dependency array + }; + + document.addEventListener('visibilitychange', handleVisibilityChange); + return () => document.removeEventListener('visibilitychange', handleVisibilityChange); +}, []); +``` + +**2. Add formatTime utility:** +```javascript +// Helper: Format milliseconds to MM:SS +const formatTime = (ms) => { + if (!ms || isNaN(ms)) return '00:00'; + const totalSeconds = Math.floor(ms / 1000); + const minutes = Math.floor(totalSeconds / 60); + const seconds = totalSeconds % 60; + return `${minutes.toString().padStart(2, '0')}:${seconds.toString().padStart(2, '0')}`; +}; +``` + +**3. Update time display in render:** +```javascript +

+ {formatTime(jamTrackState.currentPositionMs)} / {formatTime(jamTrackState.durationMs)} +

+``` + +**Follow Phase 3 patterns:** +- 500ms when visible, 2000ms when hidden +- parseInt before using position/duration strings +- Conditional update (only if values changed) +- End-of-track detection and reset +- No error state for polling failures (non-critical) +
+ +1. grep "pollInterval = isVisible ? 500 : 2000" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (visibility-aware) +2. grep "parseInt(positionStr, 10)" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (parseInt before use) +3. grep "position !== jamTrackState.currentPositionMs" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (conditional update) +4. grep "formatTime" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (utility exists) + + +- Visibility-aware polling implemented (500ms visible, 2000ms hidden) +- Polling fetches position and duration via jamClient +- parseInt applied before using string values +- Conditional state updates (only when values change) +- End-of-track handling (stop and reset) +- formatTime utility for MM:SS display +- Time display updated in render +- visibilitychange listener updates polling interval + +
+ + + Task 3: Implement seek slider + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +Add seek slider following Phase 3 Backing Track pattern: + +**1. handleSeek:** +```javascript +const handleSeek = useCallback(async (newPositionMs) => { + if (isOperating || !jamClient || !fqIdRef.current) return; + + try { + setError(null); + + // UAT-003 fix: if paused, store pending seek + if (jamTrackState.isPaused) { + pendingSeekRef.current = newPositionMs; + dispatch(setJamTrackState({ currentPositionMs: newPositionMs })); + return; + } + + // If playing, seek immediately + await jamClient.JamTrackSeekMs(fqIdRef.current, newPositionMs); + dispatch(setJamTrackState({ currentPositionMs: newPositionMs })); + } catch (err) { + console.error('[JamTrack] Seek error:', err); + setError({ type: 'playback', message: 'Failed to seek' }); + } +}, [isOperating, jamClient, jamTrackState.isPaused, dispatch]); +``` + +**2. Add slider to render:** +```javascript +
+ handleSeek(parseInt(e.target.value, 10))} + disabled={isOperating || !jamTrackState.durationMs} + style={{ width: '300px' }} + /> +
+``` + +**UAT-003 fix pattern:** +- If paused: store in pendingSeekRef, update UI immediately +- On resume (handlePlay): apply pendingSeekRef if set, then clear +- If playing: seek immediately via jamClient + +**Follow Phase 3 patterns:** +- handleSeek wrapped in useCallback +- Disabled during operations and before duration loaded +- parseInt on slider value +- Error handling with user feedback +
+ +1. grep "const handleSeek" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists) +2. grep "pendingSeekRef.current = newPositionMs" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (UAT-003 fix) +3. grep "type=\"range\"" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (slider exists) +4. grep "disabled={isOperating" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (slider disabled correctly) + + +- handleSeek implemented with useCallback +- UAT-003 fix applied (pending seek while paused, apply on resume) +- Seek slider rendered with correct min/max/value +- Slider disabled during operations and before duration loaded +- onChange parses int before passing to handler +- Error handling for seek failures +- Works while playing (immediate seek) and while paused (pending seek) + +
+ +
+ + +Before declaring plan complete: +- [ ] All 3 playback handlers implemented (play/pause/stop) +- [ ] Visibility-aware polling working (500ms/2000ms) +- [ ] Position and duration update in real-time +- [ ] Seek slider functional with UAT-003 fix +- [ ] No errors when using controls + + + + +- Playback controls complete: play/pause/stop with jamClient integration +- Visibility-aware polling: 500ms visible, 2000ms hidden, conditional updates +- Seek slider functional with UAT-003 fix (pending seek while paused) +- End-of-track handling (stop and reset when position >= duration) +- formatTime utility for MM:SS display +- All handlers use useCallback for performance +- Error handling with user-friendly messages +- Buttons and slider disabled appropriately +- Player matches Backing Track feel and quality + + + +After completion, create `.planning/phases/05-jamtrack-implementation/05-03-SUMMARY.md` + diff --git a/.planning/phases/05-jamtrack-implementation/05-04-PLAN.md b/.planning/phases/05-jamtrack-implementation/05-04-PLAN.md new file mode 100644 index 000000000..7cc631a98 --- /dev/null +++ b/.planning/phases/05-jamtrack-implementation/05-04-PLAN.md @@ -0,0 +1,381 @@ +--- +phase: 05-jamtrack-implementation +plan: 04 +type: execute +--- + + +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. + + + +@./.claude/get-shit-done/workflows/execute-phase.md +@./.claude/get-shit-done/templates/summary.md + + + +@.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 + + + + + + Task 1: Fetch and organize mixdowns + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +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 + + +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) + + +- 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) + + + + + Task 2: Implement mixdown picker UI + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +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 && ( +
+ + +
+)} +``` + +**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 +
+ +1. grep "const handleMixdownChange" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (handler exists) +2. grep " + +- 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 + +
+ + + Task 3: Implement download/sync UI + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +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' && ( +
+

+ {downloadState.state === 'checking' && 'Checking sync status...'} + {downloadState.state === 'downloading' && 'Downloading JamTrack...'} + {downloadState.state === 'keying' && 'Requesting decryption keys...'} + {downloadState.state === 'error' && 'Download Failed'} +

+ + {downloadState.state === 'downloading' && ( +
+ +

{downloadState.progress}%

+ {downloadState.totalSteps > 0 && ( +

Step {downloadState.currentStep} of {downloadState.totalSteps}

+ )} + +
+ )} + + {downloadState.state === 'keying' && ( +
+

Finalizing download...

+
+ )} + + {downloadState.state === 'error' && ( +
+

{downloadState.error?.message || 'Download failed'}

+ +
+ )} +
+)} +``` + +**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 +
+ +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 " + +- 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) + +
+ +
+ + +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 + + + + +- 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 + + + +After completion, create `.planning/phases/05-jamtrack-implementation/05-04-SUMMARY.md` + diff --git a/.planning/phases/05-jamtrack-implementation/05-05-PLAN.md b/.planning/phases/05-jamtrack-implementation/05-05-PLAN.md new file mode 100644 index 000000000..62f3ffc27 --- /dev/null +++ b/.planning/phases/05-jamtrack-implementation/05-05-PLAN.md @@ -0,0 +1,423 @@ +--- +phase: 05-jamtrack-implementation +plan: 05 +type: execute +--- + + +Add comprehensive error handling, apply performance optimizations, and conduct user acceptance testing. + +Purpose: Complete JamTrack player with production-ready error handling, React performance optimizations from Phase 3, and validate all functionality through systematic UAT. +Output: Production-ready JamTrack player with documented known issues (if any). + + + +@./.claude/get-shit-done/workflows/execute-phase.md +@./.claude/get-shit-done/templates/summary.md +@./.claude/get-shit-done/references/checkpoints.md + + + +@.planning/PROJECT.md +@.planning/ROADMAP.md +@.planning/STATE.md +@.planning/phases/05-jamtrack-implementation/05-CONTEXT.md +@.planning/phases/05-jamtrack-implementation/05-04-SUMMARY.md +@.planning/phases/04-jamtrack-research-design/IMPLEMENTATION_ROADMAP.md +@.planning/phases/03-backing-track-finalization/03-03-SUMMARY.md +@jam-ui/src/components/client/JKSessionBackingTrackPlayer.js +@jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +**From Phase 3 patterns:** +- Error types: file (red), network (red), playback (yellow), general (yellow) +- Error display: banner with dismiss and retry buttons +- Network resilience: stop polling after 3 consecutive failures +- Edge cases: invalid files, null jamClient, disconnected native client +- Cleanup on unmount: prevent stale state bugs +- Performance: useCallback, useMemo, conditional updates, remove diagnostic logging + +**From Phase 4 risk analysis:** +- HIGH risks: Download/sync state machine complexity, native client race conditions +- Expected: 2-4 deferred issues similar to Phase 3 (end-of-track double-click, loop not working) +- UAT deferral threshold: Defer minor only, fix medium+ issues + +**From Phase 5 context:** +- Match Backing Track player quality bar +- All three pillars equally important (download, playback, mixdown selection) + + + + + + Task 1: Add comprehensive error handling + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +Enhance error handling throughout the component: + +**1. Add error types:** +```javascript +const ERROR_TYPES = { + FILE: 'file', // Red - critical + NETWORK: 'network', // Red - critical + DOWNLOAD: 'download', // Red - critical + PLAYBACK: 'playback', // Yellow - warning + GENERAL: 'general' // Yellow - warning +}; +``` + +**2. Enhance error display in render:** +```javascript +{error && ( +
+ {error.type.toUpperCase()} ERROR: {error.message} +
+ + {(error.type === ERROR_TYPES.NETWORK || error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) && ( + + )} +
+
+)} +``` + +**3. Add handleRetryError:** +```javascript +const handleRetryError = useCallback(async () => { + if (!error || isOperating) return; + + setError(null); + + // Retry based on error type + if (error.type === ERROR_TYPES.DOWNLOAD || error.type === ERROR_TYPES.FILE) { + await handleRetryDownload(); + } else if (error.type === ERROR_TYPES.NETWORK) { + // Re-initialize player + if (jamTrack && jamClient) { + const fqId = await buildFqId(); + fqIdRef.current = fqId; + await dispatch(loadJamTrack({ jamTrack, mixdownId: selectedMixdownId, autoPlay: false, jamClient })); + } + } +}, [error, isOperating, jamTrack, jamClient, selectedMixdownId, handleRetryDownload, buildFqId, dispatch]); +``` + +**4. Add network resilience to polling:** +```javascript +const consecutiveFailuresRef = useRef(0); + +// In polling effect, wrap try/catch: +try { + // ... existing polling logic ... + consecutiveFailuresRef.current = 0; // Reset on success +} catch (err) { + console.error('[JamTrack] Polling error:', err); + consecutiveFailuresRef.current += 1; + + if (consecutiveFailuresRef.current >= 3) { + setError({ type: ERROR_TYPES.NETWORK, message: 'Lost connection to native client. Check if JamKazam is running.' }); + // Stop polling by setting isPlaying to false + dispatch(setJamTrackState({ isPlaying: false })); + } +} +``` + +**5. Add edge case handling:** +```javascript +// In initializePlayer, before any jamClient calls: +if (!jamClient) { + setError({ type: ERROR_TYPES.GENERAL, message: 'Native client not available. Please ensure JamKazam is running.' }); + return; +} + +if (!jamTrack || !jamTrack.id) { + setError({ type: ERROR_TYPES.FILE, message: 'Invalid JamTrack data' }); + return; +} +``` + +**Follow Phase 3 patterns:** +- Typed errors with color coding (red/yellow) +- User-friendly error messages +- Retry buttons for recoverable errors +- Network resilience (3 consecutive failures) +- Edge case validation (null jamClient, invalid jamTrack) +
+ +1. grep "const ERROR_TYPES" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (error types defined) +2. grep "consecutiveFailuresRef" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (network resilience) +3. grep "handleRetryError" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (retry handler) +4. Count error.type checks: should have file, network, download, playback, general + + +- ERROR_TYPES constant defined (file, network, download, playback, general) +- Error display color-coded (red for critical, yellow for warnings) +- Retry button for recoverable errors (network, download, file) +- handleRetryError implemented with type-specific retry logic +- Network resilience: stop polling after 3 consecutive failures +- Edge case validation: null jamClient, invalid jamTrack data +- User-friendly error messages +- Dismiss button for all errors + +
+ + + Task 2: Apply performance optimizations + jam-ui/src/components/client/JKSessionJamTrackPlayer.js + +Apply React performance optimizations from Phase 3: + +**1. Add useMemo for computed values:** +```javascript +// Format time with useMemo +const formattedPosition = useMemo( + () => formatTime(jamTrackState.currentPositionMs), + [jamTrackState.currentPositionMs] +); + +const formattedDuration = useMemo( + () => formatTime(jamTrackState.durationMs), + [jamTrackState.durationMs] +); + +// Compute progress percentage +const progressPercent = useMemo(() => { + if (!jamTrackState.durationMs || jamTrackState.durationMs === 0) return 0; + return Math.round((jamTrackState.currentPositionMs / jamTrackState.durationMs) * 100); +}, [jamTrackState.currentPositionMs, jamTrackState.durationMs]); +``` + +**2. Verify all handlers use useCallback:** +- handlePlay ✓ (already has useCallback) +- handlePause ✓ +- handleStop ✓ +- handleSeek ✓ +- handleMixdownChange ✓ +- handleCancelDownload ✓ +- handleRetryDownload ✓ +- handleRetryError (add if not already) +- buildFqId ✓ + +**3. Verify conditional state updates in polling:** +```javascript +// Already implemented in Plan 3, verify: +if ( + position !== jamTrackState.currentPositionMs || + duration !== jamTrackState.durationMs +) { + dispatch(setJamTrackState({ ... })); +} +``` + +**4. Verify visibility-aware polling:** +```javascript +// Already implemented in Plan 3, verify: +const isVisible = document.visibilityState === 'visible'; +const pollInterval = isVisible ? 500 : 2000; +``` + +**5. Remove all diagnostic logging:** +```javascript +// Search for console.log statements and remove unless they're error logs +// Keep: console.error for error tracking +// Remove: console.log for debugging (e.g., "[JamTrack] Not synchronized, will download") +``` + +**6. Add React.memo if component re-renders unnecessarily:** +```javascript +// At export, wrap in memo if needed: +export default React.memo(JKSessionJamTrackPlayer); +``` + +**Follow Phase 3 patterns:** +- useMemo for computed values (formatted time, progress percentage) +- useCallback for all event handlers +- Conditional state updates (only update if values changed) +- Visibility-aware polling (500ms/2000ms) +- Clean console output (no diagnostic logs) + + +1. grep "useMemo" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (computed values memoized) +2. grep "useCallback" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (all handlers wrapped) +3. grep "console.log" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (should only find console.error, not console.log) +4. grep "React.memo" jam-ui/src/components/client/JKSessionJamTrackPlayer.js (component memoized) + + +- useMemo added for formattedPosition, formattedDuration, progressPercent +- All handlers verified to use useCallback +- Conditional state updates verified in polling +- Visibility-aware polling verified (500ms/2000ms) +- Diagnostic console.log removed (only console.error remains) +- Component wrapped in React.memo at export +- Performance optimizations match Phase 3 Backing Track quality + + + + + Complete JamTrack player with download, playback, mixdown selection, error handling, and performance optimizations + +**Test Environment:** +1. Ensure JamKazam native client is running +2. Start jam-ui dev server: `cd jam-ui && npm run start` +3. Navigate to a session with JamTracks available +4. Open JamTrack player (modal or popup mode) + +**Test Cases (40+ scenarios):** + +**Initialization (4 cases):** +- [ ] JamTrack opens in modal mode +- [ ] JamTrack opens in popup mode (if supported) +- [ ] JamTrack already synced → shows player immediately +- [ ] JamTrack not synced → shows download UI + +**Download/Sync (6 cases):** +- [ ] Download progress shows percentage (0-100%) +- [ ] Download progress shows step indicator ("Step X of Y") +- [ ] Download state transitions: checking → downloading → keying → synchronized +- [ ] Download completes, player becomes available +- [ ] Cancel button works during download (stops download) +- [ ] Download error shows error message with retry button + +**Playback Controls (7 cases):** +- [ ] Play button starts playback +- [ ] Pause button pauses playback +- [ ] Stop button stops playback and resets position to 0 +- [ ] Play after stop restarts from beginning +- [ ] Resume after pause continues from pause point +- [ ] Buttons disabled during operations (isOperating flag) +- [ ] Buttons disabled during sync check (isLoadingSync flag) + +**Seek Slider (4 cases):** +- [ ] Seek slider moves during playback +- [ ] Drag seek slider to new position while playing → seeks immediately +- [ ] Drag seek slider while paused → updates UI, applies on resume (UAT-003 fix) +- [ ] Seek slider disabled before duration loads + +**Mixdown Selection (5 cases):** +- [ ] Mixdown picker shows available mixdowns +- [ ] Mixdowns sorted: master → custom mixes → stems +- [ ] Visual indicators show mixdown types (🎵 🎨 🎸) +- [ ] Select different mixdown → playback stops and restarts +- [ ] Mixdown picker disabled during operations + +**Display (5 cases):** +- [ ] Position displays in MM:SS format +- [ ] Duration displays in MM:SS format +- [ ] Seek slider position matches audio position +- [ ] Download progress updates in real-time +- [ ] Error messages display clearly + +**Error Handling (5 cases):** +- [ ] Invalid JamTrack shows error +- [ ] Network failure during download shows error with retry +- [ ] Network failure during polling (after 3 failures) shows error +- [ ] Null jamClient shows "Native client not available" error +- [ ] Error dismiss button clears error + +**Performance (4 cases):** +- [ ] No lag during playback +- [ ] Polling doesn't cause CPU spikes (check Activity Monitor) +- [ ] Visibility-aware polling: 500ms visible, 2000ms hidden (check intervals) +- [ ] Multiple JamTracks can be opened sequentially without issues + +**Cleanup (4 cases):** +- [ ] Close modal → playback stops +- [ ] Close popup → playback stops +- [ ] Open different JamTrack → previous stops +- [ ] No console errors after interactions + +**Known Issues from Phase 3 (check if they apply):** +- [ ] End-of-track restart requires double-click? (Minor - defer if present) +- [ ] Loop functionality? (Not in scope for Phase 5) +- [ ] Volume control in popup mode? (Not in scope - defer if issue) + +**Pass Criteria:** +- Core functionality works (download, playback, mixdown selection) +- No critical errors (red errors should be rare, retryable) +- Performance acceptable (no lag, smooth playback) +- Known issues documented (minor issues OK to defer) + +**If issues found:** +- CRITICAL (blocks functionality): Describe issue, I'll fix immediately +- MEDIUM (feature not working): Describe issue, decide fix now or defer +- MINOR (UX inconvenience): Document in ISSUES.md, defer to future work + + Type "approved" if UAT passes, or describe issues found with severity (critical/medium/minor) + + +
+ + +Before presenting UAT checkpoint: +- [ ] Error handling comprehensive (5 error types, retry logic, network resilience) +- [ ] Performance optimizations applied (useMemo, useCallback, conditional updates, clean logs) +- [ ] All code follows Phase 3 patterns +- [ ] Component ready for manual testing + + + + +- Comprehensive error handling: 5 error types (file, network, download, playback, general) +- Error display color-coded (red for critical, yellow for warnings) +- Retry logic for recoverable errors +- Network resilience (stop polling after 3 failures) +- Edge case validation (null jamClient, invalid data) +- Performance optimizations: useMemo, useCallback, conditional updates, visibility-aware polling +- Diagnostic logging removed (only console.error) +- Component wrapped in React.memo +- UAT conducted with 40+ test cases +- Known issues documented (if any) +- Phase 5 complete: JamTrack player production-ready + + + +After UAT completion, create `.planning/phases/05-jamtrack-implementation/05-05-SUMMARY.md`: + +# Phase 5 Plan 5: Error Handling & Final UAT Summary + +**[Substantive one-liner - production-ready JamTrack player]** + +## Accomplishments + +- Comprehensive error handling with 5 error types +- Performance optimizations applied +- UAT conducted: [X]/40+ test cases passed +- Known issues documented (if any) + +## Files Created/Modified + +- `jam-ui/src/components/client/JKSessionJamTrackPlayer.js` - Error handling + performance + +## UAT Results + +**Working Features:** [list] + +**Known Issues (Deferred):** [if any, with severity and root cause] + +## Issues Encountered + +[Problems during UAT and resolutions] + +## Phase 5 Complete + +JamTrack player is production-ready with: +- ✅ Download/sync with progress tracking +- ✅ Playback controls (play/pause/stop/seek) +- ✅ Mixdown selection (master/custom/stems) +- ✅ Comprehensive error handling +- ✅ Performance optimizations +- ⚠️ Known issues documented (if any) + +**If UAT revealed issues requiring fixes:** Create `05-05-ISSUES.md` documenting deferred issues for future work. + +## Next Phase Readiness + +Phase 5 complete. Project ready for next phase (Phase 6: Metronome Research & Design). +