diff --git a/.claude/skills/dev-server/SKILL.md b/.claude/skills/dev-server/SKILL.md index daf69e6..afaa258 100644 --- a/.claude/skills/dev-server/SKILL.md +++ b/.claude/skills/dev-server/SKILL.md @@ -1,6 +1,6 @@ # Dev Server Skill -Start both frontend and backend development servers with pre-flight checks. +Start the complete Mantimon TCG development environment including all services. ## Usage @@ -9,9 +9,9 @@ Start both frontend and backend development servers with pre-flight checks. ``` **Commands:** -- `start` (default) - Run checks and start both servers -- `stop` - Stop all running dev servers -- `status` - Check if servers are running +- `start` (default) - Start all services (Docker infra + backend + frontend) +- `stop` - Stop all running services +- `status` - Check status of all services ## Instructions @@ -19,144 +19,225 @@ When this skill is invoked, follow these steps: ### 1. Pre-flight Checks -Run all checks in parallel where possible: - -#### Environment Variables -Check that required env files exist and have valid values: - -**Frontend** (`frontend/.env.development`): -- `VITE_API_BASE_URL` - Must be a valid URL -- `VITE_WS_URL` - Must be a valid URL -- `VITE_OAUTH_REDIRECT_URI` - Must be a valid URL - -**Backend** (`backend/.env` or environment): -- `DATABASE_URL` - PostgreSQL connection string -- `REDIS_URL` - Redis connection string -- `JWT_SECRET` - Must be set (check length >= 32) - -#### Port Availability -Check if default ports are available: -- Frontend: 5173, 3000, 3001 (Vite will auto-increment) -- Backend: 8000, 8001 +Run checks to understand current state: ```bash -# Check port availability -lsof -i :8000 2>/dev/null | grep LISTEN -lsof -i :5173 2>/dev/null | grep LISTEN +# Check Docker services +cd /mnt/NV2/Development/mantimon-tcg/backend +docker compose ps 2>/dev/null | grep -E "mantimon-(postgres|redis|adminer)" || echo "Docker services: not running" + +# Check application servers +curl -s http://localhost:8001/health 2>/dev/null && echo "Backend: running" || echo "Backend: not running" +curl -s http://localhost:3001 >/dev/null 2>&1 && echo "Frontend: running" || echo "Frontend: not running" + +# Check environment files +[ -f "/mnt/NV2/Development/mantimon-tcg/frontend/.env.development" ] && echo "Frontend .env: OK" || echo "Frontend .env: MISSING" +[ -f "/mnt/NV2/Development/mantimon-tcg/backend/.env" ] && echo "Backend .env: OK" || echo "Backend .env: MISSING" + +# Check dependencies +[ -d "/mnt/NV2/Development/mantimon-tcg/frontend/node_modules" ] && echo "node_modules: OK" || echo "node_modules: MISSING" +[ -d "/mnt/NV2/Development/mantimon-tcg/backend/.venv" ] && echo "Backend venv: OK" || echo "Backend venv: MISSING" ``` -#### Dependencies -Verify dependencies are installed: +### 2. Start Infrastructure Services (Docker Compose) + +Start PostgreSQL, Redis, and Adminer using the existing docker compose file: + +```bash +cd /mnt/NV2/Development/mantimon-tcg/backend +docker compose up -d + +# Wait for services to be healthy +echo "Waiting for services to be ready..." +sleep 5 + +# Verify services are up +docker compose ps +``` + +### 3. Install Dependencies (if needed) + ```bash # Frontend -[ -d "frontend/node_modules" ] || echo "Run: cd frontend && npm install" +if [ ! -d "/mnt/NV2/Development/mantimon-tcg/frontend/node_modules" ]; then + echo "Installing frontend dependencies..." + cd /mnt/NV2/Development/mantimon-tcg/frontend && npm install +fi # Backend -[ -f "backend/.venv/bin/activate" ] || echo "Run: cd backend && uv sync" +if [ ! -d "/mnt/NV2/Development/mantimon-tcg/backend/.venv" ]; then + echo "Installing backend dependencies..." + cd /mnt/NV2/Development/mantimon-tcg/backend && uv sync +fi ``` -#### Database/Redis -Check if PostgreSQL and Redis are accessible: +### 4. Run Database Migrations + ```bash -# PostgreSQL (if DATABASE_URL is set) -pg_isready -d "$DATABASE_URL" 2>/dev/null - -# Redis (if REDIS_URL is set) -redis-cli -u "$REDIS_URL" ping 2>/dev/null +cd /mnt/NV2/Development/mantimon-tcg/backend && uv run alembic upgrade head ``` -### 2. Report Issues - -If any checks fail, display a clear report: - -```markdown -## Pre-flight Check Results - -| Check | Status | Action Required | -|-------|--------|-----------------| -| Frontend .env | MISSING | Create frontend/.env.development | -| Port 8000 | IN USE | Kill process or use --port 8001 | -| node_modules | MISSING | Run: cd frontend && npm install | -| PostgreSQL | OK | - | -``` - -If there are blocking issues, ask the user how to proceed. - -### 3. Start Servers +### 5. Start Application Servers Start both servers in the background: ```bash -# Start backend first (frontend depends on it) -cd backend && uv run uvicorn app.main:app --reload --port $BACKEND_PORT 2>&1 & +# Start backend (port 8001) +cd /mnt/NV2/Development/mantimon-tcg/backend && uv run uvicorn app.main:app --reload --port 8001 2>&1 & # Wait for backend to be ready -sleep 3 +for i in {1..15}; do + curl -s http://localhost:8001/health 2>/dev/null && break + sleep 1 +done -# Start frontend -cd frontend && npm run dev 2>&1 & +# Start frontend (port 3001) +cd /mnt/NV2/Development/mantimon-tcg/frontend && npm run dev 2>&1 & + +# Wait for frontend to be ready +for i in {1..15}; do + curl -s http://localhost:3001 >/dev/null 2>&1 && break + sleep 1 +done ``` -### 4. Verify and Display Status +### 6. Display Status -Wait for both servers to be ready, then display: +After starting, display the status table: ```markdown -## Dev Environment Ready +## Dev Environment Status -| Service | URL | Status | -|---------|-----|--------| -| Frontend | http://localhost:5173/ | Running | -| Backend API | http://localhost:8000/ | Running | -| Backend Docs | http://localhost:8000/docs | Running | -| PostgreSQL | localhost:5432 | Connected | -| Redis | localhost:6379 | Connected | +| Service | URL/Port | Status | +|---------|----------|--------| +| Frontend | http://localhost:3001 | Running | +| Backend API | http://localhost:8001 | Running | +| API Docs | http://localhost:8001/docs | Running | +| PostgreSQL | localhost:5433 | Running | +| Redis | localhost:6380 | Running | +| Adminer | http://localhost:8090 | Running | **Quick Links:** -- App: http://localhost:5173/ -- API Docs: http://localhost:8000/docs -- Health Check: http://localhost:8000/api/health +- App: http://localhost:3001 +- API Docs: http://localhost:8001/docs +- Health Check: http://localhost:8001/health +- Database Admin: http://localhost:8090 (System: PostgreSQL, Server: mantimon-postgres, User: mantimon, Pass: mantimon) + +**OAuth Status:** +- Discord: [Configured/Not configured] +- Google: [Configured/Not configured] ``` -### 5. Stop Command +Check OAuth configuration: +```bash +# Check if Discord is configured (has actual values, not placeholder) +grep -E "^DISCORD_CLIENT_ID=.+" /mnt/NV2/Development/mantimon-tcg/backend/.env 2>/dev/null | grep -v "your-client-id" && \ + echo "Discord OAuth: Configured" || echo "Discord OAuth: Not configured" + +# Check if Google is configured +grep -E "^GOOGLE_CLIENT_ID=.+" /mnt/NV2/Development/mantimon-tcg/backend/.env 2>/dev/null | grep -v "your-client-id" && \ + echo "Google OAuth: Configured" || echo "Google OAuth: Not configured" +``` + +### 7. Stop Command When `stop` is requested: + ```bash -pkill -f "uvicorn app.main:app" -pkill -f "vite" +# Stop application servers +pkill -f "uvicorn app.main:app" 2>/dev/null +pkill -f "vite" 2>/dev/null + +echo "Application servers stopped." ``` -### 6. Status Command +Then ask: "Stop Docker containers (PostgreSQL/Redis/Adminer) too? They can be left running for faster restarts." -When `status` is requested, check running processes and display current state. - -## Common Gotchas - -The skill should check for and warn about: - -1. **Port mismatch** - Frontend .env pointing to wrong backend port -2. **Missing OAuth config** - Google/Discord credentials not set -3. **Database not migrated** - Check if alembic migrations are pending -4. **Redis not running** - Socket.IO requires Redis for pub/sub -5. **Old processes** - Zombie uvicorn/vite processes from crashed sessions -6. **TypeScript errors** - Run `npm run typecheck` before starting - -## Environment Variable Reference - -### Frontend (.env.development) +If yes: ```bash -VITE_API_BASE_URL=http://localhost:8000 -VITE_WS_URL=http://localhost:8000 -VITE_OAUTH_REDIRECT_URI=http://localhost:5173/auth/callback +cd /mnt/NV2/Development/mantimon-tcg/backend && docker compose down ``` -### Backend (.env) +### 8. Status Command + +When `status` is requested, check and display all services without starting anything: + ```bash -DATABASE_URL=postgresql+asyncpg://user:pass@localhost:5432/mantimon -REDIS_URL=redis://localhost:6379/0 -JWT_SECRET=your-secret-key-at-least-32-characters -GOOGLE_CLIENT_ID=optional -GOOGLE_CLIENT_SECRET=optional -DISCORD_CLIENT_ID=optional -DISCORD_CLIENT_SECRET=optional +cd /mnt/NV2/Development/mantimon-tcg/backend + +echo "=== Docker Services ===" +docker compose ps + +echo "" +echo "=== Application Servers ===" +curl -s http://localhost:8001/health 2>/dev/null && echo "Backend: healthy" || echo "Backend: not running" +curl -s http://localhost:3001 >/dev/null 2>&1 && echo "Frontend: running" || echo "Frontend: not running" +``` + +## Port Configuration + +| Service | Port | Notes | +|---------|------|-------| +| Frontend | 3001 | Vite dev server | +| Backend | 8001 | FastAPI with uvicorn | +| PostgreSQL | 5433 | Non-standard to avoid conflicts | +| Redis | 6380 | Non-standard to avoid conflicts | +| Adminer | 8090 | Database admin UI | + +## Environment Files + +### Frontend (`frontend/.env.development`) +```bash +VITE_API_BASE_URL=http://localhost:8001 +VITE_WS_URL=http://localhost:8001 +VITE_OAUTH_REDIRECT_URI=http://localhost:3001/auth/callback +``` + +### Backend (`backend/.env`) +```bash +# Database +DATABASE_URL=postgresql+asyncpg://mantimon:mantimon@localhost:5433/mantimon + +# Redis +REDIS_URL=redis://localhost:6380/0 + +# Security +SECRET_KEY=dev-secret-key-change-in-production + +# OAuth (optional for dev) +DISCORD_CLIENT_ID=your-client-id +DISCORD_CLIENT_SECRET=your-client-secret +# GOOGLE_CLIENT_ID=your-client-id +# GOOGLE_CLIENT_SECRET=your-client-secret + +# Base URL for OAuth callbacks +BASE_URL=http://localhost:8001 +``` + +## Troubleshooting + +### Port already in use +```bash +lsof -i :8001 # Find process +kill -9 # Kill it +``` + +### Docker services not starting +```bash +cd backend +docker compose logs # Check what went wrong +docker compose down && docker compose up -d # Restart +``` + +### Database migrations failed +```bash +cd backend && uv run alembic downgrade -1 && uv run alembic upgrade head +``` + +### Clear all and start fresh +```bash +cd backend +docker compose down -v # Remove volumes too +docker compose up -d +uv run alembic upgrade head ``` diff --git a/backend/.env.example b/backend/.env.example index e49f99e..ab407e1 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -68,7 +68,8 @@ JWT_REFRESH_EXPIRE_DAYS=7 # CORS # ============================================================================= # Comma-separated list of allowed origins -# CORS_ORIGINS=http://localhost:3000,http://localhost:5173 +# Default: http://localhost:3000,http://localhost:3001,http://localhost:5173 +# CORS_ORIGINS=http://localhost:3000,http://localhost:3001,http://localhost:5173 # ============================================================================= # GAME SETTINGS diff --git a/backend/app/config.py b/backend/app/config.py index cf62f48..ea6a607 100644 --- a/backend/app/config.py +++ b/backend/app/config.py @@ -150,7 +150,7 @@ class Settings(BaseSettings): # CORS cors_origins: list[str] = Field( - default=["http://localhost:3000", "http://localhost:5173"], + default=["http://localhost:3000", "http://localhost:3001", "http://localhost:5173"], description="Allowed CORS origins", ) diff --git a/frontend/project_plans/PHASE_F1_authentication.json b/frontend/project_plans/PHASE_F1_authentication.json index cafd520..7db7ed7 100644 --- a/frontend/project_plans/PHASE_F1_authentication.json +++ b/frontend/project_plans/PHASE_F1_authentication.json @@ -4,9 +4,9 @@ "name": "Authentication Flow", "version": "1.0.0", "created": "2026-01-30", - "lastUpdated": "2026-01-30", + "lastUpdated": "2026-01-30T15:00:00Z", "totalTasks": 10, - "completedTasks": 2, + "completedTasks": 5, "status": "in_progress", "description": "Complete OAuth authentication flow including login, callback handling, starter deck selection, profile management, and app initialization." }, @@ -95,8 +95,8 @@ "description": "Vue composable for auth operations with loading/error states", "category": "composables", "priority": 3, - "completed": false, - "tested": false, + "completed": true, + "tested": true, "dependencies": ["F1-002"], "files": [ {"path": "src/composables/useAuth.ts", "status": "create"}, @@ -124,8 +124,8 @@ "description": "Initialize auth state on app startup", "category": "setup", "priority": 4, - "completed": false, - "tested": false, + "completed": true, + "tested": true, "dependencies": ["F1-003"], "files": [ {"path": "src/App.vue", "status": "modify"}, @@ -153,8 +153,8 @@ "description": "Complete starter deck selection with API integration", "category": "pages", "priority": 5, - "completed": false, - "tested": false, + "completed": true, + "tested": true, "dependencies": ["F1-003"], "files": [ {"path": "src/pages/StarterSelectionPage.vue", "status": "modify"}, diff --git a/frontend/src/App.spec.ts b/frontend/src/App.spec.ts new file mode 100644 index 0000000..1940de1 --- /dev/null +++ b/frontend/src/App.spec.ts @@ -0,0 +1,234 @@ +/** + * Tests for App.vue root component. + * + * Verifies that auth initialization happens on app startup and that + * the loading state is properly displayed during initialization. + */ +import { describe, it, expect, beforeEach, vi, afterEach } from 'vitest' +import { mount, flushPromises } from '@vue/test-utils' +import { setActivePinia, createPinia } from 'pinia' +import { defineComponent, h, nextTick } from 'vue' + +// Track initialization calls and control resolution +let initializeResolve: ((value: boolean) => void) | null = null +let initializeCalled = false + +// Mock useAuth composable +vi.mock('@/composables/useAuth', () => ({ + useAuth: () => ({ + initialize: vi.fn(() => { + initializeCalled = true + return new Promise((resolve) => { + initializeResolve = resolve + }) + }), + isInitialized: { value: false }, + isAuthenticated: { value: false }, + user: { value: null }, + isLoading: { value: false }, + error: { value: null }, + }), +})) + +// Mock vue-router +vi.mock('vue-router', () => ({ + useRoute: () => ({ + meta: { layout: 'default' }, + name: 'Dashboard', + path: '/', + }), + useRouter: () => ({ + push: vi.fn(), + replace: vi.fn(), + }), +})) + +import App from './App.vue' + +// Create stub components for layouts +const DefaultLayoutStub = defineComponent({ + name: 'DefaultLayout', + setup(_, { slots }) { + return () => h('div', { class: 'default-layout' }, slots.default?.()) + }, +}) + +const MinimalLayoutStub = defineComponent({ + name: 'MinimalLayout', + setup(_, { slots }) { + return () => h('div', { class: 'minimal-layout' }, slots.default?.()) + }, +}) + +const GameLayoutStub = defineComponent({ + name: 'GameLayout', + setup(_, { slots }) { + return () => h('div', { class: 'game-layout' }, slots.default?.()) + }, +}) + +const LoadingOverlayStub = defineComponent({ + name: 'LoadingOverlay', + setup() { + return () => h('div', { class: 'loading-overlay-mock' }) + }, +}) + +const ToastContainerStub = defineComponent({ + name: 'ToastContainer', + setup() { + return () => h('div', { class: 'toast-container-mock' }) + }, +}) + +describe('App.vue', () => { + beforeEach(() => { + setActivePinia(createPinia()) + initializeCalled = false + initializeResolve = null + }) + + afterEach(() => { + vi.clearAllMocks() + }) + + const mountApp = () => { + return mount(App, { + global: { + stubs: { + RouterView: true, + DefaultLayout: DefaultLayoutStub, + MinimalLayout: MinimalLayoutStub, + GameLayout: GameLayoutStub, + LoadingOverlay: LoadingOverlayStub, + ToastContainer: ToastContainerStub, + // Also stub async component wrappers + AsyncComponentWrapper: true, + Suspense: false, + }, + }, + }) + } + + describe('auth initialization', () => { + it('shows loading spinner while auth is initializing', async () => { + /** + * Test loading state during auth initialization. + * + * When the app starts, it should show a loading spinner while + * auth tokens are being validated. This prevents navigation guards + * from running before auth state is known. + */ + const wrapper = mountApp() + + // Should show loading text initially + expect(wrapper.text()).toContain('Loading...') + + wrapper.unmount() + }) + + it('calls initialize on mount', async () => { + /** + * Test that auth initialization is triggered on mount. + * + * The initialize() function must be called to validate tokens, + * refresh if needed, and fetch the user profile. + */ + const wrapper = mountApp() + + // Wait for onMounted to run + await nextTick() + + expect(initializeCalled).toBe(true) + + wrapper.unmount() + }) + + it('hides loading spinner after initialization completes', async () => { + /** + * Test transition from loading to content. + * + * Once auth initialization completes, the loading spinner should + * be hidden and the main app content should be rendered. + */ + const wrapper = mountApp() + + // Wait for onMounted + await nextTick() + + // Should show loading initially + expect(wrapper.text()).toContain('Loading...') + + // Resolve initialization (successful) + expect(initializeResolve).not.toBeNull() + initializeResolve!(true) + + // Wait for state update + await flushPromises() + await nextTick() + + // Loading text should be gone + expect(wrapper.text()).not.toContain('Loading...') + + wrapper.unmount() + }) + }) + + describe('initialization state machine', () => { + it('starts in initializing state and transitions to initialized', async () => { + /** + * Test the auth initialization state machine. + * + * The app should start showing the loading state and transition + * to the main content after initialization completes. + */ + const wrapper = mountApp() + await nextTick() + + // Before initialization completes + expect(wrapper.find('.bg-gray-900').exists()).toBe(true) + + // Complete initialization (successful) + initializeResolve!(true) + await flushPromises() + await nextTick() + + // After initialization - the fixed loading div should be gone + // (checking that the loading screen specific class combo is gone) + const loadingScreen = wrapper.find('.fixed.inset-0.z-50.bg-gray-900') + expect(loadingScreen.exists()).toBe(false) + + wrapper.unmount() + }) + + it('renders app content even when initialization returns false (auth failed)', async () => { + /** + * Test that app renders correctly when auth initialization fails. + * + * When initialize() returns false (e.g., tokens invalid, network error + * during profile fetch), the app should still transition out of the + * loading state and render the main content. Navigation guards will + * handle redirecting unauthenticated users to login. + */ + const wrapper = mountApp() + await nextTick() + + // Should show loading initially + expect(wrapper.text()).toContain('Loading...') + + // Complete initialization with failure (auth failed) + initializeResolve!(false) + await flushPromises() + await nextTick() + + // Loading screen should be gone - app renders even on auth failure + const loadingScreen = wrapper.find('.fixed.inset-0.z-50.bg-gray-900') + expect(loadingScreen.exists()).toBe(false) + + // Loading text should be gone + expect(wrapper.text()).not.toContain('Loading...') + + wrapper.unmount() + }) + }) +}) diff --git a/frontend/src/App.vue b/frontend/src/App.vue index f1d3414..27f423c 100644 --- a/frontend/src/App.vue +++ b/frontend/src/App.vue @@ -2,14 +2,21 @@ /** * Root application component. * - * Renders the appropriate layout based on the current route's meta.layout - * property. Also includes global UI components (loading overlay, toasts). + * Handles auth initialization on startup and renders the appropriate layout + * based on the current route's meta.layout property. + * + * Auth initialization: + * - Shows loading spinner while validating tokens + * - Refreshes expired tokens automatically + * - Fetches user profile if authenticated + * - Blocks navigation until initialization completes */ -import { computed, defineAsyncComponent } from 'vue' +import { computed, defineAsyncComponent, onMounted, ref } from 'vue' import { useRoute } from 'vue-router' import LoadingOverlay from '@/components/ui/LoadingOverlay.vue' import ToastContainer from '@/components/ui/ToastContainer.vue' +import { useAuth } from '@/composables/useAuth' // Lazy-load layouts to reduce initial bundle size const DefaultLayout = defineAsyncComponent(() => import('@/layouts/DefaultLayout.vue')) @@ -17,6 +24,10 @@ const MinimalLayout = defineAsyncComponent(() => import('@/layouts/MinimalLayout const GameLayout = defineAsyncComponent(() => import('@/layouts/GameLayout.vue')) const route = useRoute() +const { initialize } = useAuth() + +// Track if auth initialization is in progress +const isAuthInitializing = ref(true) type LayoutType = 'default' | 'minimal' | 'game' @@ -30,12 +41,43 @@ const currentLayout = computed(() => { const layout = (route.meta.layout as LayoutType) || 'default' return layoutComponents[layout] || layoutComponents.default }) + +// Initialize auth on app mount +onMounted(async () => { + try { + await initialize() + } finally { + isAuthInitializing.value = false + } +})