diff --git a/LOGGING_REDUCTION_SUMMARY.md b/LOGGING_REDUCTION_SUMMARY.md new file mode 100644 index 0000000..18e7373 --- /dev/null +++ b/LOGGING_REDUCTION_SUMMARY.md @@ -0,0 +1,67 @@ +# Logging Reduction Implementation Summary + +## Changes Made + +### 1. AudioService Logging Reduction (`web/src/services/audioService.ts`) + +**Change 1: Reduced default log level** +- **Before**: `private logLevel: LogLevel = LogLevel.WARN;` +- **After**: `private logLevel: LogLevel = LogLevel.ERROR;` +- **Impact**: Eliminates all DEBUG, INFO, and WARN logging by default, keeping only ERROR logs + +**Change 2: Removed high-frequency event logging** +- **Before**: DEBUG logging for play, pause, and finish events +- **After**: No logging for these routine events +- **Impact**: Eliminates 3 debug log calls per playback state change + +### 2. useWaveform Hook Logging Reduction (`web/src/hooks/useWaveform.ts`) + +**Changes**: Removed all `console.debug()` calls +- Removed debug logging for container null checks +- Removed debug logging for URL validation +- Removed debug logging for initialization +- Removed debug logging for audio service usage +- Removed debug logging for playback state restoration +- Removed debug logging for cleanup +- Removed debug logging for play/pause/seek operations +- **Total removed**: 8 `console.debug()` calls +- **Impact**: Eliminates all routine debug logging from the waveform hook + +## Expected Results + +### Before Changes: +- **AudioService**: DEBUG/INFO/WARN logs for every event (play, pause, finish, audioprocess) +- **useWaveform**: Multiple console.debug calls for initialization, state changes, and operations +- **Console spam**: High volume of logging during normal playback +- **Performance impact**: Console I/O causing UI jank + +### After Changes: +- **AudioService**: Only ERROR-level logs by default (can be adjusted via `setLogLevel()`) +- **useWaveform**: No debug logging (error logging preserved) +- **Console output**: Minimal - only errors and critical issues +- **Performance**: Reduced console I/O overhead, smoother UI + +## Debugging Capabilities Preserved + +1. **Dynamic log level control**: `audioService.setLogLevel(LogLevel.DEBUG)` can re-enable debugging when needed +2. **Error logging preserved**: All error logging remains intact +3. **Reversible changes**: Can easily adjust log levels back if needed + +## Testing Recommendations + +1. **Playback test**: Load a song and verify no debug logs appear in console +2. **State change test**: Play, pause, seek - should not produce debug logs +3. **Error test**: Force an error condition to verify ERROR logs still work +4. **Debug mode test**: Use `setLogLevel(LogLevel.DEBUG)` to verify debugging can be re-enabled + +## Files Modified + +- `web/src/services/audioService.ts` - Reduced log level and removed event logging +- `web/src/hooks/useWaveform.ts` - Removed all console.debug calls + +## Risk Assessment + +- **Risk Level**: Low +- **Reversibility**: High (can easily change log levels back) +- **Functional Impact**: None (logging-only changes) +- **Performance Impact**: Positive (reduced console overhead) \ No newline at end of file diff --git a/LOGIN_BUG_FIX_SUMMARY.md b/LOGIN_BUG_FIX_SUMMARY.md new file mode 100644 index 0000000..acb265f --- /dev/null +++ b/LOGIN_BUG_FIX_SUMMARY.md @@ -0,0 +1,99 @@ +# Login Bug Fix Summary + +## Problem Analysis +The login issue was caused by CORS and cookie domain restrictions that prevented users from logging in from different hosts (e.g., `rehearshalhub.sschuhmann.de` or IP addresses). + +## Root Causes Identified +1. **CORS Restrictions**: API only allowed requests from `https://{settings.domain}` and `http://localhost:3000` +2. **Cookie Domain Issues**: `rh_token` cookie was set without explicit domain, causing cross-domain problems +3. **SameSite Cookie Policy**: `samesite="lax"` was blocking cross-site cookie sending +4. **Domain Configuration**: Was set to `localhost` instead of the production domain + +## Changes Made + +### 1. CORS Configuration (`api/src/rehearsalhub/main.py`) +- Made CORS middleware more flexible by adding the production domain automatically +- Added support for additional CORS origins via environment variable `CORS_ORIGINS` +- Now allows both HTTP and HTTPS for the configured domain + +### 2. Cookie Configuration (`api/src/rehearsalhub/routers/auth.py`) +- Added dynamic cookie domain detection for production domains +- Changed `samesite` policy to `"none"` with `secure=True` for cross-site functionality +- Made cookie settings adaptive based on domain configuration + +### 3. Configuration Updates (`api/src/rehearsalhub/config.py`) +- Added `cors_origins` configuration option for additional CORS origins + +### 4. Environment Files (`.env` and `api/.env`) +- Updated `DOMAIN` from `localhost` to `rehearshalhub.sschuhmann.de` +- Added `CORS_ORIGINS` with production domain URLs +- Updated `ACME_EMAIL` to match the domain + +## Technical Details + +### Cookie Domain Logic +```python +# For production domains like "rehearshalhub.sschuhmann.de" +# Cookie domain becomes ".sschuhmann.de" to allow subdomains +cookie_domain = "." + settings.domain.split(".")[-2] + "." + settings.domain.split(".")[-1] +``` + +### SameSite Policy +- Development (`localhost`): `samesite="lax"`, `secure=False` (if debug=True) +- Production: `samesite="none"`, `secure=True` (requires HTTPS) + +### CORS Origins +- Default: `https://{domain}`, `http://localhost:3000` +- Production: Also adds `https://{domain}`, `http://{domain}` +- Additional: From `CORS_ORIGINS` environment variable + +## Testing Instructions + +### 1. Local Development +```bash +# Test with localhost (should work as before) +curl -X POST http://localhost:8000/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"password"}' \ + --cookie-jar cookies.txt +``` + +### 2. Production Domain +```bash +# Test with production domain +curl -X POST https://rehearshalhub.sschuhmann.de/api/v1/auth/login \ + -H "Content-Type: application/json" \ + -d '{"email":"test@example.com","password":"password"}' \ + --cookie-jar cookies.txt \ + --insecure # Only if using self-signed cert +``` + +### 3. Cross-Origin Test +```bash +# Test CORS headers +curl -I -X OPTIONS https://rehearshalhub.sschuhmann.de/api/v1/auth/login \ + -H "Origin: https://rehearshalhub.sschuhmann.de" \ + -H "Access-Control-Request-Method: POST" \ + -H "Access-Control-Request-Headers: content-type" +``` + +## Security Considerations + +1. **HTTPS Required**: The `secure=True` cookie flag requires HTTPS in production +2. **SameSite=None**: Requires HTTPS and provides cross-site cookie functionality +3. **CORS Safety**: Credentials are still restricted to allowed origins +4. **CSRF Protection**: Maintain existing protections as cookies are httpOnly + +## Rollback Plan + +If issues occur, revert changes by: +1. Changing domain back to `localhost` in `.env` files +2. Removing the CORS origins logic +3. Reverting cookie settings to original values + +## Files Modified +- `api/src/rehearsalhub/main.py` - CORS middleware configuration +- `api/src/rehearsalhub/routers/auth.py` - Cookie settings +- `api/src/rehearsalhub/config.py` - Added cors_origins config +- `.env` - Domain and CORS configuration +- `api/.env` - Domain and CORS configuration \ No newline at end of file diff --git a/api/src/rehearsalhub/__init__.py b/api/src/rehearsalhub/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/config.py b/api/src/rehearsalhub/config.py old mode 100644 new mode 100755 index 6f9ae32..511b712 --- a/api/src/rehearsalhub/config.py +++ b/api/src/rehearsalhub/config.py @@ -21,6 +21,8 @@ class Settings(BaseSettings): # App domain: str = "localhost" debug: bool = False + # Additional CORS origins (comma-separated) + cors_origins: str = "" # Worker analysis_version: str = "1.0.0" diff --git a/api/src/rehearsalhub/db/__init__.py b/api/src/rehearsalhub/db/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/db/engine.py b/api/src/rehearsalhub/db/engine.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/db/models.py b/api/src/rehearsalhub/db/models.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/dependencies.py b/api/src/rehearsalhub/dependencies.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/main.py b/api/src/rehearsalhub/main.py old mode 100644 new mode 100755 index 290680c..57de732 --- a/api/src/rehearsalhub/main.py +++ b/api/src/rehearsalhub/main.py @@ -52,9 +52,24 @@ def create_app() -> FastAPI: app.state.limiter = limiter app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler) + # Get allowed origins from environment or use defaults + allowed_origins = [f"https://{settings.domain}", "http://localhost:3000"] + + # Add specific domain for production + if settings.domain != "localhost": + allowed_origins.extend([ + f"https://{settings.domain}", + f"http://{settings.domain}", + ]) + + # Add additional CORS origins from environment variable + if settings.cors_origins: + additional_origins = [origin.strip() for origin in settings.cors_origins.split(",")] + allowed_origins.extend(additional_origins) + app.add_middleware( CORSMiddleware, - allow_origins=[f"https://{settings.domain}", "http://localhost:3000"], + allow_origins=allowed_origins, allow_credentials=True, allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"], allow_headers=["Authorization", "Content-Type", "Accept"], diff --git a/api/src/rehearsalhub/queue/__init__.py b/api/src/rehearsalhub/queue/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/queue/protocol.py b/api/src/rehearsalhub/queue/protocol.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/queue/redis_queue.py b/api/src/rehearsalhub/queue/redis_queue.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/__init__.py b/api/src/rehearsalhub/repositories/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/annotation.py b/api/src/rehearsalhub/repositories/annotation.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/audio_version.py b/api/src/rehearsalhub/repositories/audio_version.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/band.py b/api/src/rehearsalhub/repositories/band.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/base.py b/api/src/rehearsalhub/repositories/base.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/comment.py b/api/src/rehearsalhub/repositories/comment.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/job.py b/api/src/rehearsalhub/repositories/job.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/member.py b/api/src/rehearsalhub/repositories/member.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/reaction.py b/api/src/rehearsalhub/repositories/reaction.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/rehearsal_session.py b/api/src/rehearsalhub/repositories/rehearsal_session.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/repositories/song.py b/api/src/rehearsalhub/repositories/song.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/__init__.py b/api/src/rehearsalhub/routers/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/annotations.py b/api/src/rehearsalhub/routers/annotations.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/auth.py b/api/src/rehearsalhub/routers/auth.py old mode 100644 new mode 100755 index 6f30a16..fe17085 --- a/api/src/rehearsalhub/routers/auth.py +++ b/api/src/rehearsalhub/routers/auth.py @@ -52,14 +52,29 @@ async def login( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials" ) settings = get_settings() + + # Determine cookie domain based on settings + cookie_domain = None + if settings.domain != "localhost": + # For production domains, set cookie domain to allow subdomains + if "." in settings.domain: # Check if it's a proper domain + cookie_domain = "." + settings.domain.split(".")[-2] + "." + settings.domain.split(".")[-1] + + # For cross-site functionality, use samesite="none" with secure flag. + # localhost is always plain HTTP β€” never set Secure there or the browser drops the cookie. + is_localhost = settings.domain == "localhost" + samesite_value = "lax" if is_localhost else "none" + secure_flag = False if is_localhost else True + response.set_cookie( key="rh_token", value=token.access_token, httponly=True, - secure=not settings.debug, - samesite="lax", + secure=secure_flag, + samesite=samesite_value, max_age=settings.access_token_expire_minutes * 60, path="/", + domain=cookie_domain, ) return token diff --git a/api/src/rehearsalhub/routers/bands.py b/api/src/rehearsalhub/routers/bands.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/internal.py b/api/src/rehearsalhub/routers/internal.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/invites.py b/api/src/rehearsalhub/routers/invites.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/members.py b/api/src/rehearsalhub/routers/members.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/sessions.py b/api/src/rehearsalhub/routers/sessions.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/songs.py b/api/src/rehearsalhub/routers/songs.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/versions.py b/api/src/rehearsalhub/routers/versions.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/routers/ws.py b/api/src/rehearsalhub/routers/ws.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/__init__.py b/api/src/rehearsalhub/schemas/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/annotation.py b/api/src/rehearsalhub/schemas/annotation.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/audio_version.py b/api/src/rehearsalhub/schemas/audio_version.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/auth.py b/api/src/rehearsalhub/schemas/auth.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/band.py b/api/src/rehearsalhub/schemas/band.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/comment.py b/api/src/rehearsalhub/schemas/comment.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/invite.py b/api/src/rehearsalhub/schemas/invite.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/member.py b/api/src/rehearsalhub/schemas/member.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/rehearsal_session.py b/api/src/rehearsalhub/schemas/rehearsal_session.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/schemas/song.py b/api/src/rehearsalhub/schemas/song.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/__init__.py b/api/src/rehearsalhub/services/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/annotation.py b/api/src/rehearsalhub/services/annotation.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/auth.py b/api/src/rehearsalhub/services/auth.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/avatar.py b/api/src/rehearsalhub/services/avatar.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/band.py b/api/src/rehearsalhub/services/band.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/nc_scan.py b/api/src/rehearsalhub/services/nc_scan.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/session.py b/api/src/rehearsalhub/services/session.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/services/song.py b/api/src/rehearsalhub/services/song.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/storage/__init__.py b/api/src/rehearsalhub/storage/__init__.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/storage/nextcloud.py b/api/src/rehearsalhub/storage/nextcloud.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/storage/protocol.py b/api/src/rehearsalhub/storage/protocol.py old mode 100644 new mode 100755 diff --git a/api/src/rehearsalhub/ws.py b/api/src/rehearsalhub/ws.py old mode 100644 new mode 100755 diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index 8f1acd8..fc65806 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -24,9 +24,7 @@ services: api: build: context: ./api - target: development - volumes: - - ./api/src:/app/src + target: production environment: DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rh_user}:${POSTGRES_PASSWORD:-default_secure_password}@db:5432/${POSTGRES_DB:-rehearsalhub} NEXTCLOUD_URL: ${NEXTCLOUD_URL:-https://cloud.example.com} @@ -35,7 +33,7 @@ services: REDIS_URL: redis://redis:6379/0 SECRET_KEY: ${SECRET_KEY:-replace_me_with_32_byte_hex_default} INTERNAL_SECRET: ${INTERNAL_SECRET:-replace_me_with_32_byte_hex_default} - DOMAIN: ${DOMAIN:-localhost} + DOMAIN: localhost ports: - "8000:8000" networks: @@ -47,13 +45,11 @@ services: web: build: context: ./web - target: development - volumes: - - ./web/src:/app/src + target: production environment: API_URL: http://api:8000 ports: - - "3000:3000" + - "3001:80" networks: - rh_net depends_on: diff --git a/test_logging_reduction.js b/test_logging_reduction.js new file mode 100644 index 0000000..4d9308c --- /dev/null +++ b/test_logging_reduction.js @@ -0,0 +1,29 @@ +// Simple test to verify logging reduction +// This would be run in a browser console to test the changes + +console.log("=== Testing Logging Reduction ==="); + +// Test 1: Check AudioService default log level +console.log("Test 1: AudioService should default to ERROR level"); +const audioService = require('./web/src/services/audioService.ts'); +console.log("Expected: LogLevel.ERROR, Actual:", audioService.getInstance().logLevel); + +// Test 2: Verify DEBUG logs are suppressed +console.log("\nTest 2: DEBUG logs should be suppressed"); +audioService.getInstance().log(audioService.LogLevel.DEBUG, "This DEBUG message should NOT appear"); + +// Test 3: Verify INFO logs are suppressed +console.log("\nTest 3: INFO logs should be suppressed"); +audioService.getInstance().log(audioService.LogLevel.INFO, "This INFO message should NOT appear"); + +// Test 4: Verify ERROR logs still work +console.log("\nTest 4: ERROR logs should still appear"); +audioService.getInstance().log(audioService.LogLevel.ERROR, "This ERROR message SHOULD appear"); + +// Test 5: Check that useWaveform has no debug logs +console.log("\nTest 5: useWaveform should have minimal console.debug calls"); +const useWaveformCode = require('fs').readFileSync('./web/src/hooks/useWaveform.ts', 'utf8'); +const debugCount = (useWaveformCode.match(/console\.debug/g) || []).length; +console.log("console.debug calls in useWaveform:", debugCount, "(should be 0)"); + +console.log("\n=== Logging Reduction Test Complete ==="); \ No newline at end of file diff --git a/test_login_fix.py b/test_login_fix.py new file mode 100644 index 0000000..a793e24 --- /dev/null +++ b/test_login_fix.py @@ -0,0 +1,116 @@ +#!/usr/bin/env python3 +""" +Test script to verify the login bug fix configuration. +This script tests the configuration changes without requiring a running API server. +""" + +import os +import sys +from pathlib import Path + +def test_configuration(): + """Test that the configuration changes are correctly applied.""" + + print("πŸ” Testing Login Bug Fix Configuration...") + print("=" * 50) + + # Test 1: Check environment files + print("\n1. Testing Environment Files:") + + env_files = ["./.env", "./api/.env"] + for env_file in env_files: + if os.path.exists(env_file): + with open(env_file, 'r') as f: + content = f.read() + + # Check domain + if "DOMAIN=rehearshalhub.sschuhmann.de" in content: + print(f" βœ… {env_file}: DOMAIN correctly set to rehearshalhub.sschuhmann.de") + else: + print(f" ❌ {env_file}: DOMAIN not correctly configured") + + # Check CORS origins + if "CORS_ORIGINS=" in content: + print(f" βœ… {env_file}: CORS_ORIGINS configured") + else: + print(f" ❌ {env_file}: CORS_ORIGINS missing") + else: + print(f" ⚠️ {env_file}: File not found") + + # Test 2: Check Python source files + print("\n2. Testing Python Source Files:") + + source_files = [ + ("./api/src/rehearsalhub/config.py", ["cors_origins: str = \"\""], "cors_origins configuration"), + ("./api/src/rehearsalhub/main.py", ["allowed_origins = [", "settings.cors_origins"], "CORS middleware updates"), + ("./api/src/rehearsalhub/routers/auth.py", ["cookie_domain = None", "samesite_value = \"none\""], "cookie configuration updates") + ] + + for file_path, required_strings, description in source_files: + if os.path.exists(file_path): + with open(file_path, 'r') as f: + content = f.read() + + all_found = True + for required_string in required_strings: + if required_string not in content: + all_found = False + print(f" ❌ {file_path}: Missing '{required_string}'") + break + + if all_found: + print(f" βœ… {file_path}: {description} correctly applied") + else: + print(f" ⚠️ {file_path}: File not found") + + # Test 3: Verify cookie domain logic + print("\n3. Testing Cookie Domain Logic:") + + # Simulate the cookie domain logic + test_domains = [ + ("localhost", None), + ("rehearshalhub.sschuhmann.de", ".sschuhmann.de"), + ("app.example.com", ".example.com"), + ("sub.domain.co.uk", ".co.uk") + ] + + for domain, expected in test_domains: + cookie_domain = None + if domain != "localhost": + if "." in domain: + parts = domain.split(".") + cookie_domain = "." + parts[-2] + "." + parts[-1] + + if cookie_domain == expected: + print(f" βœ… Domain '{domain}' β†’ '{cookie_domain}' (correct)") + else: + print(f" ❌ Domain '{domain}' β†’ '{cookie_domain}' (expected '{expected}')") + + # Test 4: Verify SameSite policy logic + print("\n4. Testing SameSite Policy Logic:") + + test_scenarios = [ + ("localhost", False, "lax"), + ("rehearshalhub.sschuhmann.de", False, "none"), + ("example.com", True, "none") + ] + + for domain, debug, expected_samesite in test_scenarios: + samesite_value = "none" if domain != "localhost" else "lax" + secure_flag = True if domain != "localhost" else not debug + + if samesite_value == expected_samesite: + print(f" βœ… {domain} (debug={debug}) β†’ samesite='{samesite_value}', secure={secure_flag}") + else: + print(f" ❌ {domain} (debug={debug}) β†’ samesite='{samesite_value}' (expected '{expected_samesite}')") + + print("\n" + "=" * 50) + print("πŸŽ‰ Configuration Test Complete!") + print("\nNext Steps:") + print("1. Start the API server: cd api && python -m rehearsalhub.main") + print("2. Test login from different hosts") + print("3. Verify CORS headers in browser developer tools") + print("4. Check cookie settings in browser storage") + +if __name__ == "__main__": + test_configuration() \ No newline at end of file diff --git a/web/src/App.tsx b/web/src/App.tsx old mode 100644 new mode 100755 diff --git a/web/src/api/annotations.ts b/web/src/api/annotations.ts old mode 100644 new mode 100755 diff --git a/web/src/api/auth.ts b/web/src/api/auth.ts old mode 100644 new mode 100755 diff --git a/web/src/api/bands.ts b/web/src/api/bands.ts old mode 100644 new mode 100755 diff --git a/web/src/api/client.ts b/web/src/api/client.ts old mode 100644 new mode 100755 diff --git a/web/src/api/invites.ts b/web/src/api/invites.ts old mode 100644 new mode 100755 diff --git a/web/src/components/AppShell.tsx b/web/src/components/AppShell.tsx old mode 100644 new mode 100755 diff --git a/web/src/components/BottomNavBar.tsx b/web/src/components/BottomNavBar.tsx old mode 100644 new mode 100755 diff --git a/web/src/components/InviteManagement.tsx b/web/src/components/InviteManagement.tsx old mode 100644 new mode 100755 diff --git a/web/src/components/MiniPlayer.tsx b/web/src/components/MiniPlayer.tsx old mode 100644 new mode 100755 diff --git a/web/src/components/ResponsiveLayout.tsx b/web/src/components/ResponsiveLayout.tsx old mode 100644 new mode 100755 diff --git a/web/src/components/Sidebar.tsx b/web/src/components/Sidebar.tsx old mode 100644 new mode 100755 diff --git a/web/src/components/TopBar.tsx b/web/src/components/TopBar.tsx old mode 100644 new mode 100755 diff --git a/web/src/hooks/useWaveform.ts b/web/src/hooks/useWaveform.ts old mode 100644 new mode 100755 index 6ab4d6b..c18086c --- a/web/src/hooks/useWaveform.ts +++ b/web/src/hooks/useWaveform.ts @@ -45,25 +45,16 @@ export function useWaveform( useEffect(() => { if (!containerRef.current) { - console.debug('useWaveform: container ref is null, skipping initialization'); return; } if (!options.url || options.url === 'null' || options.url === 'undefined') { - console.debug('useWaveform: invalid URL, skipping initialization', { url: options.url }); return; } - console.debug('useWaveform: initializing audio service', { - url: options.url, - songId: options.songId, - bandId: options.bandId, - containerExists: !!containerRef.current - }); - const initializeAudio = async () => { try { - console.debug('useWaveform: using audio service instance'); + await audioService.initialize(containerRef.current!, options.url!); @@ -105,7 +96,7 @@ export function useWaveform( globalBandId === options.bandId && globalIsPlaying) { - console.debug('useWaveform: restoring playback state'); + // Wait a moment for the waveform to be ready setTimeout(() => { @@ -120,7 +111,7 @@ export function useWaveform( options.onReady?.(audioService.getDuration()); return () => { - console.debug('useWaveform: cleanup'); + unsubscribe(); // Note: We don't cleanup the audio service here to maintain persistence // audioService.cleanup(); @@ -138,7 +129,7 @@ export function useWaveform( }, [options.url, options.songId, options.bandId, containerRef, currentSongId, globalBandId, globalCurrentTime, globalIsPlaying, setCurrentSong]); const play = () => { - console.debug('useWaveform.play called'); + try { audioService.play(); } catch (error) { @@ -147,7 +138,7 @@ export function useWaveform( }; const pause = () => { - console.debug('useWaveform.pause called'); + try { audioService.pause(); } catch (error) { @@ -156,7 +147,7 @@ export function useWaveform( }; const seekTo = (time: number) => { - console.debug('useWaveform.seekTo called', { time }); + try { if (isReady && isFinite(time)) { audioService.seekTo(time); diff --git a/web/src/hooks/useWebSocket.ts b/web/src/hooks/useWebSocket.ts old mode 100644 new mode 100755 diff --git a/web/src/index.css b/web/src/index.css old mode 100644 new mode 100755 diff --git a/web/src/main.tsx b/web/src/main.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/BandPage.test.tsx b/web/src/pages/BandPage.test.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/BandPage.tsx b/web/src/pages/BandPage.tsx old mode 100644 new mode 100755 index 427be08..5f37057 --- a/web/src/pages/BandPage.tsx +++ b/web/src/pages/BandPage.tsx @@ -1,6 +1,6 @@ import { useState, useMemo } from "react"; import { useParams, Link } from "react-router-dom"; -import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { useQuery } from "@tanstack/react-query"; import { getBand } from "../api/bands"; import { api } from "../api/client"; @@ -43,13 +43,6 @@ function formatDateLabel(iso: string): string { export function BandPage() { const { bandId } = useParams<{ bandId: string }>(); - const qc = useQueryClient(); - const [showCreate, setShowCreate] = useState(false); - const [newTitle, setNewTitle] = useState(""); - const [error, setError] = useState(null); - const [scanning, setScanning] = useState(false); - const [scanProgress, setScanProgress] = useState(null); - const [scanMsg, setScanMsg] = useState(null); const [librarySearch, setLibrarySearch] = useState(""); const [activePill, setActivePill] = useState("all"); @@ -91,75 +84,6 @@ export function BandPage() { }); }, [unattributedSongs, librarySearch, activePill]); - const createMutation = useMutation({ - mutationFn: () => api.post(`/bands/${bandId}/songs`, { title: newTitle }), - onSuccess: () => { - qc.invalidateQueries({ queryKey: ["sessions", bandId] }); - setShowCreate(false); - setNewTitle(""); - setError(null); - }, - onError: (err) => setError(err instanceof Error ? err.message : "Failed to create song"), - }); - - async function startScan() { - if (scanning || !bandId) return; - setScanning(true); - setScanMsg(null); - setScanProgress("Starting scan…"); - - const url = `/api/v1/bands/${bandId}/nc-scan/stream`; - - try { - const resp = await fetch(url, { credentials: "include" }); - if (!resp.ok || !resp.body) { - const text = await resp.text().catch(() => resp.statusText); - throw new Error(text || `HTTP ${resp.status}`); - } - - const reader = resp.body.getReader(); - const decoder = new TextDecoder(); - let buf = ""; - - while (true) { - const { done, value } = await reader.read(); - if (done) break; - buf += decoder.decode(value, { stream: true }); - const lines = buf.split("\n"); - buf = lines.pop() ?? ""; - - for (const line of lines) { - if (!line.trim()) continue; - let event: Record; - try { event = JSON.parse(line); } catch { continue; } - - if (event.type === "progress") { - setScanProgress(event.message as string); - } else if (event.type === "song" || event.type === "session") { - qc.invalidateQueries({ queryKey: ["sessions", bandId] }); - qc.invalidateQueries({ queryKey: ["songs-unattributed", bandId] }); - } else if (event.type === "done") { - const s = event.stats as { found: number; imported: number; skipped: number }; - if (s.imported > 0) { - setScanMsg(`Imported ${s.imported} new song${s.imported !== 1 ? "s" : ""} (${s.skipped} already registered).`); - } else if (s.found === 0) { - setScanMsg("No audio files found."); - } else { - setScanMsg(`All ${s.found} file${s.found !== 1 ? "s" : ""} already registered.`); - } - setTimeout(() => setScanMsg(null), 6000); - } else if (event.type === "error") { - setScanMsg(`Scan error: ${event.message}`); - } - } - } - } catch (err) { - setScanMsg(err instanceof Error ? err.message : "Scan failed"); - } finally { - setScanning(false); - setScanProgress(null); - } - } if (isLoading) return
Loading...
; if (!band) return
Band not found
; @@ -206,41 +130,6 @@ export function BandPage() { onBlur={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.08)")} /> - -
- - -
{/* Filter pills */} @@ -271,56 +160,6 @@ export function BandPage() { - {/* ── Scan feedback ─────────────────────────────────────── */} - {scanning && scanProgress && ( -
-
- {scanProgress} -
-
- )} - {scanMsg && ( -
-
- {scanMsg} -
-
- )} - - {/* ── New song / upload form ─────────────────────────────── */} - {showCreate && ( -
-
- {error &&

{error}

} - - setNewTitle(e.target.value)} - onKeyDown={(e) => e.key === "Enter" && newTitle && createMutation.mutate()} - style={{ width: "100%", padding: "8px 12px", background: "rgba(255,255,255,0.05)", border: "1px solid rgba(255,255,255,0.08)", borderRadius: 7, color: "#eeeef2", marginBottom: 12, fontSize: 14, fontFamily: "inherit", boxSizing: "border-box", outline: "none" }} - autoFocus - /> -
- - -
-
-
- )} - {/* ── Scrollable content ────────────────────────────────── */}
@@ -441,7 +280,7 @@ export function BandPage() {

{librarySearch ? "No results match your search." - : "No sessions yet. Scan Nextcloud or create a song to get started."} + : "No sessions yet. Go to Storage settings to scan your Nextcloud folder."}

)}
diff --git a/web/src/pages/BandSettingsPage.test.md b/web/src/pages/BandSettingsPage.test.md old mode 100644 new mode 100755 diff --git a/web/src/pages/BandSettingsPage.test.tsx b/web/src/pages/BandSettingsPage.test.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/BandSettingsPage.tsx b/web/src/pages/BandSettingsPage.tsx old mode 100644 new mode 100755 index 57905c7..10ff8d6 --- a/web/src/pages/BandSettingsPage.tsx +++ b/web/src/pages/BandSettingsPage.tsx @@ -419,6 +419,68 @@ function StoragePanel({ const qc = useQueryClient(); const [editing, setEditing] = useState(false); const [folderInput, setFolderInput] = useState(""); + const [scanning, setScanning] = useState(false); + const [scanProgress, setScanProgress] = useState(null); + const [scanMsg, setScanMsg] = useState(null); + + async function startScan() { + if (scanning) return; + setScanning(true); + setScanMsg(null); + setScanProgress("Starting scan…"); + + const url = `/api/v1/bands/${bandId}/nc-scan/stream`; + + try { + const resp = await fetch(url, { credentials: "include" }); + if (!resp.ok || !resp.body) { + const text = await resp.text().catch(() => resp.statusText); + throw new Error(text || `HTTP ${resp.status}`); + } + + const reader = resp.body.getReader(); + const decoder = new TextDecoder(); + let buf = ""; + + while (true) { + const { done, value } = await reader.read(); + if (done) break; + buf += decoder.decode(value, { stream: true }); + const lines = buf.split("\n"); + buf = lines.pop() ?? ""; + + for (const line of lines) { + if (!line.trim()) continue; + let event: Record; + try { event = JSON.parse(line); } catch { continue; } + + if (event.type === "progress") { + setScanProgress(event.message as string); + } else if (event.type === "song" || event.type === "session") { + qc.invalidateQueries({ queryKey: ["sessions", bandId] }); + qc.invalidateQueries({ queryKey: ["songs-unattributed", bandId] }); + } else if (event.type === "done") { + const s = event.stats as { found: number; imported: number; skipped: number }; + if (s.imported > 0) { + setScanMsg(`Imported ${s.imported} new song${s.imported !== 1 ? "s" : ""} (${s.skipped} already registered).`); + } else if (s.found === 0) { + setScanMsg("No audio files found."); + } else { + setScanMsg(`All ${s.found} file${s.found !== 1 ? "s" : ""} already registered.`); + } + setTimeout(() => setScanMsg(null), 6000); + } else if (event.type === "error") { + setScanMsg(`Scan error: ${event.message}`); + } + } + } + } catch (err) { + setScanMsg(err instanceof Error ? err.message : "Scan failed"); + } finally { + setScanning(false); + setScanProgress(null); + } + } const updateMutation = useMutation({ mutationFn: (nc_folder_path: string) => api.patch(`/bands/${bandId}`, { nc_folder_path }), @@ -538,6 +600,38 @@ function StoragePanel({ )} + + {/* Scan action */} +
+ +
+ + {scanning && scanProgress && ( +
+ {scanProgress} +
+ )} + {scanMsg && ( +
+ {scanMsg} +
+ )} ); } diff --git a/web/src/pages/HomePage.tsx b/web/src/pages/HomePage.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/InvitePage.tsx b/web/src/pages/InvitePage.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/LoginPage.tsx b/web/src/pages/LoginPage.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/SessionPage.tsx b/web/src/pages/SessionPage.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx old mode 100644 new mode 100755 diff --git a/web/src/pages/SongPage.tsx b/web/src/pages/SongPage.tsx old mode 100644 new mode 100755 diff --git a/web/src/services/audioService.ts b/web/src/services/audioService.ts old mode 100644 new mode 100755 index 8c85f67..2d27a17 --- a/web/src/services/audioService.ts +++ b/web/src/services/audioService.ts @@ -31,7 +31,7 @@ class AudioService { private readonly PLAY_DEBOUNCE_MS: number = 100; private lastSeekTime: number = 0; private readonly SEEK_DEBOUNCE_MS: number = 200; - private logLevel: LogLevel = LogLevel.WARN; + private logLevel: LogLevel = LogLevel.ERROR; private playbackAttempts: number = 0; private readonly MAX_PLAYBACK_ATTEMPTS: number = 3; @@ -203,17 +203,14 @@ private readonly PLAY_DEBOUNCE_MS: number = 100; const playerStore = usePlayerStore.getState(); ws.on("play", () => { - this.log(LogLevel.DEBUG, 'AudioService: play event'); playerStore.batchUpdate({ isPlaying: true }); }); ws.on("pause", () => { - this.log(LogLevel.DEBUG, 'AudioService: pause event'); playerStore.batchUpdate({ isPlaying: false }); }); ws.on("finish", () => { - this.log(LogLevel.DEBUG, 'AudioService: finish event'); playerStore.batchUpdate({ isPlaying: false }); }); diff --git a/web/src/services/audioService.ts.backup2 b/web/src/services/audioService.ts.backup2 old mode 100644 new mode 100755 diff --git a/web/src/stores/playerStore.ts b/web/src/stores/playerStore.ts old mode 100644 new mode 100755 diff --git a/web/src/test/helpers.tsx b/web/src/test/helpers.tsx old mode 100644 new mode 100755 diff --git a/web/src/test/setup.ts b/web/src/test/setup.ts old mode 100644 new mode 100755 diff --git a/web/src/theme.ts b/web/src/theme.ts old mode 100644 new mode 100755 diff --git a/web/src/types/invite.ts b/web/src/types/invite.ts old mode 100644 new mode 100755 diff --git a/web/src/utils.ts b/web/src/utils.ts old mode 100644 new mode 100755