Commit Graph

43 Commits

Author SHA1 Message Date
Mistral Vibe
a8cbd333d2 Fix dev container startup for both API and web
API:
- Add python symlink (python3-slim has no bare 'python') so uv doesn't
  invalidate the baked venv on every container start
- Copy src/ before uv sync so hatchling installs rehearsalhub as a
  proper editable install (.pth pointing to /app/src) — previously
  sync ran with no source present, producing a broken empty wheel
- Remove ENV PYTHONPATH workaround (no longer needed with correct install)
- Add --reload-dir /app/src to scope uvicorn's file watcher to the
  mounted source directory

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 19:14:21 +02:00
Mistral Vibe
44503cca30 Add hot-reload dev environment via docker-compose.dev.yml
- web/Dockerfile: add `development` stage that installs deps and runs
  `vite dev --host 0.0.0.0`; source is mounted at runtime so edits
  reflect immediately without rebuilding the image
- web/vite.config.ts: read proxy target from API_URL env var
  (falls back to localhost:8000 for outside-compose usage)
- docker-compose.dev.yml: lightweight compose for development
  - api uses existing `development` target (uvicorn --reload)
  - web uses new `development` target with ./web mounted as volume
    and an anonymous volume to preserve container node_modules
  - worker and nc-watcher omitted (not needed for UI work)
  - separate pg_data_dev volume keeps dev DB isolated from prod

Usage:
  podman-compose -f docker-compose.dev.yml up --build

Frontend hot-reloads at http://localhost:3000
API auto-reloads at http://localhost:8000

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:49:33 +02:00
Mistral Vibe
659598913b Remove play button from Library session rows
Play buttons don't make sense at the session level since sessions
group multiple recordings. Removed from both session rows and
unattributed song rows.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:44:02 +02:00
Mistral Vibe
013a2fc2d6 Fix Invalid Date for datetime strings from API
The API returns dates as "2024-12-11T00:00:00" (datetime, no timezone),
not bare "2024-12-11". Appending T12:00:00 directly produced an invalid
string. Use .slice(0, 10) to extract the date part first before parsing.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:40:38 +02:00
Mistral Vibe
b09094658c Redesign Library view to match mockup spec
- Replace band-name header + tab structure (By Date / Search) with a
  unified Library view: title, inline search input, filter pills
  (All / instrument / Commented), and date-group headers
- Session rows now use the recording-row card style (play circle,
  mono filename, recording count)
- Move mini waveform bars from session list to individual recording
  rows in SessionPage, where they correspond to a single track
- Fix Invalid Date by appending T12:00:00 when parsing date-only
  ISO strings in both BandPage and SessionPage
- Update tests: drop tab assertions (TC-07), add Library heading
  (TC-08) and filter pill (TC-09) checks, update upload button label

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-06 18:37:49 +02:00
Mistral Vibe
16bfdd2e90 Move band management into dedicated settings pages
- Add BandSettingsPage (/bands/:id/settings/:panel) with Members,
  Storage, and Band Settings panels matching the mockup design
- Strip members list, invite controls, and NC folder config from
  BandPage — library view now focuses purely on recordings workflow
- Add band-scoped nav section to AppShell sidebar (Members, Storage,
  Band Settings) with correct per-panel active states
- Fix amAdmin bug: was checking if any member is admin; now correctly
  checks if the current user holds the admin role
- Add 31 vitest tests covering BandPage cleanliness, routing, access
  control (admin vs member), and per-panel mutation behaviour
- Add test:web, test:api:unit, test:feature (post-feature pipeline),
  and ci tasks to Taskfile; frontend tests run via podman node:20-alpine
- Add README with architecture overview, setup guide, and test docs
- Add @testing-library/dom and @testing-library/jest-dom to package.json

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 14:55:10 +02:00
Mistral Vibe
5d7c613317 Component for invitations 2026-04-01 12:12:26 +02:00
Mistral Vibe
d61772207e Fix remaining TS6133 errors
- invites.ts: Remove unused bandId from listNonMemberUsers
- InviteManagement.tsx: Remove unused code (useEffect, queryClient, isRefreshing)

All TypeScript errors resolved!

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 11:55:38 +02:00
Mistral Vibe
1280020f83 Temporarily remove UserSearch - needs backend support
UserSearch component disabled until backend endpoint for listing non-member users is implemented.

