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>
This commit is contained in:
Mistral Vibe
2026-04-01 09:43:47 +02:00
parent ae7bf96dc1
commit d9035acdff
11 changed files with 763 additions and 255 deletions

View File

@@ -7,7 +7,7 @@ class Settings(BaseSettings):
# Security
secret_key: str
internal_secret: str # Shared secret for internal service-to-service calls
internal_secret: str = "dev-change-me-in-production" # Shared secret for internal service-to-service calls
jwt_algorithm: str = "HS256"
access_token_expire_minutes: int = 60 # 1 hour

View File

@@ -79,12 +79,18 @@ async def _member_from_request(
token: str | None = Query(None),
session: AsyncSession = Depends(get_session),
) -> Member:
"""Resolve member from Authorization header or ?token= query param."""
"""Resolve member from Authorization header, ?token= query param, or httpOnly cookie.
The cookie fallback allows fetch()-based callers (which send credentials:include)
to use the same endpoint as EventSource callers (which must use ?token=).
"""
auth_header = request.headers.get("Authorization")
if auth_header:
scheme, param = get_authorization_scheme_param(auth_header)
if scheme.lower() == "bearer":
token = param
if not token:
token = request.cookies.get("rh_token")
if not token:
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Token required")
try: