Commit Graph

39 Commits

Author SHA1 Message Date
Mistral Vibe
ba90f581ae Remove --reload-dir from API dev CMD
uvicorn's path check for --reload-dir fails in Podman rootless even
though uv sync can read the same path. Drop the flag — the editable
install already points uvicorn's watcher at /app/src implicitly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:21:52 +02:00
Mistral Vibe
a8cbd333d2 Fix dev container startup for both API and web
API:
- Add python symlink (python3-slim has no bare 'python') so uv doesn't
  invalidate the baked venv on every container start
- Copy src/ before uv sync so hatchling installs rehearsalhub as a
  proper editable install (.pth pointing to /app/src) — previously
  sync ran with no source present, producing a broken empty wheel
- Remove ENV PYTHONPATH workaround (no longer needed with correct install)
- Add --reload-dir /app/src to scope uvicorn's file watcher to the
  mounted source directory

Web:
- Add COPY . . after npm install so index.html and vite.config.ts are
  baked into the image — without them Vite ignored port config and fell
  back to 5173 with no entry point

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:14:21 +02:00
Mistral Vibe
261942be03 Fix dev compose: API PYTHONPATH and web volume permissions
API: bake ENV PYTHONPATH=/app/src into the development Dockerfile stage
so it's available to uvicorn's WatchFiles reloader subprocess — relying
on compose env vars isn't reliable across process forks.

Web: replace ./web:/app bind mount (caused EACCES in Podman rootless due
to UID mismatch) with ./web/src:/app/src — this preserves the container's
package.json and node_modules while still giving Vite live access to
source files for HMR.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:02:36 +02:00
Mistral Vibe
3fa734c686 Fix 403 for invited members streaming audio and 500 on invite listing
Invited members have no Nextcloud credentials of their own — stream and
waveform endpoints now use the file uploader's NC credentials instead of
the current member's. Falls back to the current member if uploaded_by is
null.

The invite listing/info endpoints were comparing timezone-aware
expires_at values against naive datetime.now(), causing a TypeError (500).
Fixed by using datetime.now(timezone.utc) throughout bands.py and
invites.py.

Also removes leftover debug logging from versions.py.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:03:42 +02:00
Mistral Vibe
b3033cf43e Fix NameError: BandRepository not defined
- Added import for BandRepository in stream_version function
- Import at function level to avoid circular import issues
- Now debug logging will work correctly

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 12:56:16 +02:00
Mistral Vibe
a36d769478 Fix NameError in versions.py
- Fixed undefined 'song' and 'role' variables in stream_version
- Now properly gets song and role from _get_version_and_assert_band_membership
- Debug logging will now work correctly

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 12:51:08 +02:00
Mistral Vibe
cad2bc1b5c Fix 403 errors for invited users
- bands.py: Change permission from admin-only to member-only
  - Line 33: Changed 'role != "admin"' to 'role is None'
  - Now regular band members can list invites

- versions.py: Add debug logging for audio stream access
  - Added logging to track user access and membership status
  - Helps diagnose why users get 403 on /versions/{id}/stream

These changes should resolve:
- 403 on /bands/{id}/invites (invited users)
- 403 on /versions/{id}/stream (audio playback)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 12:47:40 +02:00
Mistral Vibe
56ffd98f5e Phase 1 backend implementation: Add invite management endpoints
Implements core invite management features for band admins:
- GET /bands/{band_id}/invites - List all invites for a band (admin only)
- DELETE /invites/{invite_id} - Revoke pending invite (admin only)
- GET /invites/{token}/info - Get invite details (public)

Backend changes:
- Add invites router with 3 endpoints
- Update BandRepository with get_invites_for_band and get_invite_by_id methods
- Add new schemas for invite listing and info
- Register invites router in main.py

Tests:
- Integration tests for all 3 endpoints
- Permission tests (admin vs non-admin)
- Edge cases (not found, expired, etc.)

