Initial commit: RehearsalHub POC

Full-stack self-hosted band rehearsal platform:

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Steffen Schuhmann
2026-03-28 21:53:03 +01:00
commit f7be1b994d
139 changed files with 12743 additions and 0 deletions

48
api/tests/conftest.py Normal file
View File

@@ -0,0 +1,48 @@
"""Top-level test fixtures. Unit tests use these mocked fixtures (no external services)."""
import uuid
from datetime import datetime, timezone
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from sqlalchemy.ext.asyncio import AsyncSession
@pytest.fixture
def mock_session():
"""AsyncSession mock for unit tests."""
session = AsyncMock(spec=AsyncSession)
session.flush = AsyncMock()
session.commit = AsyncMock()
session.rollback = AsyncMock()
session.refresh = AsyncMock()
session.get = AsyncMock(return_value=None)
session.execute = AsyncMock()
session.delete = AsyncMock()
session.add = MagicMock()
return session
@pytest.fixture
def sample_member_id():
return uuid.uuid4()
@pytest.fixture
def sample_band_id():
return uuid.uuid4()
@pytest.fixture
def sample_song_id():
return uuid.uuid4()
@pytest.fixture
def sample_version_id():
return uuid.uuid4()
@pytest.fixture
def sample_annotation_id():
return uuid.uuid4()