WIP: Working on player
This commit is contained in:
67
LOGGING_REDUCTION_SUMMARY.md
Normal file
67
LOGGING_REDUCTION_SUMMARY.md
Normal file
@@ -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)
|
||||||
99
LOGIN_BUG_FIX_SUMMARY.md
Normal file
99
LOGIN_BUG_FIX_SUMMARY.md
Normal file
@@ -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
|
||||||
0
api/src/rehearsalhub/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/__init__.py
Normal file → Executable file
2
api/src/rehearsalhub/config.py
Normal file → Executable file
2
api/src/rehearsalhub/config.py
Normal file → Executable file
@@ -21,6 +21,8 @@ class Settings(BaseSettings):
|
|||||||
# App
|
# App
|
||||||
domain: str = "localhost"
|
domain: str = "localhost"
|
||||||
debug: bool = False
|
debug: bool = False
|
||||||
|
# Additional CORS origins (comma-separated)
|
||||||
|
cors_origins: str = ""
|
||||||
|
|
||||||
# Worker
|
# Worker
|
||||||
analysis_version: str = "1.0.0"
|
analysis_version: str = "1.0.0"
|
||||||
|
|||||||
0
api/src/rehearsalhub/db/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/db/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/db/engine.py
Normal file → Executable file
0
api/src/rehearsalhub/db/engine.py
Normal file → Executable file
0
api/src/rehearsalhub/db/models.py
Normal file → Executable file
0
api/src/rehearsalhub/db/models.py
Normal file → Executable file
0
api/src/rehearsalhub/dependencies.py
Normal file → Executable file
0
api/src/rehearsalhub/dependencies.py
Normal file → Executable file
17
api/src/rehearsalhub/main.py
Normal file → Executable file
17
api/src/rehearsalhub/main.py
Normal file → Executable file
@@ -52,9 +52,24 @@ def create_app() -> FastAPI:
|
|||||||
app.state.limiter = limiter
|
app.state.limiter = limiter
|
||||||
app.add_exception_handler(RateLimitExceeded, _rate_limit_exceeded_handler)
|
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(
|
app.add_middleware(
|
||||||
CORSMiddleware,
|
CORSMiddleware,
|
||||||
allow_origins=[f"https://{settings.domain}", "http://localhost:3000"],
|
allow_origins=allowed_origins,
|
||||||
allow_credentials=True,
|
allow_credentials=True,
|
||||||
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
allow_methods=["GET", "POST", "PUT", "PATCH", "DELETE"],
|
||||||
allow_headers=["Authorization", "Content-Type", "Accept"],
|
allow_headers=["Authorization", "Content-Type", "Accept"],
|
||||||
|
|||||||
0
api/src/rehearsalhub/queue/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/queue/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/queue/protocol.py
Normal file → Executable file
0
api/src/rehearsalhub/queue/protocol.py
Normal file → Executable file
0
api/src/rehearsalhub/queue/redis_queue.py
Normal file → Executable file
0
api/src/rehearsalhub/queue/redis_queue.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/annotation.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/annotation.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/audio_version.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/audio_version.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/band.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/band.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/base.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/base.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/comment.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/comment.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/job.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/job.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/member.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/member.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/reaction.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/reaction.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/rehearsal_session.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/rehearsal_session.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/song.py
Normal file → Executable file
0
api/src/rehearsalhub/repositories/song.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/annotations.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/annotations.py
Normal file → Executable file
19
api/src/rehearsalhub/routers/auth.py
Normal file → Executable file
19
api/src/rehearsalhub/routers/auth.py
Normal file → Executable file
@@ -52,14 +52,29 @@ async def login(
|
|||||||
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
|
status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid credentials"
|
||||||
)
|
)
|
||||||
settings = get_settings()
|
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(
|
response.set_cookie(
|
||||||
key="rh_token",
|
key="rh_token",
|
||||||
value=token.access_token,
|
value=token.access_token,
|
||||||
httponly=True,
|
httponly=True,
|
||||||
secure=not settings.debug,
|
secure=secure_flag,
|
||||||
samesite="lax",
|
samesite=samesite_value,
|
||||||
max_age=settings.access_token_expire_minutes * 60,
|
max_age=settings.access_token_expire_minutes * 60,
|
||||||
path="/",
|
path="/",
|
||||||
|
domain=cookie_domain,
|
||||||
)
|
)
|
||||||
return token
|
return token
|
||||||
|
|
||||||
|
|||||||
0
api/src/rehearsalhub/routers/bands.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/bands.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/internal.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/internal.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/invites.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/invites.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/members.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/members.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/sessions.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/sessions.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/songs.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/songs.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/versions.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/versions.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/ws.py
Normal file → Executable file
0
api/src/rehearsalhub/routers/ws.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/annotation.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/annotation.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/audio_version.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/audio_version.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/auth.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/auth.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/band.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/band.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/comment.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/comment.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/invite.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/invite.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/member.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/member.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/rehearsal_session.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/rehearsal_session.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/song.py
Normal file → Executable file
0
api/src/rehearsalhub/schemas/song.py
Normal file → Executable file
0
api/src/rehearsalhub/services/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/services/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/services/annotation.py
Normal file → Executable file
0
api/src/rehearsalhub/services/annotation.py
Normal file → Executable file
0
api/src/rehearsalhub/services/auth.py
Normal file → Executable file
0
api/src/rehearsalhub/services/auth.py
Normal file → Executable file
0
api/src/rehearsalhub/services/avatar.py
Normal file → Executable file
0
api/src/rehearsalhub/services/avatar.py
Normal file → Executable file
0
api/src/rehearsalhub/services/band.py
Normal file → Executable file
0
api/src/rehearsalhub/services/band.py
Normal file → Executable file
0
api/src/rehearsalhub/services/nc_scan.py
Normal file → Executable file
0
api/src/rehearsalhub/services/nc_scan.py
Normal file → Executable file
0
api/src/rehearsalhub/services/session.py
Normal file → Executable file
0
api/src/rehearsalhub/services/session.py
Normal file → Executable file
0
api/src/rehearsalhub/services/song.py
Normal file → Executable file
0
api/src/rehearsalhub/services/song.py
Normal file → Executable file
0
api/src/rehearsalhub/storage/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/storage/__init__.py
Normal file → Executable file
0
api/src/rehearsalhub/storage/nextcloud.py
Normal file → Executable file
0
api/src/rehearsalhub/storage/nextcloud.py
Normal file → Executable file
0
api/src/rehearsalhub/storage/protocol.py
Normal file → Executable file
0
api/src/rehearsalhub/storage/protocol.py
Normal file → Executable file
0
api/src/rehearsalhub/ws.py
Normal file → Executable file
0
api/src/rehearsalhub/ws.py
Normal file → Executable file
@@ -24,9 +24,7 @@ services:
|
|||||||
api:
|
api:
|
||||||
build:
|
build:
|
||||||
context: ./api
|
context: ./api
|
||||||
target: development
|
target: production
|
||||||
volumes:
|
|
||||||
- ./api/src:/app/src
|
|
||||||
environment:
|
environment:
|
||||||
DATABASE_URL: postgresql+asyncpg://${POSTGRES_USER:-rh_user}:${POSTGRES_PASSWORD:-default_secure_password}@db:5432/${POSTGRES_DB:-rehearsalhub}
|
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}
|
NEXTCLOUD_URL: ${NEXTCLOUD_URL:-https://cloud.example.com}
|
||||||
@@ -35,7 +33,7 @@ services:
|
|||||||
REDIS_URL: redis://redis:6379/0
|
REDIS_URL: redis://redis:6379/0
|
||||||
SECRET_KEY: ${SECRET_KEY:-replace_me_with_32_byte_hex_default}
|
SECRET_KEY: ${SECRET_KEY:-replace_me_with_32_byte_hex_default}
|
||||||
INTERNAL_SECRET: ${INTERNAL_SECRET:-replace_me_with_32_byte_hex_default}
|
INTERNAL_SECRET: ${INTERNAL_SECRET:-replace_me_with_32_byte_hex_default}
|
||||||
DOMAIN: ${DOMAIN:-localhost}
|
DOMAIN: localhost
|
||||||
ports:
|
ports:
|
||||||
- "8000:8000"
|
- "8000:8000"
|
||||||
networks:
|
networks:
|
||||||
@@ -47,13 +45,11 @@ services:
|
|||||||
web:
|
web:
|
||||||
build:
|
build:
|
||||||
context: ./web
|
context: ./web
|
||||||
target: development
|
target: production
|
||||||
volumes:
|
|
||||||
- ./web/src:/app/src
|
|
||||||
environment:
|
environment:
|
||||||
API_URL: http://api:8000
|
API_URL: http://api:8000
|
||||||
ports:
|
ports:
|
||||||
- "3000:3000"
|
- "3001:80"
|
||||||
networks:
|
networks:
|
||||||
- rh_net
|
- rh_net
|
||||||
depends_on:
|
depends_on:
|
||||||
|
|||||||
29
test_logging_reduction.js
Normal file
29
test_logging_reduction.js
Normal file
@@ -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 ===");
|
||||||
116
test_login_fix.py
Normal file
116
test_login_fix.py
Normal file
@@ -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()
|
||||||
0
web/src/App.tsx
Normal file → Executable file
0
web/src/App.tsx
Normal file → Executable file
0
web/src/api/annotations.ts
Normal file → Executable file
0
web/src/api/annotations.ts
Normal file → Executable file
0
web/src/api/auth.ts
Normal file → Executable file
0
web/src/api/auth.ts
Normal file → Executable file
0
web/src/api/bands.ts
Normal file → Executable file
0
web/src/api/bands.ts
Normal file → Executable file
0
web/src/api/client.ts
Normal file → Executable file
0
web/src/api/client.ts
Normal file → Executable file
0
web/src/api/invites.ts
Normal file → Executable file
0
web/src/api/invites.ts
Normal file → Executable file
0
web/src/components/AppShell.tsx
Normal file → Executable file
0
web/src/components/AppShell.tsx
Normal file → Executable file
0
web/src/components/BottomNavBar.tsx
Normal file → Executable file
0
web/src/components/BottomNavBar.tsx
Normal file → Executable file
0
web/src/components/InviteManagement.tsx
Normal file → Executable file
0
web/src/components/InviteManagement.tsx
Normal file → Executable file
0
web/src/components/MiniPlayer.tsx
Normal file → Executable file
0
web/src/components/MiniPlayer.tsx
Normal file → Executable file
0
web/src/components/ResponsiveLayout.tsx
Normal file → Executable file
0
web/src/components/ResponsiveLayout.tsx
Normal file → Executable file
0
web/src/components/Sidebar.tsx
Normal file → Executable file
0
web/src/components/Sidebar.tsx
Normal file → Executable file
0
web/src/components/TopBar.tsx
Normal file → Executable file
0
web/src/components/TopBar.tsx
Normal file → Executable file
21
web/src/hooks/useWaveform.ts
Normal file → Executable file
21
web/src/hooks/useWaveform.ts
Normal file → Executable file
@@ -45,25 +45,16 @@ export function useWaveform(
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!containerRef.current) {
|
if (!containerRef.current) {
|
||||||
console.debug('useWaveform: container ref is null, skipping initialization');
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!options.url || options.url === 'null' || options.url === 'undefined') {
|
if (!options.url || options.url === 'null' || options.url === 'undefined') {
|
||||||
console.debug('useWaveform: invalid URL, skipping initialization', { url: options.url });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.debug('useWaveform: initializing audio service', {
|
|
||||||
url: options.url,
|
|
||||||
songId: options.songId,
|
|
||||||
bandId: options.bandId,
|
|
||||||
containerExists: !!containerRef.current
|
|
||||||
});
|
|
||||||
|
|
||||||
const initializeAudio = async () => {
|
const initializeAudio = async () => {
|
||||||
try {
|
try {
|
||||||
console.debug('useWaveform: using audio service instance');
|
|
||||||
|
|
||||||
await audioService.initialize(containerRef.current!, options.url!);
|
await audioService.initialize(containerRef.current!, options.url!);
|
||||||
|
|
||||||
@@ -105,7 +96,7 @@ export function useWaveform(
|
|||||||
globalBandId === options.bandId &&
|
globalBandId === options.bandId &&
|
||||||
globalIsPlaying) {
|
globalIsPlaying) {
|
||||||
|
|
||||||
console.debug('useWaveform: restoring playback state');
|
|
||||||
|
|
||||||
// Wait a moment for the waveform to be ready
|
// Wait a moment for the waveform to be ready
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -120,7 +111,7 @@ export function useWaveform(
|
|||||||
options.onReady?.(audioService.getDuration());
|
options.onReady?.(audioService.getDuration());
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
console.debug('useWaveform: cleanup');
|
|
||||||
unsubscribe();
|
unsubscribe();
|
||||||
// Note: We don't cleanup the audio service here to maintain persistence
|
// Note: We don't cleanup the audio service here to maintain persistence
|
||||||
// audioService.cleanup();
|
// audioService.cleanup();
|
||||||
@@ -138,7 +129,7 @@ export function useWaveform(
|
|||||||
}, [options.url, options.songId, options.bandId, containerRef, currentSongId, globalBandId, globalCurrentTime, globalIsPlaying, setCurrentSong]);
|
}, [options.url, options.songId, options.bandId, containerRef, currentSongId, globalBandId, globalCurrentTime, globalIsPlaying, setCurrentSong]);
|
||||||
|
|
||||||
const play = () => {
|
const play = () => {
|
||||||
console.debug('useWaveform.play called');
|
|
||||||
try {
|
try {
|
||||||
audioService.play();
|
audioService.play();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -147,7 +138,7 @@ export function useWaveform(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const pause = () => {
|
const pause = () => {
|
||||||
console.debug('useWaveform.pause called');
|
|
||||||
try {
|
try {
|
||||||
audioService.pause();
|
audioService.pause();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -156,7 +147,7 @@ export function useWaveform(
|
|||||||
};
|
};
|
||||||
|
|
||||||
const seekTo = (time: number) => {
|
const seekTo = (time: number) => {
|
||||||
console.debug('useWaveform.seekTo called', { time });
|
|
||||||
try {
|
try {
|
||||||
if (isReady && isFinite(time)) {
|
if (isReady && isFinite(time)) {
|
||||||
audioService.seekTo(time);
|
audioService.seekTo(time);
|
||||||
|
|||||||
0
web/src/hooks/useWebSocket.ts
Normal file → Executable file
0
web/src/hooks/useWebSocket.ts
Normal file → Executable file
0
web/src/index.css
Normal file → Executable file
0
web/src/index.css
Normal file → Executable file
0
web/src/main.tsx
Normal file → Executable file
0
web/src/main.tsx
Normal file → Executable file
0
web/src/pages/BandPage.test.tsx
Normal file → Executable file
0
web/src/pages/BandPage.test.tsx
Normal file → Executable file
165
web/src/pages/BandPage.tsx
Normal file → Executable file
165
web/src/pages/BandPage.tsx
Normal file → Executable file
@@ -1,6 +1,6 @@
|
|||||||
import { useState, useMemo } from "react";
|
import { useState, useMemo } from "react";
|
||||||
import { useParams, Link } from "react-router-dom";
|
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 { getBand } from "../api/bands";
|
||||||
import { api } from "../api/client";
|
import { api } from "../api/client";
|
||||||
|
|
||||||
@@ -43,13 +43,6 @@ function formatDateLabel(iso: string): string {
|
|||||||
|
|
||||||
export function BandPage() {
|
export function BandPage() {
|
||||||
const { bandId } = useParams<{ bandId: string }>();
|
const { bandId } = useParams<{ bandId: string }>();
|
||||||
const qc = useQueryClient();
|
|
||||||
const [showCreate, setShowCreate] = useState(false);
|
|
||||||
const [newTitle, setNewTitle] = useState("");
|
|
||||||
const [error, setError] = useState<string | null>(null);
|
|
||||||
const [scanning, setScanning] = useState(false);
|
|
||||||
const [scanProgress, setScanProgress] = useState<string | null>(null);
|
|
||||||
const [scanMsg, setScanMsg] = useState<string | null>(null);
|
|
||||||
const [librarySearch, setLibrarySearch] = useState("");
|
const [librarySearch, setLibrarySearch] = useState("");
|
||||||
const [activePill, setActivePill] = useState<FilterPill>("all");
|
const [activePill, setActivePill] = useState<FilterPill>("all");
|
||||||
|
|
||||||
@@ -91,75 +84,6 @@ export function BandPage() {
|
|||||||
});
|
});
|
||||||
}, [unattributedSongs, librarySearch, activePill]);
|
}, [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<string, unknown>;
|
|
||||||
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 <div style={{ color: "var(--text-muted)", padding: 32 }}>Loading...</div>;
|
if (isLoading) return <div style={{ color: "var(--text-muted)", padding: 32 }}>Loading...</div>;
|
||||||
if (!band) return <div style={{ color: "var(--danger)", padding: 32 }}>Band not found</div>;
|
if (!band) return <div style={{ color: "var(--danger)", padding: 32 }}>Band not found</div>;
|
||||||
@@ -206,41 +130,6 @@ export function BandPage() {
|
|||||||
onBlur={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.08)")}
|
onBlur={(e) => (e.currentTarget.style.borderColor = "rgba(255,255,255,0.08)")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div style={{ marginLeft: "auto", display: "flex", gap: 8, flexShrink: 0 }}>
|
|
||||||
<button
|
|
||||||
onClick={startScan}
|
|
||||||
disabled={scanning}
|
|
||||||
style={{
|
|
||||||
background: "none",
|
|
||||||
border: "1px solid rgba(255,255,255,0.09)",
|
|
||||||
borderRadius: 6,
|
|
||||||
color: scanning ? "rgba(255,255,255,0.28)" : "#4dba85",
|
|
||||||
cursor: scanning ? "default" : "pointer",
|
|
||||||
padding: "5px 12px",
|
|
||||||
fontSize: 12,
|
|
||||||
fontFamily: "inherit",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{scanning ? "Scanning…" : "⟳ Scan Nextcloud"}
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { setShowCreate(!showCreate); setError(null); }}
|
|
||||||
style={{
|
|
||||||
background: "rgba(232,162,42,0.14)",
|
|
||||||
border: "1px solid rgba(232,162,42,0.28)",
|
|
||||||
borderRadius: 6,
|
|
||||||
color: "#e8a22a",
|
|
||||||
cursor: "pointer",
|
|
||||||
padding: "5px 12px",
|
|
||||||
fontSize: 12,
|
|
||||||
fontWeight: 600,
|
|
||||||
fontFamily: "inherit",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
+ Upload
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Filter pills */}
|
{/* Filter pills */}
|
||||||
@@ -271,56 +160,6 @@ export function BandPage() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* ── Scan feedback ─────────────────────────────────────── */}
|
|
||||||
{scanning && scanProgress && (
|
|
||||||
<div style={{ padding: "10px 26px 0", flexShrink: 0 }}>
|
|
||||||
<div style={{ background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)", borderRadius: 8, color: "rgba(255,255,255,0.42)", fontSize: 12, padding: "8px 14px", fontFamily: "monospace" }}>
|
|
||||||
{scanProgress}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{scanMsg && (
|
|
||||||
<div style={{ padding: "10px 26px 0", flexShrink: 0 }}>
|
|
||||||
<div style={{ background: "rgba(61,200,120,0.06)", border: "1px solid rgba(61,200,120,0.25)", borderRadius: 8, color: "#4dba85", fontSize: 12, padding: "8px 14px" }}>
|
|
||||||
{scanMsg}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── New song / upload form ─────────────────────────────── */}
|
|
||||||
{showCreate && (
|
|
||||||
<div style={{ padding: "14px 26px 0", flexShrink: 0 }}>
|
|
||||||
<div style={{ background: "rgba(255,255,255,0.025)", border: "1px solid rgba(255,255,255,0.07)", borderRadius: 8, padding: 18 }}>
|
|
||||||
{error && <p style={{ color: "#e07070", fontSize: 13, marginBottom: 12 }}>{error}</p>}
|
|
||||||
<label style={{ display: "block", color: "rgba(255,255,255,0.28)", fontSize: 11, letterSpacing: "0.6px", textTransform: "uppercase", marginBottom: 6 }}>
|
|
||||||
Song title
|
|
||||||
</label>
|
|
||||||
<input
|
|
||||||
value={newTitle}
|
|
||||||
onChange={(e) => 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
|
|
||||||
/>
|
|
||||||
<div style={{ display: "flex", gap: 8 }}>
|
|
||||||
<button
|
|
||||||
onClick={() => createMutation.mutate()}
|
|
||||||
disabled={!newTitle}
|
|
||||||
style={{ background: "rgba(232,162,42,0.14)", border: "1px solid rgba(232,162,42,0.28)", borderRadius: 6, color: "#e8a22a", cursor: newTitle ? "pointer" : "default", padding: "7px 18px", fontWeight: 600, fontSize: 13, fontFamily: "inherit", opacity: newTitle ? 1 : 0.4 }}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</button>
|
|
||||||
<button
|
|
||||||
onClick={() => { setShowCreate(false); setError(null); }}
|
|
||||||
style={{ background: "none", border: "1px solid rgba(255,255,255,0.09)", borderRadius: 6, color: "rgba(255,255,255,0.42)", cursor: "pointer", padding: "7px 18px", fontSize: 13, fontFamily: "inherit" }}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* ── Scrollable content ────────────────────────────────── */}
|
{/* ── Scrollable content ────────────────────────────────── */}
|
||||||
<div style={{ flex: 1, overflowY: "auto", padding: "4px 26px 26px" }}>
|
<div style={{ flex: 1, overflowY: "auto", padding: "4px 26px 26px" }}>
|
||||||
|
|
||||||
@@ -441,7 +280,7 @@ export function BandPage() {
|
|||||||
<p style={{ color: "rgba(255,255,255,0.28)", fontSize: 13, padding: "24px 0 8px" }}>
|
<p style={{ color: "rgba(255,255,255,0.28)", fontSize: 13, padding: "24px 0 8px" }}>
|
||||||
{librarySearch
|
{librarySearch
|
||||||
? "No results match your search."
|
? "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."}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
0
web/src/pages/BandSettingsPage.test.md
Normal file → Executable file
0
web/src/pages/BandSettingsPage.test.md
Normal file → Executable file
0
web/src/pages/BandSettingsPage.test.tsx
Normal file → Executable file
0
web/src/pages/BandSettingsPage.test.tsx
Normal file → Executable file
94
web/src/pages/BandSettingsPage.tsx
Normal file → Executable file
94
web/src/pages/BandSettingsPage.tsx
Normal file → Executable file
@@ -419,6 +419,68 @@ function StoragePanel({
|
|||||||
const qc = useQueryClient();
|
const qc = useQueryClient();
|
||||||
const [editing, setEditing] = useState(false);
|
const [editing, setEditing] = useState(false);
|
||||||
const [folderInput, setFolderInput] = useState("");
|
const [folderInput, setFolderInput] = useState("");
|
||||||
|
const [scanning, setScanning] = useState(false);
|
||||||
|
const [scanProgress, setScanProgress] = useState<string | null>(null);
|
||||||
|
const [scanMsg, setScanMsg] = useState<string | null>(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<string, unknown>;
|
||||||
|
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({
|
const updateMutation = useMutation({
|
||||||
mutationFn: (nc_folder_path: string) => api.patch(`/bands/${bandId}`, { nc_folder_path }),
|
mutationFn: (nc_folder_path: string) => api.patch(`/bands/${bandId}`, { nc_folder_path }),
|
||||||
@@ -538,6 +600,38 @@ function StoragePanel({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Scan action */}
|
||||||
|
<div style={{ marginTop: 16 }}>
|
||||||
|
<button
|
||||||
|
onClick={startScan}
|
||||||
|
disabled={scanning}
|
||||||
|
style={{
|
||||||
|
background: scanning ? "transparent" : "rgba(61,200,120,0.08)",
|
||||||
|
border: `1px solid ${scanning ? "rgba(255,255,255,0.07)" : "rgba(61,200,120,0.25)"}`,
|
||||||
|
borderRadius: 6,
|
||||||
|
color: scanning ? "rgba(255,255,255,0.28)" : "#4dba85",
|
||||||
|
cursor: scanning ? "default" : "pointer",
|
||||||
|
padding: "6px 14px",
|
||||||
|
fontSize: 12,
|
||||||
|
fontFamily: "inherit",
|
||||||
|
transition: "all 0.12s",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{scanning ? "Scanning…" : "⟳ Scan Nextcloud"}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{scanning && scanProgress && (
|
||||||
|
<div style={{ marginTop: 10, background: "rgba(255,255,255,0.03)", border: "1px solid rgba(255,255,255,0.07)", borderRadius: 8, color: "rgba(255,255,255,0.42)", fontSize: 12, padding: "8px 14px", fontFamily: "monospace" }}>
|
||||||
|
{scanProgress}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{scanMsg && (
|
||||||
|
<div style={{ marginTop: 10, background: "rgba(61,200,120,0.06)", border: "1px solid rgba(61,200,120,0.25)", borderRadius: 8, color: "#4dba85", fontSize: 12, padding: "8px 14px" }}>
|
||||||
|
{scanMsg}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
0
web/src/pages/HomePage.tsx
Normal file → Executable file
0
web/src/pages/HomePage.tsx
Normal file → Executable file
0
web/src/pages/InvitePage.tsx
Normal file → Executable file
0
web/src/pages/InvitePage.tsx
Normal file → Executable file
0
web/src/pages/LoginPage.tsx
Normal file → Executable file
0
web/src/pages/LoginPage.tsx
Normal file → Executable file
0
web/src/pages/SessionPage.tsx
Normal file → Executable file
0
web/src/pages/SessionPage.tsx
Normal file → Executable file
0
web/src/pages/SettingsPage.tsx
Normal file → Executable file
0
web/src/pages/SettingsPage.tsx
Normal file → Executable file
0
web/src/pages/SongPage.tsx
Normal file → Executable file
0
web/src/pages/SongPage.tsx
Normal file → Executable file
5
web/src/services/audioService.ts
Normal file → Executable file
5
web/src/services/audioService.ts
Normal file → Executable file
@@ -31,7 +31,7 @@ class AudioService {
|
|||||||
private readonly PLAY_DEBOUNCE_MS: number = 100;
|
private readonly PLAY_DEBOUNCE_MS: number = 100;
|
||||||
private lastSeekTime: number = 0;
|
private lastSeekTime: number = 0;
|
||||||
private readonly SEEK_DEBOUNCE_MS: number = 200;
|
private readonly SEEK_DEBOUNCE_MS: number = 200;
|
||||||
private logLevel: LogLevel = LogLevel.WARN;
|
private logLevel: LogLevel = LogLevel.ERROR;
|
||||||
private playbackAttempts: number = 0;
|
private playbackAttempts: number = 0;
|
||||||
private readonly MAX_PLAYBACK_ATTEMPTS: number = 3;
|
private readonly MAX_PLAYBACK_ATTEMPTS: number = 3;
|
||||||
|
|
||||||
@@ -203,17 +203,14 @@ private readonly PLAY_DEBOUNCE_MS: number = 100;
|
|||||||
const playerStore = usePlayerStore.getState();
|
const playerStore = usePlayerStore.getState();
|
||||||
|
|
||||||
ws.on("play", () => {
|
ws.on("play", () => {
|
||||||
this.log(LogLevel.DEBUG, 'AudioService: play event');
|
|
||||||
playerStore.batchUpdate({ isPlaying: true });
|
playerStore.batchUpdate({ isPlaying: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("pause", () => {
|
ws.on("pause", () => {
|
||||||
this.log(LogLevel.DEBUG, 'AudioService: pause event');
|
|
||||||
playerStore.batchUpdate({ isPlaying: false });
|
playerStore.batchUpdate({ isPlaying: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
ws.on("finish", () => {
|
ws.on("finish", () => {
|
||||||
this.log(LogLevel.DEBUG, 'AudioService: finish event');
|
|
||||||
playerStore.batchUpdate({ isPlaying: false });
|
playerStore.batchUpdate({ isPlaying: false });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
0
web/src/services/audioService.ts.backup2
Normal file → Executable file
0
web/src/services/audioService.ts.backup2
Normal file → Executable file
0
web/src/stores/playerStore.ts
Normal file → Executable file
0
web/src/stores/playerStore.ts
Normal file → Executable file
0
web/src/test/helpers.tsx
Normal file → Executable file
0
web/src/test/helpers.tsx
Normal file → Executable file
0
web/src/test/setup.ts
Normal file → Executable file
0
web/src/test/setup.ts
Normal file → Executable file
0
web/src/theme.ts
Normal file → Executable file
0
web/src/theme.ts
Normal file → Executable file
0
web/src/types/invite.ts
Normal file → Executable file
0
web/src/types/invite.ts
Normal file → Executable file
0
web/src/utils.ts
Normal file → Executable file
0
web/src/utils.ts
Normal file → Executable file
Reference in New Issue
Block a user