Native Development Workflow with Network Testing Support #1

Merged
cal merged 2 commits from feature/native-dev-workflow into main 2026-02-07 00:16:06 +00:00
7 changed files with 713 additions and 13 deletions

4
.gitignore vendored
View File

@ -201,3 +201,7 @@ redis_data/
.env.local
.env.*.local
.env.production
# Native dev mode artifacts
.pids/
.logs/

View File

@ -88,14 +88,31 @@ strat-gameplay-webapp/
└── scripts/env-switch.sh # Environment profile switcher
```
## Quick Start (All-Docker Workflow)
## Quick Start
The entire stack runs in Docker with a single command. No local Python or Node.js required.
### 🚀 Recommended: Native Development (Fast)
> **⚠️ ALWAYS USE PROD MODE**: Discord OAuth does not work in dev mode due to cookie/CORS configuration. Since this system isn't live yet, always build and run with `prod` mode for testing.
For day-to-day development, use native mode for instant startup and hot-reload:
```bash
# Production (optimized build) - USE THIS
# Start everything natively (5-10 seconds, hot-reload enabled)
./dev-native.sh start
# View logs
./dev-native.sh logs
# Stop when done
./dev-native.sh stop
```
**See [NATIVE_DEV_SETUP.md](NATIVE_DEV_SETUP.md) for full setup instructions.**
### 🐳 Alternative: Docker Workflow
For testing production builds or Docker-specific scenarios:
```bash
# Production (optimized build)
./start.sh prod
# Development - DO NOT USE (auth broken)

255
NATIVE_DEV_SETUP.md Normal file
View File