Currently MVP includes:
- Backend APIs: list invites, revoke invites, get invite info 
- Frontend: InviteManagement component (list + revoke) 
- Note: UserSearch (admin search) disabled - needs backend support

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 11:53:30 +02:00
Mistral Vibe
2aa8ec8c59 Fix TypeScript build errors
- Remove unused imports in invites.ts
- Fix InviteManagement component (remove unused props, unneeded code)
- Fix BandPage.tsx (remove currentMemberId, remove UserSearch for now)
- Remove unused imports in types/invite.ts

Build errors resolved:
- TS6133: unused variables
- TS2304: missing variables
- TS2307: module not found

Note: UserSearch temporarily disabled - needs backend support for listing non-members

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 11:53:00 +02:00
Mistral Vibe
81c90222d5 Phase 2 frontend: Add React components for band invite management
Components created:
- InviteManagement.tsx: List pending invites, revoke functionality, copy links
- UserSearch.tsx: Search users to invite, role selection
- web/src/api/invites.ts: API wrappers for new endpoints
- web/src/types/invites.ts: TypeScript interfaces

UI enhancements:
- BandPage.tsx: Integrated new components, admin-only sections
- Members section now includes invite management for admins
- Search component for finding users to invite

Features:
- Admin can list, view, and revoke pending invites
- Copy invite links to clipboard
- Search existing users to invite (excluding current members)
- Real-time invite status (pending/expired/used)

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-04-01 11:48:14 +02:00
Mistral Vibe
6dc191585c style: align all pages with CLAUDE.md design system
- Inputs: uniform padding (8px 12px), borderRadius 7, bg-inset background
- List rows/cards: bg-subtle background, border-subtle border (bg-inset was input-only)
- Invite/admin badge borders: use accent-border var instead of raw accent
- Section headers: 11px, weight 500, uppercase, 0.7px letter-spacing
- Notification/status banners: borderRadius 8
- Remove debug console.log statements from SettingsPage avatar upload flow

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-01 09:43:47 +02:00
Mistral Vibe
c1941ed9ac security: httpOnly cookies, rate limiting, nginx headers, SSE sanitization
Auth / token storage:
- JWT is now set as an httpOnly Secure SameSite=Lax cookie on login
- Add POST /auth/logout endpoint that clears the cookie
- get_current_member falls back to rh_token cookie when no Authorization header
- WebSocket auth now accepts cookie (rh_token) or optional ?token= query param
- Frontend removes all localStorage JWT access; uses credentials:"include" on
  every fetch so the httpOnly cookie is sent automatically
- Replace clearToken() with logout() that calls the server logout endpoint
- Non-sensitive rh_session flag in localStorage used only for client-side routing

Rate limiting:
- Add slowapi>=0.1.9 dependency
- /auth/login limited to 10 req/min per IP
- /auth/register limited to 5 req/min per IP

Nginx security headers:
- Add X-Frame-Options, X-Content-Type-Options, Referrer-Policy,
  X-XSS-Protection, Permissions-Policy to all responses

SSE error leakage:
- songs.py nc-scan/stream no longer leaks str(exc) to clients

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 21:11:53 +02:00
Mistral Vibe
cd1d098ca4 fix: avatar stale state, nginx intercept, and dev tooling
Frontend (SettingsPage):
- Sync avatarUrl state via useEffect when me.avatar_url changes after
  background refetch, so profile section never shows stale avatar
- Invalidate ["comments"] after upload/generate/remove so SongPage
  comment avatars update immediately instead of waiting for staleTime
- Fix Remove button: was sending avatar_url: undefined which JSON.stringify
  drops entirely, so the server never cleared it; now sends ""

nginx:
- Change /api/ and /ws/ locations to use ^~ prefix so the static-asset
  regex rule (~* \.(png|svg|ico)$) cannot intercept API paths; PNG/SVG
  avatar uploads were returning 404 from nginx in production
- Merge nc-scan 300s timeout into ^~ /api/v1/bands/ block
- Add client_max_body_size 10m (default 1MB was silently rejecting
  uploads before they reached FastAPI)

