diff --git a/.claude/settings.local.json b/.claude/settings.local.json index c90b8dea9..1805afb17 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -43,7 +43,8 @@ "Skill(gsd:execute-plan)", "Skill(gsd:plan-phase)", "Skill(gsd:progress)", - "Bash(git rev-parse:*)" + "Bash(git rev-parse:*)", + "Bash(node --check:*)" ] } } diff --git a/jam-ui/src/components/client/JKSessionJamTrackPlayer.js b/jam-ui/src/components/client/JKSessionJamTrackPlayer.js index e405dd969..6ed6336fb 100644 --- a/jam-ui/src/components/client/JKSessionJamTrackPlayer.js +++ b/jam-ui/src/components/client/JKSessionJamTrackPlayer.js @@ -487,11 +487,32 @@ const JKSessionJamTrackPlayer = ({

{downloadState.state === 'checking' && 'Checking sync status...'} + {downloadState.state === 'packaging' && 'Your JamTrack is currently being created in the JamKazam server'} {downloadState.state === 'downloading' && 'Downloading JamTrack...'} {downloadState.state === 'keying' && 'Requesting decryption keys...'} {downloadState.state === 'error' && 'Download Failed'}

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

+ {downloadState.signing_state === 'SIGNED' && 'Package ready, starting download...'} + {downloadState.signing_state !== 'SIGNED' && downloadState.signing_state && `Status: ${downloadState.signing_state}`} + {!downloadState.signing_state && 'Preparing your JamTrack...'} +

+ {downloadState.packaging_steps > 0 && ( +

+ Step {downloadState.current_packaging_step} of {downloadState.packaging_steps} +

+ )} +
+
+ Packaging... +
+
+
+ )} + {downloadState.state === 'downloading' && (
{ }); } +export const enqueueMixdown = (packageId) => { + return new Promise((resolve, reject) => { + apiFetch(`/mixdowns/${packageId}/enqueue`, { + method: 'POST' + }) + .then(response => resolve(response)) + .catch(error => reject(error)); + }); +}; + export const addJamtrackToShoppingCart = (options = {}) => { return new Promise((resolve, reject) => { apiFetch(`/shopping_carts/add_jamtrack`, { diff --git a/jam-ui/src/hooks/useSessionWebSocket.js b/jam-ui/src/hooks/useSessionWebSocket.js index 4627fd7c4..a4f7573e5 100644 --- a/jam-ui/src/hooks/useSessionWebSocket.js +++ b/jam-ui/src/hooks/useSessionWebSocket.js @@ -161,6 +161,38 @@ export const useSessionWebSocket = (sessionId) => { } }, + // Phase 5 Fix: Handle packaging notifications from server + // Listens for mixdown package creation/signing progress + SUBSCRIBE_NOTIFICATION: (data) => { + console.log('[JamTrack] Packaging notification received:', data); + + // Update packaging progress in Redux + // data.body contains: signing_state, packaging_steps, current_packaging_step + if (data.type === 'mixdown' && data.body) { + dispatch(setDownloadState({ + signing_state: data.body.signing_state, + packaging_steps: data.body.packaging_steps || 0, + current_packaging_step: data.body.current_packaging_step || 0 + })); + + console.log(`[JamTrack] Packaging progress: ${data.body.signing_state}, step ${data.body.current_packaging_step}/${data.body.packaging_steps}`); + + // If packaging failed, set error state + if (data.body.signing_state === 'ERROR' || + data.body.signing_state === 'SIGNING_TIMEOUT' || + data.body.signing_state === 'QUEUED_TIMEOUT' || + data.body.signing_state === 'QUIET_TIMEOUT') { + dispatch(setDownloadState({ + state: 'error', + error: { + type: 'download', + message: `Packaging failed: ${data.body.signing_state}` + } + })); + } + } + }, + // Connection events connectionStatusChanged: (data) => { console.log('Connection status changed:', data); diff --git a/jam-ui/src/store/features/mediaSlice.js b/jam-ui/src/store/features/mediaSlice.js index cb21e91cf..7613ae84f 100644 --- a/jam-ui/src/store/features/mediaSlice.js +++ b/jam-ui/src/store/features/mediaSlice.js @@ -1,4 +1,5 @@ import { createSlice, createAsyncThunk } from '@reduxjs/toolkit'; +import { enqueueMixdown } from '../../helpers/rest'; // Async thunks for media actions export const openBackingTrack = createAsyncThunk( @@ -49,15 +50,8 @@ export const loadJamTrack = createAsyncThunk( export const downloadJamTrack = createAsyncThunk( 'media/downloadJamTrack', - async ({ jamTrack, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue }) => { + async ({ jamTrack, mixdownId, fqId, jamClient }, { dispatch, rejectWithValue, getState }) => { try { - dispatch(setDownloadState({ - jamTrackId: jamTrack.id, - mixdownId, - fqId, - state: 'downloading', - progress: 0 - })); // Get client sample rate for package selection (pickMyPackage logic) const rawSampleRate = await jamClient.GetSampleRate(); @@ -126,6 +120,81 @@ export const downloadJamTrack = createAsyncThunk( const packageId = compatiblePackage.id; + // ========== PACKAGING PHASE ========== + // Before downloading, package must be created/signed on server + // This matches legacy download_jamtrack.js.coffee behavior + + dispatch(setDownloadState({ + jamTrackId: jamTrack.id, + mixdownId, + fqId, + packageId, + state: 'packaging', + progress: 0, + packaging_steps: 0, + current_packaging_step: 0, + signing_state: null + })); + + // Enqueue mixdown for server-side packaging + console.log(`[JamTrack] Enqueueing package ${packageId} for packaging`); + try { + await enqueueMixdown(packageId); + } catch (err) { + console.error('[JamTrack] Failed to enqueue mixdown:', err); + throw new Error(`Failed to start packaging: ${err.message}`); + } + + // Wait for packaging to complete (signing_state === 'SIGNED') + // WebSocket handler will update downloadState as packaging progresses + console.log('[JamTrack] Waiting for package to be signed by server...'); + + const waitForPackaging = () => { + return new Promise((resolve, reject) => { + const timeout = setTimeout(() => { + reject(new Error('Packaging timeout after 60 seconds')); + }, 60000); // 60 second timeout + + const checkInterval = setInterval(() => { + const state = getState(); + const { signing_state, state: downloadStateValue } = state.media.downloadState; + + // Check for success + if (signing_state === 'SIGNED') { + clearTimeout(timeout); + clearInterval(checkInterval); + console.log('[JamTrack] Package signed successfully, proceeding to download'); + resolve(); + } + + // Check for errors + if (signing_state === 'ERROR' || signing_state === 'SIGNING_TIMEOUT' || + signing_state === 'QUEUED_TIMEOUT' || signing_state === 'QUIET_TIMEOUT') { + clearTimeout(timeout); + clearInterval(checkInterval); + reject(new Error(`Packaging failed with state: ${signing_state}`)); + } + + // Check if download state shows error + if (downloadStateValue === 'error') { + clearTimeout(timeout); + clearInterval(checkInterval); + reject(new Error('Packaging failed')); + } + }, 500); // Check every 500ms + }); + }; + + await waitForPackaging(); + + // ========== DOWNLOAD PHASE ========== + // Package is now signed and ready to download + + dispatch(setDownloadState({ + state: 'downloading', + progress: 0 + })); + // Set up global callbacks for native client window.jamTrackDownloadProgress = (percent) => { dispatch(setDownloadState({ progress: percent })); @@ -266,10 +335,15 @@ const initialState = { jamTrackId: null, mixdownId: null, fqId: null, - state: 'idle', // 'idle' | 'checking' | 'downloading' | 'keying' | 'synchronized' | 'error' + packageId: null, // Package being packaged/downloaded + state: 'idle', // 'idle' | 'checking' | 'packaging' | 'downloading' | 'keying' | 'synchronized' | 'error' progress: 0, currentStep: 0, totalSteps: 0, + // Packaging-specific fields (from legacy JamTrackStore) + packaging_steps: 0, // Total packaging steps + current_packaging_step: 0, // Current step in packaging + signing_state: null, // 'SIGNING_TIMEOUT' | 'QUEUED_TIMEOUT' | 'QUIET_TIMEOUT' | 'ERROR' | 'SIGNED' | null error: null }, @@ -331,10 +405,14 @@ export const mediaSlice = createSlice({ jamTrackId: null, mixdownId: null, fqId: null, + packageId: null, state: 'idle', progress: 0, currentStep: 0, totalSteps: 0, + packaging_steps: 0, + current_packaging_step: 0, + signing_state: null, error: null }; },