@ -0,0 +1,255 @@
# Native Development Setup
Fast development workflow with instant startup, hot-reload, and no Docker rebuilds.
## Why Native Development?
**Problem**: Docker production builds take 3-5 minutes to rebuild on every change.
**Solution**: Run backend and frontend natively on your machine, use Docker only for Redis.
**Benefits**:
- ⚡ **Instant startup** (seconds, not minutes)
- 🔥 **Hot-reload** on file changes (backend + frontend)
- 🐛 **Native debugging** (attach debugger directly)
- 💾 **No rebuild overhead** (just save and refresh)
## Prerequisites
### 1. Install Dependencies
```bash
# UV (Python package manager)
curl -LsSf https://astral.sh/uv/install.sh | sh
# Node.js 22+ (if not already installed)
# https://nodejs.org/
# Docker (for Redis only)
# Already installed if you've been using ./start.sh
```
### 2. Configure Discord OAuth (One-Time Setup)
You need to add `http://localhost:8000` as an allowed redirect URI in your Discord application.
**Steps**:
1. Go to [Discord Developer Portal](https://discord.com/developers/applications)
2. Select your application (ID: `1441192438055178420`)
3. Navigate to **OAuth2** → **General**
4. Under **Redirects**, click **Add Redirect**
5. Add: `http://localhost:8000/api/auth/discord/callback/server`
6. Click **Save Changes**
**Your redirect URIs should now include**:
- ✅ `http://localhost:8000/api/auth/discord/callback/server` (dev)
- ✅ `https://gameplay-demo.manticorum.com/api/auth/discord/callback/server` (prod)
## Quick Start
### Option 1: New Native Dev Script (Recommended)
```bash
# Start everything (instant, hot-reload)
./dev-native.sh start
# View logs
./dev-native.sh logs
# Stop everything
./dev-native.sh stop
# Restart everything
./dev-native.sh restart
```
**What it does**:
1. Starts Redis in Docker (seconds)
2. Starts backend natively with `uv run uvicorn --reload` (instant)
3. Starts frontend natively with `npm run dev` (instant)
**No rebuilds. Ever.**
### Option 2: Manual Setup (Full Control)
If you prefer to run each service in separate terminals:
```bash
# Terminal 1: Redis (Docker)
docker compose up redis
# Terminal 2: Backend (native)
cd backend
uv run uvicorn app.main:socket_app --host 0.0.0.0 --port 8000 --reload
# Terminal 3: Frontend (native)
cd frontend-sba
npm run dev
```
## What Changed?
### 1. Cookie Security Flags Now Respect `APP_ENV`
**Before**:
- Cookies always used `Secure=true` + `SameSite=none`
- Required HTTPS (prod URLs only)
- Broke on localhost
**After** (in `backend/app/utils/cookies.py`):
- `APP_ENV=development``Secure=false` + `SameSite=lax` (works on localhost)
- `APP_ENV=production``Secure=true` + `SameSite=none` (works cross-domain)
### 2. Environment Files Already Configured
The `.env.dev` files already have localhost URLs:
**Backend** (`backend/.env.dev`):
```bash
DISCORD_SERVER_REDIRECT_URI=http://localhost:8000/api/auth/discord/callback/server
FRONTEND_URL=http://localhost:3000
CORS_ORIGINS=["http://localhost:3000","http://localhost:3001"]
```
**Frontend** (`frontend-sba/.env.dev`):
```bash
NUXT_PUBLIC_API_URL=http://localhost:8000
NUXT_PUBLIC_WS_URL=http://localhost:8000
NUXT_PUBLIC_DISCORD_REDIRECT_URI=http://localhost:3000/auth/callback
```
The `dev-native.sh` script automatically copies these to `.env` when you run it.
## Usage Patterns
### Typical Development Flow
```bash
# Start once
./dev-native.sh start
# Make changes to backend/frontend code
# Changes auto-reload instantly!
# View logs if needed
./dev-native.sh logs
# Stop when done
./dev-native.sh stop
```
### Debugging
**Backend** (Python):
```bash
# Stop the background process
./dev-native.sh stop
# Run backend in foreground with debugger
cd backend
uv run python -m debugpy --listen 5678 -m uvicorn app.main:socket_app --host 0.0.0.0 --port 8000 --reload
# Attach your IDE's debugger to localhost:5678
```
**Frontend** (Vue):
```bash
# Browser DevTools work automatically with HMR
# Or use Vue DevTools browser extension
```
### Switching Between Native and Docker
**To Native**:
```bash
# Stop Docker services
./start.sh stop
# Start native
./dev-native.sh start
```
**To Docker** (for testing prod builds):
```bash
# Stop native
./dev-native.sh stop
# Start Docker prod
./start.sh prod
```
## Troubleshooting
### Discord OAuth Still Fails
**Symptoms**: Redirect to Discord works, but callback fails with "Invalid redirect_uri"
**Fixes**:
1. Verify you added `http://localhost:8000/api/auth/discord/callback/server` to Discord app
2. Check `backend/.env` has `APP_ENV=development`
3. Check `backend/.env` has `DISCORD_SERVER_REDIRECT_URI=http://localhost:8000/api/auth/discord/callback/server`
4. Restart backend: `./dev-native.sh restart`
### Backend Won't Start
**Check logs**:
```bash
tail -f .logs/backend.log
```
**Common issues**:
- Port 8000 already in use: `lsof -i :8000` (kill the process)
- Missing Python dependencies: `cd backend && uv sync`
- Database connection error: Check PostgreSQL is running
### Frontend Won't Start
**Check logs**:
```bash
tail -f .logs/frontend.log
```
**Common issues**:
- Port 3000 already in use: `lsof -i :3000` (kill the process)
- Missing node_modules: `cd frontend-sba && npm ci`
- Build error: Delete `.nuxt` and `.output` folders, restart
### Redis Connection Error
**Symptoms**: Backend logs show "Redis connection refused"
**Fix**:
```bash
# Check Redis status
docker compose ps redis
# Restart Redis if needed
docker compose restart redis
```
## Performance Comparison
| Method | Initial Start | After Code Change | Rebuild Time |
|--------|--------------|-------------------|--------------|
| **Native Dev** | 5-10 seconds | Instant (HMR) | Never |
| Docker Dev | 30-60 seconds | Instant (volume mount) | 2-3 minutes |
| Docker Prod | **3-5 minutes** | **3-5 minutes** | **3-5 minutes** |
## Notes
- **Redis still uses Docker** (lightweight, starts in seconds)
- **PostgreSQL** is external (on `10.10.0.42`), not affected
- `.pids/` and `.logs/` directories are gitignored
- Backend uses `uvicorn --reload` (watches Python files)
- Frontend uses `nuxt dev` (HMR for Vue files)
## When to Use Docker Prod Mode
Use `./start.sh prod` when:
- Testing production builds before deployment
- Debugging Docker-specific issues
- Testing HTTPS/cross-origin scenarios
- Running integration tests
For day-to-day development, use `./dev-native.sh start` for maximum speed.

24
backend/.env.network Normal file
View File

@ -0,0 +1,24 @@
# Application
APP_ENV=development
DEBUG=true
SECRET_KEY=5yIKYt_x20Kk-9PrQVfRzDGc-RFT06vTgIGAz13I8pU
# Database
DATABASE_URL=postgresql+asyncpg://paperdynasty:Snugly9-Gem-Configure@10.10.0.42:5432/paperdynasty_dev
# Discord OAuth
DISCORD_CLIENT_ID=1441192438055178420
DISCORD_CLIENT_SECRET=oj1OzUo4Qksph6mq57zb-qDiMxtb1n48
DISCORD_REDIRECT_URI=http://10.0.0.206:3000/auth/callback
DISCORD_SERVER_REDIRECT_URI=http://10.0.0.206:8000/api/auth/discord/callback/server
FRONTEND_URL=http://10.0.0.206:3000
# League APIs
SBA_API_URL=https://api.sba.manticorum.com
SBA_API_KEY=Tp3aO3jhYve5NJF1IqOmJTmk
PD_API_URL=https://pd-api.example.com
PD_API_KEY=placeholder-pd-api-key
# CORS
CORS_ORIGINS=["http://localhost:3000","http://localhost:3001","http://10.0.0.206:3000"]
ALLOWED_DISCORD_IDS=258104532423147520,139926308644847616

1
backend/.python-version Normal file
View File

@ -0,0 +1 @@
3.13

View File

@ -9,6 +9,7 @@ Date: 2025-11-27
"""
from fastapi import Response
from app.config import get_settings
settings = get_settings()
@ -45,27 +46,29 @@ def set_auth_cookies(
Security settings:
- HttpOnly: Prevents XSS access to tokens
- Secure: True (required for SameSite=None)
- SameSite=None: Required for Safari to send cookies with fetch/XHR requests
- Secure: Respects APP_ENV (false in dev, true in prod)
- SameSite: "lax" in dev (localhost), "none" in prod (cross-origin)
- Path: Limits cookie scope
Note: Safari's ITP treats SameSite=Lax cookies as "not sent" for XHR/fetch
requests even on same-origin. SameSite=None with Secure=true fixes this.
Note: In production with SameSite=None, Secure=true is required.
In development (localhost), we use SameSite=Lax which doesn't require Secure.
Args:
response: FastAPI Response object
access_token: JWT access token
refresh_token: JWT refresh token
"""
secure = is_secure_context()
samesite = "none" if secure else "lax"
# Access token - short-lived, sent to all requests (needed for SSR cookie forwarding)
# Using SameSite=None for Safari compatibility (requires Secure=true)
response.set_cookie(
key=ACCESS_TOKEN_COOKIE,
value=access_token,
max_age=ACCESS_TOKEN_MAX_AGE,
httponly=True,
secure=True, # Required for SameSite=None
samesite="none", # Safari requires this for fetch() to include cookies
secure=secure,
samesite=samesite,
path="/",
)
@ -75,8 +78,8 @@ def set_auth_cookies(
value=refresh_token,
max_age=REFRESH_TOKEN_MAX_AGE,
httponly=True,
secure=True, # Required for SameSite=None
samesite="none", # Safari requires this for fetch() to include cookies
secure=secure,
samesite=samesite,
path="/api/auth",
)

396
dev-native.sh Executable file
View File

@ -0,0 +1,396 @@
#!/bin/bash
#
# Paper Dynasty Game Engine - Native Development Mode
#
# Fast development workflow - no Docker rebuilds, instant restarts, hot-reload
#
# Usage:
# ./dev-native.sh start Start all services natively
# ./dev-native.sh stop Stop all services
# ./dev-native.sh logs Show logs from all services
# ./dev-native.sh restart Restart all services
#
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
cd "$SCRIPT_DIR"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
print_status() { echo -e "${BLUE}[INFO]${NC} $1"; }
print_success() { echo -e "${GREEN}[OK]${NC} $1"; }
print_warning() { echo -e "${YELLOW}[WARN]${NC} $1"; }
print_error() { echo -e "${RED}[ERROR]${NC} $1"; }
PID_DIR="$SCRIPT_DIR/.pids"
LOG_DIR="$SCRIPT_DIR/.logs"
# Create directories
mkdir -p "$PID_DIR" "$LOG_DIR"
# Check dependencies
check_dependencies() {
local missing=0
if ! command -v uv &> /dev/null; then
print_error "uv not found - install from https://docs.astral.sh/uv/"
missing=1
fi
if ! command -v node &> /dev/null; then
print_error "node not found - install Node.js 22+"
missing=1
fi
if ! command -v docker &> /dev/null; then
print_error "docker not found - needed for Redis"
missing=1
fi
if [[ $missing -eq 1 ]]; then
exit 1
fi
print_success "All dependencies found"
}
# Setup environment files
setup_env() {
local mode=${1:-local}
if [[ "$mode" == "network" ]]; then
print_status "Setting up NETWORK environment files..."
# Detect network IP
local network_ip=$(ip addr show | grep "inet " | grep -v "127.0.0.1" | grep -v "tailscale" | head -1 | awk '{print $2}' | cut -d'/' -f1)
if [[ -z "$network_ip" ]]; then
print_error "Could not detect network IP"
return 1
fi
print_status "Detected network IP: $network_ip"
# Backend
cp backend/.env.network backend/.env
sed -i "s|10\\.0\\.0\\.[0-9]*|$network_ip|g" backend/.env
# Frontend
cp frontend-sba/.env.network frontend-sba/.env
sed -i "s|10\\.0\\.0\\.[0-9]*|$network_ip|g" frontend-sba/.env
print_success "Network mode configured for $network_ip"
print_warning "Add to Discord OAuth: http://$network_ip:8000/api/auth/discord/callback/server"
else
print_status "Setting up development environment files..."
# Backend
if [[ ! -f "backend/.env" ]] || ! grep -q "localhost:8000" "backend/.env" 2>/dev/null; then
print_status "Copying backend/.env.dev -> backend/.env"
cp backend/.env.dev backend/.env
fi
# Frontend
if [[ ! -f "frontend-sba/.env" ]] || ! grep -q "localhost:8000" "frontend-sba/.env" 2>/dev/null; then
print_status "Copying frontend-sba/.env.dev -> frontend-sba/.env"
cp frontend-sba/.env.dev frontend-sba/.env
fi
print_success "Environment files ready"
fi
}
# Start Redis (Docker)
start_redis() {
print_status "Starting Redis (Docker)..."
if docker ps | grep -q "strat-gameplay-webapp-redis"; then
print_warning "Redis already running"
return 0
fi
docker compose up -d redis
# Wait for Redis to be healthy
local max_wait=30
local waited=0
while [[ $waited -lt $max_wait ]]; do
if docker compose ps redis 2>/dev/null | grep -q "healthy"; then
print_success "Redis is healthy"
return 0
fi
sleep 1
waited=$((waited + 1))
done
print_error "Redis failed to start"
return 1
}
# Start Backend (native Python with uv)
start_backend() {
print_status "Starting backend (native uvicorn)..."
cd backend
# Check if backend is already running
if [[ -f "$PID_DIR/backend.pid" ]]; then
local pid=$(cat "$PID_DIR/backend.pid")
if ps -p "$pid" > /dev/null 2>&1; then
print_warning "Backend already running (PID: $pid)"
cd ..
return 0
fi
fi
# Start backend in background
nohup uv run python -m uvicorn app.main:socket_app \
--host 0.0.0.0 \
--port 8000 \
--reload \
> "$LOG_DIR/backend.log" 2>&1 &
local pid=$!
echo $pid > "$PID_DIR/backend.pid"
cd ..
# Wait for backend to respond
local max_wait=10
local waited=0
while [[ $waited -lt $max_wait ]]; do
if curl -sf http://localhost:8000/api/health > /dev/null 2>&1; then
print_success "Backend started (PID: $pid)"
return 0
fi
sleep 1
waited=$((waited + 1))
done
print_error "Backend failed to start - check logs: tail -f $LOG_DIR/backend.log"
return 1
}
# Start Frontend (native npm)
start_frontend() {
print_status "Starting frontend (native nuxt dev)..."
cd frontend-sba
# Check if frontend is already running
if [[ -f "$PID_DIR/frontend.pid" ]]; then
local pid=$(cat "$PID_DIR/frontend.pid")
if ps -p "$pid" > /dev/null 2>&1; then
print_warning "Frontend already running (PID: $pid)"
cd ..
return 0
fi
fi
# Ensure node_modules exists
if [[ ! -d "node_modules" ]]; then
print_status "Installing frontend dependencies..."
npm ci
fi
# Start frontend in background
nohup npm run dev > "$LOG_DIR/frontend.log" 2>&1 &
local pid=$!
echo $pid > "$PID_DIR/frontend.pid"
cd ..
# Wait for frontend to respond
local max_wait=30
local waited=0
while [[ $waited -lt $max_wait ]]; do
if curl -sf http://localhost:3000 > /dev/null 2>&1; then
print_success "Frontend started (PID: $pid)"
return 0
fi
sleep 1
waited=$((waited + 1))
done
print_error "Frontend failed to start - check logs: tail -f $LOG_DIR/frontend.log"
return 1
}
# Stop all services
stop_services() {
print_status "Stopping all services..."
# Stop backend
if [[ -f "$PID_DIR/backend.pid" ]]; then
local pid=$(cat "$PID_DIR/backend.pid")
if ps -p "$pid" > /dev/null 2>&1; then
print_status "Stopping backend (PID: $pid)..."
kill $pid
rm "$PID_DIR/backend.pid"
fi
fi
# Stop frontend
if [[ -f "$PID_DIR/frontend.pid" ]]; then
local pid=$(cat "$PID_DIR/frontend.pid")
if ps -p "$pid" > /dev/null 2>&1; then
print_status "Stopping frontend (PID: $pid)..."
kill $pid
rm "$PID_DIR/frontend.pid"
fi
fi
# Stop Redis
print_status "Stopping Redis..."
docker compose stop redis
print_success "All services stopped"
}
# Show logs
show_logs() {
print_status "Showing logs (Ctrl+C to exit)..."
echo ""
# Use multitail if available, otherwise tail
if command -v multitail &> /dev/null; then
multitail -s 2 \
-l "tail -f $LOG_DIR/backend.log" \
-l "tail -f $LOG_DIR/frontend.log"
else
print_warning "Install 'multitail' for better log viewing"
tail -f "$LOG_DIR/backend.log" "$LOG_DIR/frontend.log"
fi
}
# Start all services
start_all() {
local mode=${1:-local}
print_status "Starting native development environment..."
echo ""
check_dependencies
setup_env "$mode"
start_redis || exit 1
start_backend || exit 1
start_frontend || exit 1
echo ""
print_success "Development environment ready!"
echo ""
if [[ "$mode" == "network" ]]; then
local network_ip=$(ip addr show | grep "inet " | grep -v "127.0.0.1" | grep -v "tailscale" | head -1 | awk '{print $2}' | cut -d'/' -f1)
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Native Development Mode (NETWORK)${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo " Access from ANY device on your network:"
echo " Frontend: http://$network_ip:3000"
echo " Backend: http://$network_ip:8000"
echo " API Docs: http://$network_ip:8000/docs"
echo ""
else
echo -e "${GREEN}========================================${NC}"
echo -e "${GREEN} Native Development Mode${NC}"
echo -e "${GREEN}========================================${NC}"
echo ""
echo " Backend API: http://localhost:8000"
echo " API Docs: http://localhost:8000/docs"
echo " Frontend: http://localhost:3000"
echo ""
fi
echo " Features:"
echo " ✓ Hot-reload enabled (backend + frontend)"
echo " ✓ No Docker rebuilds"
echo " ✓ Instant restarts"
echo ""
echo " Logs:"
echo " Backend: tail -f $LOG_DIR/backend.log"
echo " Frontend: tail -f $LOG_DIR/frontend.log"
echo " All: ./dev-native.sh logs"
echo ""
echo " Commands:"
echo " ./dev-native.sh logs View logs"
echo " ./dev-native.sh stop Stop services"
echo " ./dev-native.sh restart Restart services"
echo ""
}
# Restart all services
restart_all() {
local mode=${1:-local}
stop_services
sleep 2
start_all "$mode"
}
# Show usage
show_usage() {
local network_ip=$(ip addr show | grep "inet " | grep -v "127.0.0.1" | grep -v "tailscale" | head -1 | awk '{print $2}' | cut -d'/' -f1)
echo "Paper Dynasty - Native Development Mode"
echo ""
echo "Usage: ./dev-native.sh <command> [--network]"
echo ""
echo "Commands:"
echo " start Start services (localhost only)"
echo " start --network Start services (accessible from network)"
echo " stop Stop all services"
echo " logs Tail logs from all services"
echo " restart Restart services"
echo " restart --network Restart in network mode"
echo ""
echo "Network Mode:"
echo " Your IP: $network_ip"
echo " Access: http://$network_ip:3000"
echo ""
echo "Benefits:"
echo " • No Docker rebuilds (saves minutes)"
echo " • Instant startup"
echo " • Hot-reload on file changes (backend + frontend)"
echo " • Native debugging support"
echo ""
echo "Requirements:"
echo " • uv (Python package manager)"
echo " • Node.js 22+"
echo " • Docker (for Redis only)"
echo ""
}
# Main
MODE="local"
if [[ "${2:-}" == "--network" ]]; then
MODE="network"
fi
case "${1:-}" in
start)
start_all "$MODE"
;;
stop)
stop_services
;;
logs)
show_logs
;;
restart)
restart_all "$MODE"
;;
*)
show_usage
exit 1
;;
esac