- audioService: replace 'as any' with 'as unknown as AudioService' in
resetInstance() to satisfy @typescript-eslint/no-explicit-any
- SongPage: add isReady to spacebar useEffect deps so the handler always
sees the current readiness state
- useWaveform: add containerRef to deps (stable ref, safe to include);
suppress exhaustive-deps for options.onReady with explanation — adding
an un-memoized callback would cause initialization on every render
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Bug 1 — playback stops on navigation:
WaveSurfer v7 creates its <audio> element inside the container div. When
SongPage unmounts, the container is removed from the DOM, taking the audio
element with it and stopping playback. Fix: AudioService owns a persistent
hidden <audio> element on document.body and passes it to WaveSurfer via the
`media` option. WaveSurfer uses it for playback but does not destroy it on
WaveSurfer.destroy(), so audio survives any number of navigations.
Bug 2 — stale playhead/duration when opening a new song:
initialize() called destroyWaveSurfer() but never reset the store, so the
previous song's currentTime, duration, and isPlaying leaked into the new song's
load sequence. Fix: reset those three fields in the store immediately after
tearing down the old WaveSurfer instance. cleanup() also now resets duration.
Bug 3 — excessive console noise on mobile:
Remove console.warn from play() (silent return when not ready) and from
useWaveform's play() wrapper. Only console.error on actual errors remains.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
playerStore: remove currentPlayingSongId/currentPlayingBandId/setCurrentPlayingSong.
Single pair (currentSongId/currentBandId) now set exclusively when play() is
called, not when the page opens. This means MiniPlayer and sidebar links only
appear after audio has been started — correct UX for a "now playing" widget.
audioService: play() calls setCurrentSong instead of setCurrentPlayingSong;
cleanup() clears it. Remove isReadyForPlayback() and canAttemptPlayback()
aliases — all callers now use isWaveformReady() directly.
useWaveform: remove setCurrentSong call from init (store updated by play()
now); restore-playback snapshot reads currentSongId/currentBandId.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
useWaveform.ts:
- Remove requestAnimationFrame polling loop that was re-running after every
re-initialization and leaking across renders when cleanup didn't fire
- Remove local useState for isPlaying/currentTime/duration; these now come
directly from usePlayerStore selectors — WaveSurfer event handlers in
AudioService already write to the store, so no intermediate sync needed
- The useEffect is now a clean async init only; no cleanup needed (AudioService
persists intentionally across page navigations)
tests/:
- Delete 3 obsolete test files that tested removed APIs (logging system,
setupAudioContext, ensureAudioContext, initializeAudioContext)
- Add tests/audioService.test.ts: 25 tests covering initialize(), play(),
pause(), seekTo(), cleanup(), and all WaveSurfer event→store mappings
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
audioService.ts rewritten from ~850 lines to ~130:
- Remove custom logging system with throttle that suppressed ERROR logs
- Remove AudioContext management entirely (initializeAudioContext,
handleAudioContextResume, setupAudioContext, shareAudioContextWithWaveSurfer,
ensureAudioContext). WaveSurfer v7 owns its AudioContext; fighting it caused
prod/dev divergence and silent failures.
- Replace 5-state InitializationState machine + split promise with a single
isReady boolean set in the 'ready' event handler
- Remove retry/debounce logic from play() — these are UI concerns
- Remove dead methods: canPlayAudio (always returned true), getWaveSurferVersion,
updatePlayerState, getAudioContextState, setLogLevel
- Extract destroyWaveSurfer() helper so cleanup is one place
- MiniPlayer now passes songId/bandId to play() (was calling with no args)
- SongPage spacebar handler simplified: just checks isReady from hook
- SongPage no longer imports audioService directly
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- useWaveform: remove globalCurrentTime/globalIsPlaying from useEffect deps;
WaveSurfer was re-initializing every 250ms while audio played. Dep array
is now [url, songId, bandId]. Store reads inside the effect use getState()
snapshots instead of reactive values.
- useWaveform: move animationFrameId outside the async function so the
useEffect cleanup can actually cancel the RAF loop. Previously the cleanup
was returned from the inner async function and React never called it —
loops accumulated on every re-render.
- audioService: remove isDifferentSong + cleanup() call from play(). cleanup()
set this.wavesurfer = null and then play() immediately called
this.wavesurfer.play(), throwing a TypeError on every song switch.
- audioService: replace new Promise(async executor) anti-pattern in
initialize() with a plain executor + extracted onReady().catch(reject) so
errors inside the ready handler are always forwarded to the promise.
- audioService: remove currentPlayingSongId/currentPlayingBandId private
fields whose only reader was the deleted isDifferentSong block.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Connect MiniPlayer play/pause buttons to audioService
- Improve audio context management with fallback creation
- Fix state synchronization with interval-based readiness checks
- Add error handling and user feedback for playback issues
- Enhance mobile browser support with better audio context handling
Fixes playback issues in SongView where controls were not working and
state synchronization between UI and player was unreliable.
- New split layout: waveform/transport/queue left, comment panel right
- Avatar pins above waveform positioned by timestamp with hover tooltips
- Transport bar: speed selector, ±30s skip, 46px amber play/pause, volume
- Comment compose: live timestamp pill, suggestion/issue/keeper tag buttons
- Comment list: per-author colour avatars, amber timestamp seek chips,
playhead-proximity highlight, delete only shown on own comments
- Queue panel showing other songs in the same session
- Waveform colours updated to amber/dim palette (104px height)
- Add GET /songs/{song_id} endpoint for song metadata
- Add tag field to SongComment (model, schema, router, migration 0005)
- Fix migration 0005 down_revision to use short ID "0004"
- Fix ESLint no-unused-expressions in keyboard shortcut handler
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Auth / token storage:
- JWT is now set as an httpOnly Secure SameSite=Lax cookie on login
- Add POST /auth/logout endpoint that clears the cookie
- get_current_member falls back to rh_token cookie when no Authorization header
- WebSocket auth now accepts cookie (rh_token) or optional ?token= query param
- Frontend removes all localStorage JWT access; uses credentials:"include" on
every fetch so the httpOnly cookie is sent automatically
- Replace clearToken() with logout() that calls the server logout endpoint
- Non-sensitive rh_session flag in localStorage used only for client-side routing
Rate limiting:
- Add slowapi>=0.1.9 dependency
- /auth/login limited to 10 req/min per IP
- /auth/register limited to 5 req/min per IP
Nginx security headers:
- Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
X-XSS-Protection, Permissions-Policy to all responses
SSE error leakage:
- songs.py nc-scan/stream no longer leaks str(exc) to clients
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add author_avatar_url to API schema and frontend interface
- Capture current playhead timestamp when creating comments
- Display user avatars in waveform markers instead of placeholders
- Improve marker visibility with better styling (size, borders, shadows)
- Fix TypeScript type errors for nullable timestamps
- Add debug logging for troubleshooting
This implements the full comment waveform integration as requested:
- Comments are created with exact playhead timestamps
- Waveform markers show at correct positions with user avatars
- Clicking markers scrolls to corresponding comments
- Backward compatible with existing comments without timestamps
- Version switching now stops playback and resets UI state
- Playback must be started manually after version switch
- Added space key shortcut for play/pause toggle
- UI elements correctly reflect playback state
Fixes: Play/pause controls work correctly after version switching
Improves: User experience with keyboard shortcuts
- Added wasPlayingRef to preserve playback state across version changes
- Auto-play new waveform if previous version was playing
- Ensure play/pause buttons work correctly after version switch
- Added WebSocket proxy configuration for real-time features
Fixes: Version switching now preserves playback state
Todo: Test edge cases and finalize implementation
WaveSurfer makes plain browser fetches without Authorization headers,
causing 401s on the stream endpoint. The stream endpoint now accepts
a ?token= query param in addition to the Authorization header, and
proxies audio bytes directly through FastAPI instead of redirecting to
raw WebDAV (which would require a second Nextcloud auth challenge).
Falls back to nc_file_path if HLS transcoding hasn't run yet.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>