Dev tooling:
- Add docker-compose.dev.yml for hot-reload development workflow
- Add Taskfile.yml with dev, test, lint, migrate, and shell tasks

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-30 20:17:21 +02:00
Mistral Vibe
da051be673 fix: proper TypeScript type assertion for error object
- Use type assertion to define error object structure
- Use optional chaining for safe property access
- Maintain all error handling functionality

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:58:29 +02:00
Mistral Vibe
e4beebc57e fix: add type guard for detail property in error data
- Check for 'detail' property before accessing it
- Maintain all error handling functionality
- Ensure TypeScript type safety

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:57:24 +02:00
Mistral Vibe
b66660ee30 fix: TypeScript type safety for error object properties
- Add proper type guards for error object properties
- Check for 'status' and 'data' properties before accessing
- Maintain all debugging functionality

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:56:41 +02:00
Mistral Vibe
a62297f2c4 fix: add comprehensive debugging and validation for avatar uploads
- Add detailed error extraction from API responses
- Validate file content is not empty before saving
- Verify file was actually saved to disk
- Check saved file size matches expectations
- Add extensive logging for debugging upload issues
- Improve error messages with specific details

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

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:49:39 +02:00
Mistral Vibe
48969e110a fix: remove incorrect headers parameter from API call
- Remove extra parameter that was causing TypeScript error
- Keep all other file size handling improvements

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

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

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

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:34:28 +02:00
Mistral Vibe
a17b4a7ec0 fix: TypeScript errors in avatar implementation
- Remove unnecessary headers parameter from API call
- Fix unused error parameter in onError handler
- Use undefined instead of null for avatar_url removal

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

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

Generated by Mistral Vibe.
Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
2026-03-30 19:15:24 +02:00
Mistral Vibe
3b8c4a0cb8 fix: comment waveform integration with timestamps and avatars
- Add author_avatar_url to API schema and frontend interface
- Capture current playhead timestamp when creating comments
- Display user avatars in waveform markers instead of placeholders
- Improve marker visibility with better styling (size, borders, shadows)
- Fix TypeScript type errors for nullable timestamps
- Add debug logging for troubleshooting

This implements the full comment waveform integration as requested:
- Comments are created with exact playhead timestamps
- Waveform markers show at correct positions with user avatars
- Clicking markers scrolls to corresponding comments
- Backward compatible with existing comments without timestamps
2026-03-30 19:06:40 +02:00
Mistral Vibe
a8aba72b3a WIP: Add timestamp to comments and fix frontend errors 2026-03-29 22:06:36 +02:00
Mistral Vibe
e71c7fc3ad Fix play behaviour in track view
- Version switching now stops playback and resets UI state
- Playback must be started manually after version switch
- Added space key shortcut for play/pause toggle
- UI elements correctly reflect playback state

Fixes: Play/pause controls work correctly after version switching
Improves: User experience with keyboard shortcuts
2026-03-29 20:39:15 +02:00
Mistral Vibe
1179d9f063 WIP: Fix play behaviour in track view
- Added wasPlayingRef to preserve playback state across version changes
- Auto-play new waveform if previous version was playing
- Ensure play/pause buttons work correctly after version switch
- Added WebSocket proxy configuration for real-time features

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 14:11:07 +02:00
Steffen Schuhmann
25502458d0 feat(web): BandPage — By Date and Search tabs
Replaces flat song list with two tabs:
- By Date: session list (newest first) with weekday, date, label,
  recording count — each row links to SessionPage
- Search: title/key/BPM range/tag filters, results as flat song list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:45:12 +02:00
Steffen Schuhmann
5be1a9808f feat(web): SessionPage — rehearsal date detail view
Shows date, optional label/notes (admin-editable), and a flat list
of all recordings from that session. Each recording links to SongPage
and shows tags, key, BPM chips inline.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 13:42:22 +02:00
Steffen Schuhmann
53da085f28 fix: proxy audio stream through API with query-param token auth
WaveSurfer makes plain browser fetches without Authorization headers,
causing 401s on the stream endpoint. The stream endpoint now accepts
a ?token= query param in addition to the Authorization header, and
proxies audio bytes directly through FastAPI instead of redirecting to
raw WebDAV (which would require a second Nextcloud auth challenge).
Falls back to nc_file_path if HLS transcoding hasn't run yet.

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

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

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

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

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-03-29 00:29:58 +01:00
Steffen Schuhmann
a6a64032ec fix: auto-logout on 401, raise nginx timeout for nc-scan
401 Unauthorized loop:
- client.ts had no 401 handler, leaving stale expired tokens in
  localStorage. The PrivateRoute guard only checked token existence,
  so the app would render but every API call would fail silently.
- Fix: on any 401 response, clear the token and redirect to /login.

504 Gateway Timeout on nc-scan:
- nginx default proxy_read_timeout is 60s. The scan endpoint makes
  one Nextcloud request per audio file (list + metadata), which easily
  exceeds that on larger libraries.
- Fix: add a dedicated location block for nc-scan with 300s timeouts.
  General /api/ block gets explicit 60s timeouts for clarity.

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

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

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

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

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