This addresses the core requirements:
- Admins can see pending invites
- Admins can revoke pending invites
- Users can view invite details before accepting

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 11:30:52 +02:00
Mistral Vibe
d9035acdff feat: app shell with sidebar + bug fixes
UI:
- Add persistent sidebar (210px) with band switcher dropdown, Library/Player/Settings nav, user avatar row, and sign-out button
- Align design system CSS vars to CLAUDE.md spec (#0f0f12 bg, #e8a22a amber accent, rgba borders/text)
- Remove light mode toggle (no light mode in v1)
- Homepage auto-redirects to first band; shows create-band form only when no bands exist
- Strip full-page wrappers from all pages (shell owns layout)
- Remove debug console.log statements from SongPage

Bug fixes:
- nginx: trailing slash on `location ^~ /api/v1/bands/` caused 301 redirect on POST, dropping the request body — removed trailing slash
- API: _member_from_request (used by nc-scan stream) only accepted Bearer token, not httpOnly cookie — add rh_token cookie fallback
- API: internal_secret config field now has a dev default so the service starts without INTERNAL_SECRET env var set

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 09:43:47 +02:00
Mistral Vibe
c1941ed9ac security: httpOnly cookies, rate limiting, nginx headers, SSE sanitization
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>
2026-03-30 21:11:53 +02:00
Mistral Vibe
68da26588a security: fix auth, CORS, file upload, endpoint hardening + test fixes
- Add INTERNAL_SECRET shared-secret auth to /internal/nc-upload endpoint
- Add JWT token validation to WebSocket /ws/versions/{version_id}
- Fix NameError: band_slug → band.slug in internal.py
- Move inline imports to top of internal.py; add missing Member/NextcloudClient imports
- Remove ~15 debug print() statements from auth.py
- Replace Content-Type-only avatar check with extension whitelist + Pillow Image.verify()
- Sanitize exception details in versions.py (no more str(e) in 4xx/5xx responses)
- Restrict CORS allow_methods/allow_headers from "*" to explicit lists
- Add security headers middleware: X-Frame-Options, X-Content-Type-Options, Referrer-Policy
- Reduce JWT expiry from 7 days to 1 hour
- Add Pillow>=10.0 dependency; document INTERNAL_SECRET in .env.example
- Implement missing RedisJobQueue.dequeue() method (required by protocol)
- Fix 5 pre-existing unit test failures: settings env vars conftest, deferred Redis push,
  dequeue method, AsyncMock→MagicMock for sync scalar_one_or_none

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 21:02:56 +02:00
Mistral Vibe
cd6fabb31c fix: correct avatar upload and DiceBear URL version
- Add api.upload() to client.ts that passes FormData without setting
  Content-Type, letting the browser set multipart/form-data with the
  correct boundary (was causing 422 on the upload endpoint)
- Use api.upload() instead of api.post() for avatar file upload
- Update DiceBear URLs from v6 to 9.x in both frontend and backend

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:17:21 +02:00
Mistral Vibe
a62297f2c4 fix: add comprehensive debugging and validation for avatar uploads
- Add detailed error extraction from API responses
- Validate file content is not empty before saving
- Verify file was actually saved to disk
- Check saved file size matches expectations
- Add extensive logging for debugging upload issues
- Improve error messages with specific details

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:55:20 +02:00
Mistral Vibe
b20b98a17a fix: improve error handling for avatar uploads
- Change invalid file type error from 400 to 422 for better frontend handling
- Add specific error message for 422 responses in frontend
- Improve error message clarity
- Better error classification and user guidance

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:49:39 +02:00
Mistral Vibe
26f45009d4 fix: improve file size handling and error messages
- Reduce server-side limit to 5MB for upload endpoint
- Increase client-side resizing threshold to 4MB
- Add specific error handling for 413 responses
- Add more detailed logging for file sizes
- Improve user error messages

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:43:22 +02:00
Mistral Vibe
a63a1571ba feat: implement client-side image resizing for avatar uploads
- Add resizeImage function to SettingsPage
- Resize images larger than 2MB to max 800x800 pixels
- Convert to JPEG with 80% quality to reduce file size
- Add server-side validation for 10MB file size limit
- Maintain aspect ratio during resizing
- Log resizing details for debugging

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:40:33 +02:00
Mistral Vibe
675399836c fix: add comprehensive logging to debug avatar issues
- Add console.log statements to frontend avatar functions
- Add print statements to backend avatar endpoints
- Log file uploads, settings updates, and errors
- Track the complete flow from UI to backend

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:34:28 +02:00
Mistral Vibe
b59eb584a6 fix: implement proper avatar upload and display
- Add file upload endpoint to auth router
- Mount static files for avatar serving
- Implement real file upload in frontend
- Add error handling and fallback for broken images
- Fix avatar persistence and state management
- Add loading states and proper error messages

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:27:35 +02:00
Mistral Vibe
ccafcd38af feat: implement user avatars with DiceBear integration
- Add avatar_url field to MemberSettingsUpdate schema
- Create AvatarService for generating default avatars using DiceBear
- Update auth service to generate avatars on user registration
- Add avatar upload UI to settings page
- Update settings endpoint to handle avatar URL updates
- Display current avatar in settings with upload/generate options

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:15:24 +02:00
Mistral Vibe
3b8c4a0cb8 fix: comment waveform integration with timestamps and avatars
- 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
2026-03-30 19:06:40 +02:00
Mistral Vibe
a80c936537 Fix frontend errors and add timestamp support to comments 2026-03-29 22:10:21 +02:00
Mistral Vibe
a8aba72b3a WIP: Add timestamp to comments and fix frontend errors 2026-03-29 22:06:36 +02:00
Mistral Vibe
f7a07ba05e Update all files 2026-03-29 20:44:23 +02:00
Mistral Vibe
02fd556372 feat: remove global Nextcloud config, enforce member-specific storage providers
- Remove global Nextcloud settings from config
- Make NextcloudClient require explicit credentials
- Update for_member() to return None when no credentials
- Modify services to accept optional storage client
- Update routers to pass member storage to services
- Add 403 responses when no storage provider configured
- Update internal endpoints to use member storage credentials

This change enforces that each member must configure their own
Nextcloud storage provider. If no provider is configured,
file operations will return 403 FORBIDDEN instead of falling
back to global placeholders.
2026-03-29 20:06:12 +02:00
Steffen Schuhmann
1ea22d8b55 fix: each file in a dated session folder becomes its own song
Files directly in YYMMDD/ (e.g. 231015/take1.wav, 231015/take2.wav) all
shared the same nc_folder_path, so take2 was silently merged as a new
version of the take1 song instead of being a separate recording.

Fix: when a file's parent == the session folder, append the file stem
to make a unique virtual nc_folder_path per file. Files in subfolders
(e.g. 231015/groove/take1.wav) are unaffected — they still group by folder.

Applied in both nc_scan.py (manual scan) and internal.py (watcher uploads).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 15:47:10 +02:00
Steffen Schuhmann
8f86ecbeec fix: model_validate multiline call in get_session_detail (sessions.py:64)
sed one-liner only handled single-line calls; this one spanned 3 lines.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 15:35:55 +02:00
Steffen Schuhmann
7c643ff67b fix: replace model_validate(..., update=) with .model_copy(update=)
Pydantic v2 model_validate() does not accept an update kwarg; the correct
pattern is model_validate(obj).model_copy(update={...}).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 15:20:42 +02:00
Steffen Schuhmann
7cad3e544a feat: incremental SSE scan, recursive NC traversal, custom folder support
- nc_scan.py: recursive collect_audio_files (fixes depth-1 bug); scan_band_folder
  yields ndjson events (progress/song/session/skipped/done) for streaming
- songs.py: replace old flat scan with scan_band_folder; add GET nc-scan/stream
  endpoint using _member_from_request so ?token= auth works for fetch-based SSE
- BandPage.tsx: scan button now consumes ndjson stream via fetch+ReadableStream;
  sessions/unattributed invalidated as each song/session event arrives
- session.py: add extract_session_folder() for YYMMDD path extraction
- rehearsal_session.py: get_or_create uses begin_nested() savepoint to handle races
- band.py: add get_by_nc_folder_prefix() for custom nc_folder_path band lookup
- internal.py: nc-upload falls back to prefix match when slug lookup fails
- event_loop.py: remove hardcoded bands/ guard; let internal API handle filtering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 15:09:42 +02:00
Steffen Schuhmann
dc6dd9dcfd fix: scan visibility, NC folder validation, watcher logging
- nc-scan: detailed INFO logging of every path found, subfolder
  contents and skip reasons; 502 now includes the exact folder and
  error so user sees a real message instead of a blank result
- band creation: if nc_base_path is explicitly given, verify the
  folder exists in Nextcloud before saving — returns 422 with a
  clear message to the user; auto-generated paths still do MKCOL
- songs search: add ?unattributed=true to return songs with no
  session_id (files not in a YYMMDD folder)
- BandPage: show "Unattributed Recordings" section below sessions
  so scanned files without a dated folder always appear
- watcher event_loop: promote all per-activity log lines from DEBUG
  to INFO so they're visible in default Docker Compose log output;
  log normalized path and skip reason for every activity

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 14:11:07 +02:00
Steffen Schuhmann
b882c9ea6d feat(api): auto-link rehearsal sessions on watcher upload and nc-scan
parse_rehearsal_date() extracts YYMMDD / YYYYMMDD from the file path
and get_or_create() a RehearsalSession. Both the watcher nc-upload
endpoint and the nc-scan endpoint now set song.session_id when a
dated folder is detected. Existing songs without a session_id are
back-filled on the next import of the same folder.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:41:01 +02:00
Steffen Schuhmann
a779c57a26 feat(api): song search endpoint and PATCH /songs/{id}
GET /bands/{id}/songs/search — filter by title (ILIKE), tags (contains
all), key, BPM range, session_id. All params optional and composable.
PATCH /songs/{id} — update title, status, notes, tags, key, BPM.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:39:32 +02:00
Steffen Schuhmann
1e53ddf8eb feat(api): session router — list, detail, patch endpoints
GET /bands/{id}/sessions — list with recording counts, newest first
GET /bands/{id}/sessions/{sid} — session detail with flat song list
PATCH /bands/{id}/sessions/{sid} — admin: update label/notes

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:38:27 +02:00
Steffen Schuhmann
f930bb061c feat(api): SessionRepository, session/song schemas with tags
Adds RehearsalSessionRepository (get_or_create, list_for_band with
counts, get_with_songs). Adds RehearsalSession schemas (Read, Detail,
Update). Extends SongRead/SongUpdate with tags and session_id fields.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:36:52 +02:00
Steffen Schuhmann
f1f4dc5a88 feat(db): add RehearsalSession table, songs.session_id, songs.tags
Introduces the rehearsal session concept: each YYMMDD folder in
Nextcloud maps to one RehearsalSession row (band_id + date unique).
Songs gain a nullable session_id FK and a tags TEXT[] column for
manual labeling (searchable by name, tag, BPM, key).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:35:18 +02:00
Steffen Schuhmann
53da085f28 fix: proxy audio stream through API with query-param token auth
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>
2026-03-29 13:23:51 +02:00
Steffen Schuhmann
47bc802775 fix: robust NC activity filter, title extraction, scan result detail
Watcher:
- Accept both NC 22+ (type="file_created") and older NC (subject="created_self")
  so the upload filter works across all Nextcloud versions
- Add .opus to audio_extensions
- Fix tests: set nc.username on mocks, use realistic activity dicts with type field
- Add tests for old NC style, non-band path filter, normalize_nc_path, cursor advance

API:
- Fix internal.py title extraction: always use filename stem (was using
  parts[-2] for >3-part paths, which gave folder name instead of song title)
- nc-scan now returns NcScanResult with folder, files_found, imported, skipped counts
  instead of bare song list — gives the UI actionable feedback

Web:
- Show rich scan result message: folder scanned, count imported, count already registered

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 12:26:58 +02:00
Steffen Schuhmann
fbac62a0ea feat: band NC folder config, fix watcher event filter, add light/dark theme
- Add PATCH /bands/{id} endpoint for admins to update nc_folder_path
- Add band NC scan folder UI panel with inline edit
- Fix watcher: use activity type field (not human-readable subject) for upload detection
- Reorder watcher filters: audio extension check first, then band path, then type
- Add dark/light theme toggle using GitHub Primer-inspired CSS custom properties
- All inline styles migrated to CSS variables for theme-awareness

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 00:29:58 +01:00
Steffen Schuhmann
b28472c32f fix: resolve job-not-found race and YYMMDD scan folder structure
Race condition (worker "Job not found in DB"):
- RedisJobQueue.enqueue() was pushing job IDs to Redis immediately after
  flush() but before the API transaction committed, so the worker would
  read an ID that didn't exist yet in the DB from its own session.
- Fix: defer the Redis rpush until after session.commit() via a pending-
  push list drained by get_session() after each successful commit.
- Worker: drain stale Redis queue entries on startup to clear any IDs
  left over from previously uncommitted transactions.
- Worker: add 3-attempt retry with 200ms sleep when a job is not found,
  as a safety net for any remaining propagation edge cases.

NC scan folder structure (YYMMDD rehearsal subfolders):
- Previously used dir_name as song title for all files in a subdirectory,
  meaning every file got the folder name (e.g. "231015") as its title.
- Fix: derive song title from Path(sub_rel).stem so each audio file gets
  its own name; use the file's parent path as nc_folder for version grouping.
- Rehearsal folder name stored in song.notes as "Rehearsal: YYMMDD".
- Added structured logging throughout the scan: entries found, per-folder
  file counts, skip/create/import decisions, and final summary count.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:58:46 +01:00
Steffen Schuhmann
f7be1b994d Initial commit: RehearsalHub POC
Full-stack self-hosted band rehearsal platform:

Backend (FastAPI + SQLAlchemy 2.0 async):
- Auth with JWT (register, login, /me, settings)
- Band management with Nextcloud folder integration
- Song management with audio version tracking
- Nextcloud scan to auto-import audio files
- Band membership with link-based invite system
- Song comments
- Audio analysis worker (BPM, key, loudness, waveform)
- Nextcloud activity watcher for auto-import
- WebSocket support for real-time annotation updates
- Alembic migrations (0001–0003)
- Repository pattern, Ruff + mypy configured

Frontend (React 18 + Vite + TypeScript strict):
- Login/register page with post-login redirect
- Home page with band list and creation form
- Band page with member panel, invite link, song list, NC scan
- Song page with waveform player, annotations, comment thread
- Settings page for per-user Nextcloud credentials
- Invite acceptance page (/invite/:token)
- ESLint v9 flat config + TypeScript strict mode

Infrastructure:
- Docker Compose: PostgreSQL, Redis, API, worker, watcher, nginx
- nginx reverse proxy for static files + /api/ proxy
- make check runs all linters before docker compose build

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-28 21:53:03 +01:00