- Use FTUESave(true) instead of TrackSaveAssignments() to properly
persist VST assignments to the profile file
- Always call VSTLoad() when modal opens if VST not already loaded,
removing unreliable hasVstAssignment() check
- Pass correct trackIndex to JKSessionPluginModal for multi-track support
- Add trackIndex prop to track data in useMixerHelper for VST operations
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add mixType prop to JKSessionAudioInputs to select master/personal mixer
- Store both masterMixers and personalMixers in myTracks for each track
- Fix async state issue in fillTrackVolumeObject by returning volumeObj directly
- Change SessionTrackGain to use useLayoutEffect for synchronous visual updates
- Fix controlGroup to null for individual track controls (fixes persistence)
- Prevent fader reset when mixer temporarily becomes undefined during re-renders
Audio Inputs now uses master mixer, Session Mix uses personal mixer,
matching the legacy app behavior. Both faders persist independently.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
On page load, instrument_id from server is already a string ("piano").
After user selection, it's a numeric client_id (61) that needs conversion.
Now checks type before converting to handle both cases correctly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
instrument_id is stored as numeric client_id (e.g., 61 for Piano)
but the icon map uses server format strings ("piano"). Added conversion
using convertClientInstrumentToServer before passing to icon lookup.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add updateParticipantTrackInstrument reducer to update sessionData.participants
- Dispatch both participant track and userTracks updates on instrument save
- Fix syncTracksToServer call to pass clientId instead of jamClient object
- UI now reflects instrument changes without needing page refresh
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add registeredCallbacksRef alongside registeredCallbacks state
- Update unregisterMessageCallbacks to read from ref for cleanup
- Store callbacks to both state and ref during registration
- Ensures cleanup works reliably on all exit paths (browser close, navigation)
- Destructure removeVuState from useVuContext alongside updateVU3
- Add previousMixerIdsRef to track mixer IDs between renders
- Add cleanup useEffect that detects removed mixers
- Call removeVuState when mixer is removed from allMixers
- Only run cleanup when mixers are ready (isReadyRedux is true)
This prevents unbounded growth of vuStates object as tracks join/leave.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add removeVuState callback to clean up vuStates entries
- Export removeVuState alongside updateVuState
- Uses functional setState for React state batching compatibility
- Maintains immutability by creating shallow copy before deletion
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Add test verifying Cancel button closes modal without making API call
- Fill description field before cancel to verify changes are discarded
- Use page.route() to track any PUT requests (should be none)
- Verify modal closes by checking privacy select visibility
- Add test for save button making PUT /sessions/{id} API call
- Verify payload includes description and privacy fields (musician_access, approval_required)
- Use page.route() to intercept and capture API request
- Verify modal closes after successful save
- Fix locators to use privacy select instead of modal header text (avoids toast collision)
UNIT-01: Modal renders with currentSession props
- Test privacy value displayed from currentSession
- Test description value displayed from currentSession
- Test modal title renders
UNIT-02: Save functionality
- Test onSave called with correct payload on save click
- Test onSave called with updated values after user changes
UNIT-03: Loading state
- Test save button disabled when loading
- Test cancel button disabled when loading
- Test description textarea disabled when loading
- Test "Saving..." text shown when loading
- Create __tests__ directory for client components
- Add test file with imports and mocks for react-i18next
- Create renderModal helper function with default props
- Add placeholder test verifying setup works
Connect the existing Resync button to the resyncAudio hook from useGearUtils.
Calls jamClient.SessionAudioResync() via native C++ bridge to perform audio
resync, with loading state and error handling via toast notifications.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Problem: After page reload, the unread badge on chat button disappeared
even though there were unread messages.
Root cause:
1. unreadCounts was reset to {} on page reload
2. fetchChatHistory was only called when chat window opened
3. By that time, openChatWindow already reset the count to 0
Fix:
1. Fetch chat history when session joins (not just when chat opens)
2. In fetchChatHistory.fulfilled, calculate unread count based on
lastReadAt timestamp from localStorage
3. Only calculate unread if chat window is NOT open for that channel
This ensures the badge shows correct unread count after page reload.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The previous fix only added deduplication by attachmentId in addMessageFromWebSocket
and fetchChatHistory.fulfilled, but missed the uploadAttachment.fulfilled handler.
Race condition: WebSocket message can arrive BEFORE the upload API returns.
When this happens:
1. WebSocket delivers message with id='456', attachmentId=123
2. addMessageFromWebSocket adds it to state
3. Upload API returns
4. uploadAttachment.fulfilled only checked for id='attachment-123' → not found
5. Duplicate message added
Fix: Also check by attachmentId in uploadAttachment.fulfilled handler.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The optimistic message uses 'attachment-{notation.id}' as ID while
REST API and WebSocket use the chat message ID. This caused duplicates
when fetchChatHistory ran after an optimistic upload.
Now deduplication checks both message ID and attachmentId for attachment
messages, preventing duplicates regardless of ID format.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create error-handling.spec.ts with 5 test scenarios
- Test REQ-5.1: File size exceeded validation
- Test REQ-5.4: Upload success toast with auto-dismiss
- Test REQ-5.3: Network error handling with retry capability
- Test REQ-5.5: S3 404 error handling (missing file)
- Test edge case: Prevent rapid clicks during upload
- Uses test-helpers.ts for login and session creation
- Update validateFileSize error: 'File size exceeds 10 MB limit' (REQ-5.1)
- Update validateFileType error: 'File type not supported. Allowed: [list]' (REQ-5.2)
- Hardcode error messages for clarity per requirements
- Update unit tests to match new error messages
- All 37 unit tests passing
- Add selectUploadStatus selector to track upload state transitions
- Track previous upload status with useRef
- Show toast.success when upload completes (uploading -> idle)
- Auto-dismiss after 3 seconds per REQ-5.4
- Success toast provides clear feedback on successful file uploads
Changed JKSessionChatButton from bare icon to proper Button component:
- Uses Button from reactstrap with btn-custom-outline class
- Matches other nav buttons (Invite, Volume, Video, etc.)
- Icon sized at 16x16px with 0.2rem right margin
- "Chat" text label added
- Unread badge repositioned for button layout
- Reduced opacity when window is open (visual feedback)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Backend's check_session filter expects 'music_session' parameter:
@music_session = ActiveMusicSession.find(params[:music_session])
But we were sending 'session_id', causing ActiveMusicSession.find(nil)
to fail.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The useEffect condition allowed fetching when fetchStatus was 'failed',
causing infinite retry loops when the API returned an error.
Changed condition from:
fetchStatus !== 'loading' && fetchStatus !== 'succeeded'
To:
fetchStatus === 'idle'
This ensures we only fetch once per channel on initial load, and don't
retry automatically on failure.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The fetchChatHistory thunk needs sessionId to determine channel type:
- With sessionId: fetches 'session' channel messages
- Without sessionId: fetches 'global' channel messages
The useEffect was only passing `channel` (which is the sessionId for
session chats), but not the `sessionId` parameter. This caused the
API to fetch global chat instead of session chat, so session messages
disappeared after page reload.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
The chat history API returns { chats: [...], next: ... } but the
fetchChatHistory.fulfilled handler was expecting { messages: [...] }.
This caused a TypeError when opening the chat window because
`messages` was undefined.
Fixed:
- sessionChatSlice.js: Extract `chats` from payload, default to []
- Updated all test payloads to use `chats` field name
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Issues found during UAT:
1. Uploader doesn't see their own attachment message
2. Chat history doesn't load on page refresh/rejoin
Root causes:
1. Backend's server_publish_to_session excludes sender from WebSocket
broadcast (exclude_client_id: sender[:client_id])
2. fetchChatHistory was imported but never called in JKChatMessageList
Fixes:
- Add optimistic message in uploadAttachment.fulfilled for uploader
Since sender is excluded from WebSocket, we add the message locally
using the MusicNotation response + user info
- Add useEffect in JKChatMessageList to dispatch fetchChatHistory
when channel becomes active
Technical details:
- Pass userId/userName to uploadAttachment thunk for message construction
- Use 'attachment-{notation.id}' as message ID to avoid collision
- Fetch history when fetchStatus is not 'loading' or 'succeeded'
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Create test/attachments/real-time-sync.spec.ts
- Test WebSocket message receipt for attachments
- Test deduplication of duplicate messages
- Test REST API and WebSocket message deduplication
- Test attachment display in chat
- Uses Redux dispatch simulation approach for CI compatibility
- Transform nested music_notation object from REST API to flat attachment fields
- Map music_notation.id → attachmentId, file_name → attachmentName, attachment_type → attachmentType
- Include purpose field from API response root
- Set attachmentSize to null (not available from REST API, only WebSocket)
- Matches WebSocket message format for consistent JKChatMessage rendering
Tests:
- Add test for REST API attachment transformation
- Fix pre-existing test bugs: incorrect sendMessage.fulfilled payload format
- Fix pre-existing test bug: fetchChatHistory deduplication test used wrong format
- All 89 tests now pass (previously 3 failures)
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
- Add flexWrap to header (name + timestamp wrap on narrow windows)
- Change filename maxWidth from 200px to 100% (takes available width)
- Add flexShrink: 0 to file size and paperclip icon (never shrink)
- Add fontSize: 13px to file size for consistent sizing
- Remove marginRight from paperclip icon (gap handles spacing)
- Ensure minWidth: 0 on content container for text-overflow to work
- Add comment clarifying header layout
- Layout responsive at different window sizes with proper truncation
- Import getMusicNotationUrl REST helper and useState/useCallback hooks
- Add handleAttachmentClick handler that fetches signed S3 URL and opens in new tab
- Implement loading state (isLoadingUrl) to prevent rapid clicks
- Make filename a clickable link with underline and blue color
- Add error handling that logs to console without crashing
- Show 'wait' cursor during URL fetch
- Browser handles file display based on Content-Type (PDF viewer, image display, audio player)
- Import formatFileSize from attachmentValidation service
- Add isAttachmentMessage helper to detect attachment messages
- Render attachment messages with light blue background (#e3f2fd)
- Display paperclip icon with accessible aria-label
- Show '[name] attached [filename]' format with file size if available
- Update PropTypes to include optional attachment fields
- Maintain React.memo for performance
- Add attachment fields to CHAT_MESSAGE handler transformation
- Include attachmentId, attachmentName, attachmentType, purpose, attachmentSize
- Fields null/undefined for regular text messages
- WebSocket payload maps to Redux format with attachment metadata
- Import selectIsUploading and selectUploadFileName from sessionChatSlice
- Import JKChatUploadProgress component
- Add useSelector hooks for upload state (isUploading, uploadFileName)
- Render JKChatUploadProgress at end of message list when upload in progress
- Upload indicator appears at bottom of chat for easy visibility
- Conditional rendering: only shows when isUploading is true AND fileName exists
- Add imports for uploadAttachment, validation, and upload state selectors
- Add attachFileInputRef for hidden file input element
- Add handleAttachClick to trigger file dialog
- Add handleFileSelect with validateFile pre-upload validation
- Add useEffect to display upload error toasts
- Add hidden file input with accept attribute for valid file types
- Update Attach button with onClick handler and disabled state during upload
- Button shows 'Uploading...' text when upload is in progress
- Chat window opens automatically when upload starts
- File validation prevents invalid uploads (size/type) with immediate error feedback
Changes:
- .gitignore: Add .run-claude.sh to ignored files
- JKSessionScreen.js: Add "-DEBUG-" prefix to console logs for easier filtering
- user1.json, user2.json: Update test authentication state
- 11-VERIFICATION.md: Add verification document for Phase 11
These are maintenance updates from chat feature development and testing.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
When profile guard fails (single-player profile detected):
- Show JavaScript alert explaining the issue
- Leave session cleanly via handleLeaveSession()
- Redirect to dashboard (/) instead of /client (404)
Alert message explains:
- Audio profile not suitable for multi-user sessions
- Need proper audio interface or create private session
TODO: Replace alert() with proper modal dialog component
that offers options like legacy app (create private session,
go to audio settings, cancel).
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>