From cd1d098ca49bbfe95a3ce40decec8ba5e73da956 Mon Sep 17 00:00:00 2001 From: Mistral Vibe Date: Mon, 30 Mar 2026 20:41:32 +0200 Subject: [PATCH] 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 --- Taskfile.yml | 163 +++++++++++++++++++++++++++++++++ docker-compose.dev.yml | 17 ++++ web/nginx.conf | 41 +++++---- web/src/pages/SettingsPage.tsx | 13 ++- 4 files changed, 215 insertions(+), 19 deletions(-) create mode 100644 Taskfile.yml create mode 100644 docker-compose.dev.yml diff --git a/Taskfile.yml b/Taskfile.yml new file mode 100644 index 0000000..7b10e87 --- /dev/null +++ b/Taskfile.yml @@ -0,0 +1,163 @@ +version: "3" + +vars: + COMPOSE: docker compose + DEV_FLAGS: -f docker-compose.yml -f docker-compose.dev.yml + DEV_SERVICES: db redis api audio-worker nc-watcher + +# ── Production ──────────────────────────────────────────────────────────────── + +tasks: + up: + desc: Start all services (production) + cmds: + - "{{.COMPOSE}} up -d" + + down: + desc: Stop all services + cmds: + - "{{.COMPOSE}} down" + + build: + desc: Build all images + deps: [check] + cmds: + - "{{.COMPOSE}} build" + + logs: + desc: Follow logs for all services (pass SERVICE= to filter) + cmds: + - "{{.COMPOSE}} logs -f {{.SERVICE}}" + + restart: + desc: Restart a service without rebuilding (e.g. task restart SERVICE=api) + cmds: + - "{{.COMPOSE}} restart {{.SERVICE}}" + +# ── Dev / Debug ─────────────────────────────────────────────────────────────── + + dev: + desc: Start backend in dev mode (hot reload, source mounts) + cmds: + - "{{.COMPOSE}} {{.DEV_FLAGS}} up {{.DEV_SERVICES}}" + + dev:detach: + desc: Start backend in dev mode, detached + cmds: + - "{{.COMPOSE}} {{.DEV_FLAGS}} up -d {{.DEV_SERVICES}}" + + dev:web: + desc: Start Vite dev server (proxies /api to localhost:8000) + dir: web + cmds: + - npm run dev + + dev:logs: + desc: Follow logs in dev mode + cmds: + - "{{.COMPOSE}} {{.DEV_FLAGS}} logs -f {{.SERVICE}}" + + dev:restart: + desc: Restart a service in dev mode (e.g. task dev:restart SERVICE=audio-worker) + cmds: + - "{{.COMPOSE}} {{.DEV_FLAGS}} restart {{.SERVICE}}" + +# ── Database ────────────────────────────────────────────────────────────────── + + migrate: + desc: Run Alembic migrations + cmds: + - "{{.COMPOSE}} exec api alembic upgrade head" + + migrate:auto: + desc: Autogenerate a migration (e.g. task migrate:auto M="add users table") + cmds: + - "{{.COMPOSE}} exec api alembic revision --autogenerate -m '{{.M}}'" + +# ── Setup ───────────────────────────────────────────────────────────────────── + + setup: + desc: First-time setup — start services, configure Nextcloud, seed data + cmds: + - task: up + - echo "Waiting for Nextcloud to initialize (~60s)..." + - sleep 60 + - bash scripts/nc-setup.sh + - bash scripts/seed.sh + +# ── Testing ─────────────────────────────────────────────────────────────────── + + test: + desc: Run all tests + deps: [test:api, test:worker, test:watcher] + + test:api: + desc: Run API tests with coverage + dir: api + cmds: + - uv run pytest tests/ -v --cov=src/rehearsalhub --cov-report=term-missing + + test:worker: + desc: Run worker tests with coverage + dir: worker + cmds: + - uv run pytest tests/ -v --cov=src/worker --cov-report=term-missing + + test:watcher: + desc: Run watcher tests with coverage + dir: watcher + cmds: + - uv run pytest tests/ -v --cov=src/watcher --cov-report=term-missing + + test:integration: + desc: Run integration tests + dir: api + cmds: + - uv run pytest tests/integration/ -v -m integration + +# ── Linting & type checking ─────────────────────────────────────────────────── + + check: + desc: Run all linters and type checkers + deps: [lint, typecheck:web] + + lint: + desc: Lint all services + cmds: + - cd api && uv run ruff check src/ tests/ && uv run mypy src/ + - cd worker && uv run ruff check src/ tests/ + - cd watcher && uv run ruff check src/ tests/ + - cd web && npm run lint + + typecheck:web: + desc: TypeScript type check + dir: web + cmds: + - npm run typecheck + + format: + desc: Auto-format Python source + cmds: + - cd api && uv run ruff format src/ tests/ + - cd worker && uv run ruff format src/ tests/ + - cd watcher && uv run ruff format src/ tests/ + +# ── Shells ──────────────────────────────────────────────────────────────────── + + shell:api: + desc: Shell into the API container + interactive: true + cmds: + - "{{.COMPOSE}} exec api bash" + + shell:db: + desc: psql shell + interactive: true + cmds: + - "{{.COMPOSE}} exec db psql -U $POSTGRES_USER -d $POSTGRES_DB" + + shell:redis: + desc: redis-cli shell + interactive: true + cmds: + - "{{.COMPOSE}} exec redis redis-cli" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml new file mode 100644 index 0000000..5aa52b8 --- /dev/null +++ b/docker-compose.dev.yml @@ -0,0 +1,17 @@ +services: + api: + build: + context: ./api + target: development + volumes: + - ./api/src:/app/src + ports: + - "8000:8000" + + audio-worker: + volumes: + - ./worker/src:/app/src + + nc-watcher: + volumes: + - ./watcher/src:/app/src diff --git a/web/nginx.conf b/web/nginx.conf index cd1f909..c05e301 100644 --- a/web/nginx.conf +++ b/web/nginx.conf @@ -3,8 +3,27 @@ server { root /usr/share/nginx/html; index index.html; - # Proxy API requests to the FastAPI backend - location /api/ { + # Allow avatar uploads up to 10MB (API enforces a 5MB limit) + client_max_body_size 10m; + + # Band routes — NC scan can take several minutes on large libraries. + # ^~ prevents the static-asset regex below from matching /api/ paths. + location ^~ /api/v1/bands/ { + proxy_pass http://api:8000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + + proxy_read_timeout 300s; + proxy_send_timeout 300s; + } + + # All other API requests (including /api/static/avatars/* served by FastAPI). + # ^~ prevents the static-asset regex below from intercepting these paths. + location ^~ /api/ { proxy_pass http://api:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; @@ -19,26 +38,13 @@ server { proxy_send_timeout 60s; } - # NC scan hits Nextcloud for every file — can take several minutes on large libraries - location ~ ^/api/v1/bands/[^/]+/nc-scan { - proxy_pass http://api:8000; - proxy_set_header Host $host; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_http_version 1.1; - - proxy_read_timeout 300s; - proxy_send_timeout 300s; - } - # WebSocket proxy for real-time version room events - location /ws/ { + location ^~ /ws/ { proxy_pass http://api:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - # WebSocket specific headers proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection "upgrade"; @@ -52,7 +58,8 @@ server { try_files $uri $uri/ /index.html; } - # Cache static assets aggressively + # Cache static assets aggressively (Vite build output — hashed filenames). + # This regex only runs for paths NOT matched by the ^~ rules above. location ~* \.(js|css|woff2|png|svg|ico)$ { expires 1y; add_header Cache-Control "public, immutable"; diff --git a/web/src/pages/SettingsPage.tsx b/web/src/pages/SettingsPage.tsx index af0a7ea..5a5679e 100644 --- a/web/src/pages/SettingsPage.tsx +++ b/web/src/pages/SettingsPage.tsx @@ -1,4 +1,4 @@ -import { useState } from "react"; +import { useState, useEffect } from "react"; import { useNavigate } from "react-router-dom"; import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; import { api } from "../api/client"; @@ -44,6 +44,12 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { const [saved, setSaved] = useState(false); const [error, setError] = useState(null); + // Keep local avatarUrl in sync when the server-side value changes (e.g. after + // a background refetch or a change made on another device). + useEffect(() => { + setAvatarUrl(me.avatar_url ?? ""); + }, [me.avatar_url]); + // Image resizing function const resizeImage = (file: File, maxWidth: number, maxHeight: number): Promise => { return new Promise((resolve, reject) => { @@ -216,6 +222,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { setAvatarUrl(response.avatar_url || ''); qc.invalidateQueries({ queryKey: ['me'] }); + qc.invalidateQueries({ queryKey: ['comments'] }); } catch (err) { console.error("Upload failed:", err); let errorMessage = 'Failed to upload avatar. Please try again.'; @@ -271,6 +278,7 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) { await updateSettings({ avatar_url: newAvatarUrl }); setAvatarUrl(newAvatarUrl); qc.invalidateQueries({ queryKey: ["me"] }); + qc.invalidateQueries({ queryKey: ["comments"] }); console.log("Avatar updated successfully"); } catch (err) { @@ -306,9 +314,10 @@ function SettingsForm({ me, onBack }: { me: MemberRead; onBack: () => void }) {