Add comprehensive project documentation and Docker infrastructure for Paper Dynasty Real-Time Game Engine - a web-based multiplayer baseball simulation platform replacing the legacy Google Sheets system. Documentation Added: - Complete PRD (Product Requirements Document) - Project README with dual development workflows - Implementation guide with 5-phase roadmap - Architecture docs (backend, frontend, database, WebSocket) - CLAUDE.md context files for each major directory Infrastructure Added: - Root docker-compose.yml for full stack orchestration - Dockerfiles for backend and both frontends (multi-stage builds) - .dockerignore files for optimal build context - .env.example with all required configuration - Updated .gitignore for Python, Node, Nuxt, and Docker Project Structure: - backend/ - FastAPI + Socket.io game engine (Python 3.11+) - frontend-sba/ - SBA League Nuxt 3 frontend - frontend-pd/ - PD League Nuxt 3 frontend - .claude/implementation/ - Detailed implementation guides Supports two development workflows: 1. Local dev (recommended): Services run natively with hot-reload 2. Full Docker: One-command stack orchestration for testing/demos Next: Phase 1 implementation (backend/frontend foundations)
848 lines
22 KiB
Markdown
848 lines
22 KiB
Markdown
# Phase 1: Core Infrastructure Setup
|
|
|
|
**Duration**: Weeks 1-3
|
|
**Goal**: Establish foundation for both backend and frontend with authentication and real-time communication
|
|
|
|
## Objectives
|
|
|
|
By end of Phase 1, you should have:
|
|
- ✅ FastAPI backend running with PostgreSQL
|
|
- ✅ Socket.io WebSocket server functional
|
|
- ✅ Discord OAuth working for both leagues
|
|
- ✅ Basic Nuxt 3 frontends operational
|
|
- ✅ WebSocket connections established between frontend/backend
|
|
- ✅ Users can authenticate and see connection status
|
|
|
|
## Development Workflow Options
|
|
|
|
This project supports **two development workflows**:
|
|
|
|
### Option 1: Local Development (Recommended for Daily Work)
|
|
- Backend runs locally with Python hot-reload
|
|
- Frontends run locally with Nuxt hot-reload
|
|
- Only Redis runs in Docker
|
|
- **Advantages**: Fast, easy debugging, instant code changes
|
|
|
|
### Option 2: Full Docker Orchestration
|
|
- Everything runs in containers
|
|
- One-command startup with `docker-compose up`
|
|
- **Advantages**: Production-like environment, easy sharing
|
|
|
|
**See the project [README.md](../../../README.md) for detailed instructions on both workflows.**
|
|
|
|
For this guide, we'll focus on **local development** setup. Docker configuration files are already provided:
|
|
- Root `docker-compose.yml` - Full stack orchestration
|
|
- `backend/Dockerfile` - Backend container
|
|
- `frontend-sba/Dockerfile` & `frontend-pd/Dockerfile` - Frontend containers
|
|
|
|
---
|
|
|
|
## Backend Setup
|
|
|
|
### 1. Initialize FastAPI Project
|
|
|
|
```bash
|
|
# Create backend directory
|
|
mkdir -p backend/app/{core,config,models,websocket,api,database,data,utils}
|
|
mkdir -p backend/tests/{unit,integration,e2e}
|
|
cd backend
|
|
|
|
# Create virtual environment
|
|
python3.11 -m venv venv
|
|
source venv/bin/activate
|
|
|
|
# Create requirements.txt
|
|
cat > requirements.txt << EOF
|
|
fastapi==0.104.1
|
|
uvicorn[standard]==0.24.0
|
|
python-socketio==5.10.0
|
|
python-multipart==0.0.6
|
|
pydantic==2.5.0
|
|
pydantic-settings==2.1.0
|
|
sqlalchemy==2.0.23
|
|
alembic==1.12.1
|
|
asyncpg==0.29.0
|
|
psycopg2-binary==2.9.9
|
|
python-jose[cryptography]==3.3.0
|
|
passlib[bcrypt]==1.7.4
|
|
python-dotenv==1.0.0
|
|
httpx==0.25.1
|
|
redis==5.0.1
|
|
aiofiles==23.2.1
|
|
EOF
|
|
|
|
# Development dependencies
|
|
cat > requirements-dev.txt << EOF
|
|
-r requirements.txt
|
|
pytest==7.4.3
|
|
pytest-asyncio==0.21.1
|
|
pytest-cov==4.1.0
|
|
black==23.11.0
|
|
flake8==6.1.0
|
|
mypy==1.7.1
|
|
httpx==0.25.1
|
|
EOF
|
|
|
|
# Install dependencies
|
|
pip install -r requirements-dev.txt
|
|
```
|
|
|
|
### 2. Setup FastAPI Application
|
|
|
|
**File**: `backend/app/main.py`
|
|
```python
|
|
import logging
|
|
from contextlib import asynccontextmanager
|
|
from fastapi import FastAPI
|
|
from fastapi.middleware.cors import CORSMiddleware
|
|
import socketio
|
|
|
|
from app.config import get_settings
|
|
from app.api.routes import games, auth, health
|
|
from app.websocket.connection_manager import ConnectionManager
|
|
from app.websocket.handlers import register_handlers
|
|
from app.database.session import init_db
|
|
from app.utils.logging import setup_logging
|
|
|
|
logger = logging.getLogger(f'{__name__}.main')
|
|
|
|
|
|
@asynccontextmanager
|
|
async def lifespan(app: FastAPI):
|
|
"""Startup and shutdown events"""
|
|
# Startup
|
|
logger.info("Starting Paper Dynasty Game Backend")
|
|
setup_logging()
|
|
await init_db()
|
|
logger.info("Database initialized")
|
|
yield
|
|
# Shutdown
|
|
logger.info("Shutting down Paper Dynasty Game Backend")
|
|
|
|
|
|
# Initialize FastAPI app
|
|
app = FastAPI(
|
|
title="Paper Dynasty Game Backend",
|
|
description="Real-time baseball game engine for Paper Dynasty leagues",
|
|
version="1.0.0",
|
|
lifespan=lifespan
|
|
)
|
|
|
|
# CORS middleware
|
|
settings = get_settings()
|
|
app.add_middleware(
|
|
CORSMiddleware,
|
|
allow_origins=settings.cors_origins,
|
|
allow_credentials=True,
|
|
allow_methods=["*"],
|
|
allow_headers=["*"],
|
|
)
|
|
|
|
# Initialize Socket.io
|
|
sio = socketio.AsyncServer(
|
|
async_mode='asgi',
|
|
cors_allowed_origins=settings.cors_origins,
|
|
logger=True,
|
|
engineio_logger=False
|
|
)
|
|
|
|
# Create Socket.io ASGI app
|
|
socket_app = socketio.ASGIApp(sio, app)
|
|
|
|
# Initialize connection manager and register handlers
|
|
connection_manager = ConnectionManager(sio)
|
|
register_handlers(sio, connection_manager)
|
|
|
|
# Include API routes
|
|
app.include_router(health.router, prefix="/api", tags=["health"])
|
|
app.include_router(auth.router, prefix="/api/auth", tags=["auth"])
|
|
app.include_router(games.router, prefix="/api/games", tags=["games"])
|
|
|
|
|
|
@app.get("/")
|
|
async def root():
|
|
return {"message": "Paper Dynasty Game Backend", "version": "1.0.0"}
|
|
|
|
|
|
if __name__ == "__main__":
|
|
import uvicorn
|
|
uvicorn.run(
|
|
"app.main:socket_app",
|
|
host="0.0.0.0",
|
|
port=8000,
|
|
reload=True,
|
|
log_level="info"
|
|
)
|
|
```
|
|
|
|
**File**: `backend/app/config.py`
|
|
```python
|
|
from functools import lru_cache
|
|
from pydantic_settings import BaseSettings
|
|
|
|
|
|
class Settings(BaseSettings):
|
|
"""Application settings"""
|
|
|
|
# Application
|
|
app_env: str = "development"
|
|
debug: bool = True
|
|
secret_key: str
|
|
|
|
# Database
|
|
database_url: str
|
|
db_pool_size: int = 20
|
|
db_max_overflow: int = 10
|
|
|
|
# Discord OAuth
|
|
discord_client_id: str
|
|
discord_client_secret: str
|
|
discord_redirect_uri: str
|
|
|
|
# League APIs
|
|
sba_api_url: str
|
|
sba_api_key: str
|
|
pd_api_url: str
|
|
pd_api_key: str
|
|
|
|
# WebSocket
|
|
ws_heartbeat_interval: int = 30
|
|
ws_connection_timeout: int = 60
|
|
|
|
# CORS
|
|
cors_origins: list[str] = ["http://localhost:3000", "http://localhost:3001"]
|
|
|
|
# Game settings
|
|
max_concurrent_games: int = 20
|
|
game_idle_timeout: int = 86400 # 24 hours
|
|
|
|
class Config:
|
|
env_file = ".env"
|
|
case_sensitive = False
|
|
|
|
|
|
@lru_cache()
|
|
def get_settings() -> Settings:
|
|
"""Get cached settings instance"""
|
|
return Settings()
|
|
```
|
|
|
|
**File**: `backend/.env.example`
|
|
```bash
|
|
# Application
|
|
APP_ENV=development
|
|
DEBUG=true
|
|
SECRET_KEY=your-secret-key-change-in-production
|
|
|
|
# Database
|
|
# Update with your actual database server hostname/IP and credentials
|
|
DATABASE_URL=postgresql+asyncpg://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
|
|
|
|
# Discord OAuth
|
|
DISCORD_CLIENT_ID=your-discord-client-id
|
|
DISCORD_CLIENT_SECRET=your-discord-client-secret
|
|
DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
|
|
|
|
# League APIs
|
|
SBA_API_URL=https://sba-api.example.com
|
|
SBA_API_KEY=your-sba-api-key
|
|
PD_API_URL=https://pd-api.example.com
|
|
PD_API_KEY=your-pd-api-key
|
|
|
|
# CORS
|
|
CORS_ORIGINS=http://localhost:3000,http://localhost:3001
|
|
|
|
# Redis (optional - for caching)
|
|
# REDIS_URL=redis://localhost:6379
|
|
```
|
|
|
|
### 3. Database Setup
|
|
|
|
**File**: `backend/app/database/session.py`
|
|
```python
|
|
import logging
|
|
from typing import AsyncGenerator
|
|
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession, async_sessionmaker
|
|
from sqlalchemy.orm import declarative_base
|
|
|
|
from app.config import get_settings
|
|
|
|
logger = logging.getLogger(f'{__name__}.session')
|
|
|
|
settings = get_settings()
|
|
|
|
# Create async engine
|
|
engine = create_async_engine(
|
|
settings.database_url,
|
|
echo=settings.debug,
|
|
pool_size=settings.db_pool_size,
|
|
max_overflow=settings.db_max_overflow,
|
|
)
|
|
|
|
# Create session factory
|
|
AsyncSessionLocal = async_sessionmaker(
|
|
engine,
|
|
class_=AsyncSession,
|
|
expire_on_commit=False,
|
|
autocommit=False,
|
|
autoflush=False,
|
|
)
|
|
|
|
# Base class for models
|
|
Base = declarative_base()
|
|
|
|
|
|
async def init_db() -> None:
|
|
"""Initialize database tables"""
|
|
async with engine.begin() as conn:
|
|
# Import all models here to ensure they're registered
|
|
from app.models import db_models
|
|
|
|
# Create tables
|
|
await conn.run_sync(Base.metadata.create_all)
|
|
logger.info("Database tables created")
|
|
|
|
|
|
async def get_session() -> AsyncGenerator[AsyncSession, None]:
|
|
"""Dependency for getting database session"""
|
|
async with AsyncSessionLocal() as session:
|
|
try:
|
|
yield session
|
|
await session.commit()
|
|
except Exception:
|
|
await session.rollback()
|
|
raise
|
|
finally:
|
|
await session.close()
|
|
```
|
|
|
|
**File**: `backend/app/models/db_models.py`
|
|
```python
|
|
from datetime import datetime
|
|
from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON, Text, ForeignKey
|
|
from sqlalchemy.dialects.postgresql import UUID
|
|
import uuid
|
|
|
|
from app.database.session import Base
|
|
|
|
|
|
class Game(Base):
|
|
"""Game model"""
|
|
__tablename__ = "games"
|
|
|
|
id = Column(UUID(as_uuid=True), primary_key=True, default=uuid.uuid4)
|
|
league_id = Column(String(50), nullable=False, index=True)
|
|
home_team_id = Column(Integer, nullable=False)
|
|
away_team_id = Column(Integer, nullable=False)
|
|
status = Column(String(20), nullable=False, default="pending", index=True)
|
|
game_mode = Column(String(20), nullable=False)
|
|
visibility = Column(String(20), nullable=False)
|
|
current_inning = Column(Integer)
|
|
current_half = Column(String(10))
|
|
home_score = Column(Integer, default=0)
|
|
away_score = Column(Integer, default=0)
|
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
started_at = Column(DateTime)
|
|
completed_at = Column(DateTime)
|
|
winner_team_id = Column(Integer)
|
|
metadata = Column(JSON, default=dict)
|
|
|
|
|
|
class Play(Base):
|
|
"""Play model"""
|
|
__tablename__ = "plays"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), nullable=False, index=True)
|
|
play_number = Column(Integer, nullable=False)
|
|
inning = Column(Integer, nullable=False)
|
|
half = Column(String(10), nullable=False)
|
|
outs_before = Column(Integer, nullable=False)
|
|
outs_recorded = Column(Integer, nullable=False)
|
|
batter_id = Column(Integer, nullable=False)
|
|
pitcher_id = Column(Integer, nullable=False)
|
|
runners_before = Column(JSON)
|
|
runners_after = Column(JSON)
|
|
balls = Column(Integer)
|
|
strikes = Column(Integer)
|
|
defensive_positioning = Column(String(50))
|
|
offensive_approach = Column(String(50))
|
|
dice_roll = Column(Integer)
|
|
hit_type = Column(String(50))
|
|
result_description = Column(Text)
|
|
runs_scored = Column(Integer, default=0)
|
|
created_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
metadata = Column(JSON, default=dict)
|
|
|
|
|
|
class Lineup(Base):
|
|
"""Lineup model"""
|
|
__tablename__ = "lineups"
|
|
|
|
id = Column(Integer, primary_key=True, autoincrement=True)
|
|
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), nullable=False, index=True)
|
|
team_id = Column(Integer, nullable=False, index=True)
|
|
card_id = Column(Integer, nullable=False)
|
|
position = Column(String(10), nullable=False)
|
|
batting_order = Column(Integer)
|
|
is_starter = Column(Boolean, default=True)
|
|
is_active = Column(Boolean, default=True, index=True)
|
|
entered_inning = Column(Integer, default=1)
|
|
metadata = Column(JSON, default=dict)
|
|
|
|
|
|
class GameSession(Base):
|
|
"""Game session tracking"""
|
|
__tablename__ = "game_sessions"
|
|
|
|
game_id = Column(UUID(as_uuid=True), ForeignKey("games.id"), primary_key=True)
|
|
connected_users = Column(JSON, default=dict)
|
|
last_action_at = Column(DateTime, default=datetime.utcnow, index=True)
|
|
state_snapshot = Column(JSON, default=dict)
|
|
```
|
|
|
|
### 4. WebSocket Connection Manager
|
|
|
|
**File**: `backend/app/websocket/connection_manager.py`
|
|
```python
|
|
import logging
|
|
from typing import Dict, Set
|
|
import socketio
|
|
|
|
logger = logging.getLogger(f'{__name__}.ConnectionManager')
|
|
|
|
|
|
class ConnectionManager:
|
|
"""Manages WebSocket connections and rooms"""
|
|
|
|
def __init__(self, sio: socketio.AsyncServer):
|
|
self.sio = sio
|
|
self.user_sessions: Dict[str, str] = {} # sid -> user_id
|
|
self.game_rooms: Dict[str, Set[str]] = {} # game_id -> set of sids
|
|
|
|
async def connect(self, sid: str, user_id: str) -> None:
|
|
"""Register a new connection"""
|
|
self.user_sessions[sid] = user_id
|
|
logger.info(f"User {user_id} connected with session {sid}")
|
|
|
|
async def disconnect(self, sid: str) -> None:
|
|
"""Handle disconnection"""
|
|
user_id = self.user_sessions.pop(sid, None)
|
|
if user_id:
|
|
logger.info(f"User {user_id} disconnected (session {sid})")
|
|
|
|
# Remove from all game rooms
|
|
for game_id, sids in self.game_rooms.items():
|
|
if sid in sids:
|
|
sids.remove(sid)
|
|
await self.broadcast_to_game(
|
|
game_id,
|
|
"user_disconnected",
|
|
{"user_id": user_id}
|
|
)
|
|
|
|
async def join_game(self, sid: str, game_id: str, role: str) -> None:
|
|
"""Add user to game room"""
|
|
await self.sio.enter_room(sid, game_id)
|
|
|
|
if game_id not in self.game_rooms:
|
|
self.game_rooms[game_id] = set()
|
|
self.game_rooms[game_id].add(sid)
|
|
|
|
user_id = self.user_sessions.get(sid)
|
|
logger.info(f"User {user_id} joined game {game_id} as {role}")
|
|
|
|
await self.broadcast_to_game(
|
|
game_id,
|
|
"user_connected",
|
|
{"user_id": user_id, "role": role}
|
|
)
|
|
|
|
async def leave_game(self, sid: str, game_id: str) -> None:
|
|
"""Remove user from game room"""
|
|
await self.sio.leave_room(sid, game_id)
|
|
|
|
if game_id in self.game_rooms:
|
|
self.game_rooms[game_id].discard(sid)
|
|
|
|
user_id = self.user_sessions.get(sid)
|
|
logger.info(f"User {user_id} left game {game_id}")
|
|
|
|
async def broadcast_to_game(
|
|
self,
|
|
game_id: str,
|
|
event: str,
|
|
data: dict
|
|
) -> None:
|
|
"""Broadcast event to all users in game room"""
|
|
await self.sio.emit(event, data, room=game_id)
|
|
logger.debug(f"Broadcast {event} to game {game_id}")
|
|
|
|
async def emit_to_user(self, sid: str, event: str, data: dict) -> None:
|
|
"""Emit event to specific user"""
|
|
await self.sio.emit(event, data, room=sid)
|
|
|
|
def get_game_participants(self, game_id: str) -> Set[str]:
|
|
"""Get all session IDs in game room"""
|
|
return self.game_rooms.get(game_id, set())
|
|
```
|
|
|
|
**File**: `backend/app/websocket/handlers.py`
|
|
```python
|
|
import logging
|
|
from socketio import AsyncServer
|
|
|
|
from app.websocket.connection_manager import ConnectionManager
|
|
from app.utils.auth import verify_token
|
|
|
|
logger = logging.getLogger(f'{__name__}.handlers')
|
|
|
|
|
|
def register_handlers(sio: AsyncServer, manager: ConnectionManager) -> None:
|
|
"""Register all WebSocket event handlers"""
|
|
|
|
@sio.event
|
|
async def connect(sid, environ, auth):
|
|
"""Handle new connection"""
|
|
try:
|
|
# Verify JWT token
|
|
token = auth.get("token")
|
|
if not token:
|
|
logger.warning(f"Connection {sid} rejected: no token")
|
|
return False
|
|
|
|
user_data = verify_token(token)
|
|
user_id = user_data.get("user_id")
|
|
|
|
if not user_id:
|
|
logger.warning(f"Connection {sid} rejected: invalid token")
|
|
return False
|
|
|
|
await manager.connect(sid, user_id)
|
|
await sio.emit("connected", {"user_id": user_id}, room=sid)
|
|
|
|
logger.info(f"Connection {sid} accepted for user {user_id}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Connection error: {e}")
|
|
return False
|
|
|
|
@sio.event
|
|
async def disconnect(sid):
|
|
"""Handle disconnection"""
|
|
await manager.disconnect(sid)
|
|
|
|
@sio.event
|
|
async def join_game(sid, data):
|
|
"""Handle join game request"""
|
|
try:
|
|
game_id = data.get("game_id")
|
|
role = data.get("role", "player")
|
|
|
|
if not game_id:
|
|
await manager.emit_to_user(
|
|
sid,
|
|
"error",
|
|
{"message": "Missing game_id"}
|
|
)
|
|
return
|
|
|
|
# TODO: Verify user has access to game
|
|
|
|
await manager.join_game(sid, game_id, role)
|
|
await manager.emit_to_user(
|
|
sid,
|
|
"game_joined",
|
|
{"game_id": game_id, "role": role}
|
|
)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Join game error: {e}")
|
|
await manager.emit_to_user(
|
|
sid,
|
|
"error",
|
|
{"message": str(e)}
|
|
)
|
|
|
|
@sio.event
|
|
async def leave_game(sid, data):
|
|
"""Handle leave game request"""
|
|
try:
|
|
game_id = data.get("game_id")
|
|
if game_id:
|
|
await manager.leave_game(sid, game_id)
|
|
|
|
except Exception as e:
|
|
logger.error(f"Leave game error: {e}")
|
|
|
|
@sio.event
|
|
async def heartbeat(sid):
|
|
"""Handle heartbeat ping"""
|
|
await sio.emit("heartbeat_ack", {}, room=sid)
|
|
```
|
|
|
|
### 5. Logging Configuration
|
|
|
|
**File**: `backend/app/utils/logging.py`
|
|
```python
|
|
import logging
|
|
import logging.handlers
|
|
import os
|
|
from datetime import datetime
|
|
|
|
|
|
def setup_logging() -> None:
|
|
"""Configure application logging"""
|
|
|
|
# Create logs directory
|
|
log_dir = "logs"
|
|
os.makedirs(log_dir, exist_ok=True)
|
|
|
|
# Log file name with date
|
|
log_file = os.path.join(log_dir, f"app_{datetime.now().strftime('%Y%m%d')}.log")
|
|
|
|
# Create formatter
|
|
formatter = logging.Formatter(
|
|
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
|
datefmt='%Y-%m-%d %H:%M:%S'
|
|
)
|
|
|
|
# Console handler
|
|
console_handler = logging.StreamHandler()
|
|
console_handler.setLevel(logging.INFO)
|
|
console_handler.setFormatter(formatter)
|
|
|
|
# Rotating file handler
|
|
file_handler = logging.handlers.RotatingFileHandler(
|
|
log_file,
|
|
maxBytes=10 * 1024 * 1024, # 10MB
|
|
backupCount=5
|
|
)
|
|
file_handler.setLevel(logging.DEBUG)
|
|
file_handler.setFormatter(formatter)
|
|
|
|
# Configure root logger
|
|
root_logger = logging.getLogger()
|
|
root_logger.setLevel(logging.DEBUG)
|
|
root_logger.addHandler(console_handler)
|
|
root_logger.addHandler(file_handler)
|
|
|
|
# Silence noisy loggers
|
|
logging.getLogger("sqlalchemy.engine").setLevel(logging.WARNING)
|
|
logging.getLogger("socketio").setLevel(logging.INFO)
|
|
logging.getLogger("engineio").setLevel(logging.WARNING)
|
|
```
|
|
|
|
### 6. Database Setup
|
|
|
|
**Note**: This project uses existing PostgreSQL servers (dev and prod). You'll need to manually create the database before running the backend.
|
|
|
|
**Create Database on Your PostgreSQL Server**:
|
|
```sql
|
|
-- On your dev PostgreSQL server
|
|
CREATE DATABASE paperdynasty_dev;
|
|
CREATE USER paperdynasty WITH PASSWORD 'your-secure-password';
|
|
GRANT ALL PRIVILEGES ON DATABASE paperdynasty_dev TO paperdynasty;
|
|
|
|
-- Later, on your prod PostgreSQL server
|
|
CREATE DATABASE paperdynasty_prod;
|
|
-- Use a different, secure password for production
|
|
```
|
|
|
|
**Test Connection**:
|
|
```bash
|
|
# Test that you can connect to the database
|
|
psql postgresql://paperdynasty:your-password@your-db-server:5432/paperdynasty_dev
|
|
```
|
|
|
|
### 7. Docker Compose for Redis (Development)
|
|
|
|
**File**: `backend/docker-compose.yml`
|
|
```yaml
|
|
version: '3.8'
|
|
|
|
services:
|
|
redis:
|
|
image: redis:7-alpine
|
|
ports:
|
|
- "6379:6379"
|
|
volumes:
|
|
- redis_data:/data
|
|
restart: unless-stopped
|
|
|
|
volumes:
|
|
redis_data:
|
|
```
|
|
|
|
**Start Redis**:
|
|
```bash
|
|
cd backend
|
|
docker-compose up -d
|
|
```
|
|
|
|
## Frontend Setup
|
|
|
|
### 1. Initialize Nuxt 3 Projects
|
|
|
|
```bash
|
|
# Create SBA League frontend
|
|
npx nuxi@latest init frontend-sba
|
|
cd frontend-sba
|
|
|
|
# Install dependencies
|
|
npm install @nuxtjs/tailwindcss @pinia/nuxt socket.io-client axios
|
|
npm install -D @nuxtjs/eslint-config-typescript
|
|
|
|
# Initialize Tailwind
|
|
npx tailwindcss init
|
|
|
|
cd ..
|
|
|
|
# Repeat for PD League
|
|
npx nuxi@latest init frontend-pd
|
|
cd frontend-pd
|
|
npm install @nuxtjs/tailwindcss @pinia/nuxt socket.io-client axios
|
|
npm install -D @nuxtjs/eslint-config-typescript
|
|
npx tailwindcss init
|
|
```
|
|
|
|
### 2. Configure Nuxt
|
|
|
|
**File**: `frontend-sba/nuxt.config.ts`
|
|
```typescript
|
|
export default defineNuxtConfig({
|
|
modules: ['@nuxtjs/tailwindcss', '@pinia/nuxt'],
|
|
|
|
runtimeConfig: {
|
|
public: {
|
|
leagueId: 'sba',
|
|
leagueName: 'Super Baseball Alliance',
|
|
apiUrl: process.env.NUXT_PUBLIC_API_URL || 'http://localhost:8000',
|
|
wsUrl: process.env.NUXT_PUBLIC_WS_URL || 'http://localhost:8000',
|
|
discordClientId: process.env.NUXT_PUBLIC_DISCORD_CLIENT_ID || '',
|
|
discordRedirectUri: process.env.NUXT_PUBLIC_DISCORD_REDIRECT_URI || 'http://localhost:3000/auth/callback',
|
|
}
|
|
},
|
|
|
|
css: ['~/assets/css/tailwind.css'],
|
|
|
|
devtools: { enabled: true },
|
|
|
|
typescript: {
|
|
strict: true,
|
|
typeCheck: true
|
|
}
|
|
})
|
|
```
|
|
|
|
### 3. Setup WebSocket Plugin
|
|
|
|
**File**: `frontend-sba/plugins/socket.client.ts`
|
|
```typescript
|
|
import { io, Socket } from 'socket.io-client'
|
|
|
|
export default defineNuxtPlugin((nuxtApp) => {
|
|
const config = useRuntimeConfig()
|
|
let socket: Socket | null = null
|
|
|
|
const connect = (token: string) => {
|
|
if (socket?.connected) return socket
|
|
|
|
socket = io(config.public.wsUrl, {
|
|
auth: { token },
|
|
reconnection: true,
|
|
reconnectionDelay: 1000,
|
|
reconnectionAttempts: 5
|
|
})
|
|
|
|
socket.on('connect', () => {
|
|
console.log('WebSocket connected')
|
|
})
|
|
|
|
socket.on('disconnect', () => {
|
|
console.log('WebSocket disconnected')
|
|
})
|
|
|
|
socket.on('connect_error', (error) => {
|
|
console.error('WebSocket connection error:', error)
|
|
})
|
|
|
|
return socket
|
|
}
|
|
|
|
const disconnect = () => {
|
|
socket?.disconnect()
|
|
socket = null
|
|
}
|
|
|
|
return {
|
|
provide: {
|
|
socket: {
|
|
connect,
|
|
disconnect,
|
|
get instance() {
|
|
return socket
|
|
}
|
|
}
|
|
}
|
|
}
|
|
})
|
|
```
|
|
|
|
## Testing Phase 1
|
|
|
|
### Backend Tests
|
|
|
|
```bash
|
|
# Run backend tests
|
|
cd backend
|
|
pytest tests/ -v
|
|
|
|
# Test WebSocket connection
|
|
python -m pytest tests/integration/test_websocket.py -v
|
|
|
|
# Check code quality
|
|
black app/ tests/
|
|
flake8 app/ tests/
|
|
mypy app/
|
|
```
|
|
|
|
### Frontend Tests
|
|
|
|
```bash
|
|
# Run frontend dev server
|
|
cd frontend-sba
|
|
npm run dev
|
|
|
|
# Test in browser
|
|
# Navigate to http://localhost:3000
|
|
# Check console for WebSocket connection
|
|
```
|
|
|
|
## Phase 1 Completion Checklist
|
|
|
|
- [ ] PostgreSQL database created on existing server
|
|
- [ ] Database connection tested successfully
|
|
- [ ] FastAPI server running on port 8000
|
|
- [ ] Socket.io WebSocket server operational
|
|
- [ ] Database tables created successfully
|
|
- [ ] Logging system working (check logs/ directory)
|
|
- [ ] Redis running via Docker Compose
|
|
- [ ] WebSocket connections established
|
|
- [ ] Nuxt 3 apps running (SBA on 3000, PD on 3001)
|
|
- [ ] CORS configured correctly
|
|
- [ ] Health check endpoint responding
|
|
- [ ] Basic error handling in place
|
|
|
|
## Next Steps
|
|
|
|
Once Phase 1 is complete, proceed to [Phase 2: Game Engine Core](./02-game-engine.md).
|
|
|
|
---
|
|
|
|
**Notes**:
|
|
- Keep `.env` files secure and never commit to git
|
|
- Test WebSocket connections thoroughly before proceeding
|
|
- Document any deviations from this setup
|
|
- Update CLAUDE.md in subdirectories as needed |