refactor: move legacy files to organized directories
This commit is contained in:
@@ -1,273 +0,0 @@
|
|||||||
# EuchreCamp CasaOS Deployment Guide
|
|
||||||
|
|
||||||
This guide explains how to deploy EuchreCamp on your CasaOS server.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
1. **CasaOS** installed on your server
|
|
||||||
2. **Docker** and **Docker Compose** available in CasaOS
|
|
||||||
3. **Port 3000** available for the web interface
|
|
||||||
4. **Port 5432** available for PostgreSQL (or use external database)
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
**External PostgreSQL Database Required**
|
|
||||||
- PostgreSQL 15 or higher
|
|
||||||
- Database name: `euchre_camp`
|
|
||||||
- User: `euchre_camp`
|
|
||||||
- Privileges: ALL on database `euchre_camp`
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### Option 1: Using CasaOS App Store (Recommended)
|
|
||||||
|
|
||||||
1. **Set up PostgreSQL database first** (see below)
|
|
||||||
2. Open CasaOS Dashboard
|
|
||||||
3. Go to **App Store** → **Custom App**
|
|
||||||
4. Click **Install** and configure the following:
|
|
||||||
|
|
||||||
**Basic Settings:**
|
|
||||||
- App Name: `EuchreCamp`
|
|
||||||
- Image: `euchre-camp:latest` (build locally first)
|
|
||||||
- Port: `3000`
|
|
||||||
|
|
||||||
**Environment Variables:**
|
|
||||||
```
|
|
||||||
DATABASE_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp
|
|
||||||
DATABASE_SHADOW_URL=postgresql://euchre_camp:password@your-postgres-host:5432/euchre_camp_shadow
|
|
||||||
BETTER_AUTH_SECRET=your-secret-key-here
|
|
||||||
BETTER_AUTH_URL=http://your-casaos-ip:3000
|
|
||||||
TRUSTED_ORIGINS=http://your-casaos-ip:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
5. Click **Install**
|
|
||||||
|
|
||||||
### Option 2: Using Docker Compose
|
|
||||||
|
|
||||||
1. **Set up PostgreSQL database first** (see below)
|
|
||||||
2. **Build the Docker image:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.casaos.yml build
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Start the application:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.casaos.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
4. **Check logs:**
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.casaos.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
## PostgreSQL Database Setup
|
|
||||||
|
|
||||||
### Option A: External PostgreSQL Server
|
|
||||||
|
|
||||||
Connect to your PostgreSQL server and run:
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- Create database
|
|
||||||
CREATE DATABASE euchre_camp;
|
|
||||||
|
|
||||||
-- Create user
|
|
||||||
CREATE USER euchre_camp WITH PASSWORD 'your-strong-password';
|
|
||||||
|
|
||||||
-- Grant privileges
|
|
||||||
GRANT ALL PRIVILEGES ON DATABASE euchre_camp TO euchre_camp;
|
|
||||||
|
|
||||||
-- Connect to database
|
|
||||||
\c euchre_camp
|
|
||||||
|
|
||||||
-- Grant schema privileges
|
|
||||||
GRANT ALL ON SCHEMA public TO euchre_camp;
|
|
||||||
```
|
|
||||||
|
|
||||||
### Option B: CasaOS PostgreSQL Container
|
|
||||||
|
|
||||||
If you want to run PostgreSQL in CasaOS:
|
|
||||||
|
|
||||||
1. Install PostgreSQL from CasaOS App Store
|
|
||||||
2. Configure it with:
|
|
||||||
- Database: `euchre_camp`
|
|
||||||
- User: `euchre_camp`
|
|
||||||
- Password: `your-strong-password`
|
|
||||||
3. Use the connection string in your EuchreCamp configuration:
|
|
||||||
```
|
|
||||||
DATABASE_URL=postgresql://euchre_camp:your-strong-password@192.168.1.100:5432/euchre_camp
|
|
||||||
```
|
|
||||||
(Replace `192.168.1.100` with your CasaOS IP)
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
### Required Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `DATABASE_URL` | PostgreSQL connection string (REQUIRED) | `postgresql://euchre_camp:pass@host:5432/euchre_camp` |
|
|
||||||
| `BETTER_AUTH_SECRET` | Secret key for session encryption (generate with `openssl rand -base64 32`) | `your-generated-secret` |
|
|
||||||
| `BETTER_AUTH_URL` | Public URL of your application | `http://192.168.1.100:3000` |
|
|
||||||
| `TRUSTED_ORIGINS` | Comma-separated list of trusted origins | `http://192.168.1.100:3000` |
|
|
||||||
|
|
||||||
### Optional Environment Variables
|
|
||||||
|
|
||||||
| Variable | Description | Default |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `DATABASE_SHADOW_URL` | Shadow database for migrations | Same as DATABASE_URL with `_shadow` suffix |
|
|
||||||
|
|
||||||
## Database Management
|
|
||||||
|
|
||||||
### First-Time Setup
|
|
||||||
|
|
||||||
1. **Create the database and user** (see PostgreSQL Setup above)
|
|
||||||
2. **Run database migrations:**
|
|
||||||
```bash
|
|
||||||
docker exec -it euchre-camp npx prisma migrate deploy
|
|
||||||
```
|
|
||||||
3. **Create admin user:**
|
|
||||||
```bash
|
|
||||||
docker exec -it euchre-camp node scripts/create-admin-via-api.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Accessing the Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Connect to external PostgreSQL server
|
|
||||||
psql -h your-postgres-host -U euchre_camp -d euchre_camp
|
|
||||||
|
|
||||||
# List tables
|
|
||||||
\dt
|
|
||||||
|
|
||||||
# Exit
|
|
||||||
\q
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backing Up Data
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Backup PostgreSQL data (run on your PostgreSQL server)
|
|
||||||
pg_dump -h your-postgres-host -U euchre_camp euchre_camp > backup.sql
|
|
||||||
|
|
||||||
# Backup uploaded files
|
|
||||||
tar -czf euchre_camp_files.tar.gz /var/lib/docker/volumes/euchre_camp_app_data
|
|
||||||
```
|
|
||||||
|
|
||||||
## Security Considerations
|
|
||||||
|
|
||||||
### Change Default Passwords
|
|
||||||
|
|
||||||
1. **PostgreSQL Password:**
|
|
||||||
- Edit `docker-compose.casaos.yml` or set via CasaOS environment variables
|
|
||||||
- Update `POSTGRES_PASSWORD` to a strong password
|
|
||||||
|
|
||||||
2. **Better Auth Secret:**
|
|
||||||
```bash
|
|
||||||
openssl rand -base64 32
|
|
||||||
```
|
|
||||||
- Use this value for `BETTER_AUTH_SECRET`
|
|
||||||
|
|
||||||
### HTTPS Setup
|
|
||||||
|
|
||||||
For production use with HTTPS:
|
|
||||||
|
|
||||||
1. **Option A: CasaOS Reverse Proxy**
|
|
||||||
- Configure CasaOS to use HTTPS
|
|
||||||
- Update `BETTER_AUTH_URL` to `https://your-domain.com`
|
|
||||||
|
|
||||||
2. **Option B: External Reverse Proxy (Nginx, Traefik)**
|
|
||||||
- Configure your reverse proxy
|
|
||||||
- Update `TRUSTED_ORIGINS` to include your domain
|
|
||||||
|
|
||||||
### Firewall Rules
|
|
||||||
|
|
||||||
Ensure only necessary ports are exposed:
|
|
||||||
- **Port 3000**: Web interface (HTTP/HTTPS)
|
|
||||||
- **Port 5432**: PostgreSQL (only if external access needed)
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Database Connection Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check PostgreSQL logs
|
|
||||||
docker logs euchre-camp-postgres
|
|
||||||
|
|
||||||
# Check if PostgreSQL is healthy
|
|
||||||
docker exec euchre-camp-postgres pg_isready -U euchre -d euchre_camp
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Not Starting
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check application logs
|
|
||||||
docker logs euchre-camp
|
|
||||||
|
|
||||||
# Check if Prisma migrations are applied
|
|
||||||
docker exec euchre-camp npx prisma migrate status
|
|
||||||
```
|
|
||||||
|
|
||||||
### Permission Issues
|
|
||||||
|
|
||||||
If you see permission errors:
|
|
||||||
```bash
|
|
||||||
# Fix volume permissions
|
|
||||||
docker exec euchre-camp chown -R euchre:euchre /app/public/uploads
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Tuning
|
|
||||||
|
|
||||||
### PostgreSQL Optimization
|
|
||||||
|
|
||||||
Add to `docker-compose.casaos.yml` under `postgres` service:
|
|
||||||
```yaml
|
|
||||||
environment:
|
|
||||||
- POSTGRES_SHARED_BUFFERS=256MB
|
|
||||||
- POSTGRES_WORK_MEM=16MB
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Optimization
|
|
||||||
|
|
||||||
The Dockerfile is already optimized with:
|
|
||||||
- Multi-stage build (smaller image size)
|
|
||||||
- Non-root user for security
|
|
||||||
- Health checks
|
|
||||||
- Production-ready settings
|
|
||||||
|
|
||||||
## Updating
|
|
||||||
|
|
||||||
To update EuchreCamp:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull latest changes
|
|
||||||
git pull origin main
|
|
||||||
|
|
||||||
# Rebuild and restart
|
|
||||||
docker-compose -f docker-compose.casaos.yml down
|
|
||||||
docker-compose -f docker-compose.casaos.yml build --no-cache
|
|
||||||
docker-compose -f docker-compose.casaos.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.casaos.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Check Container Status
|
|
||||||
```bash
|
|
||||||
docker ps | grep euchre
|
|
||||||
```
|
|
||||||
|
|
||||||
### Resource Usage
|
|
||||||
```bash
|
|
||||||
docker stats euchre-camp euchre-camp-postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
For issues or questions:
|
|
||||||
- Check the application logs first
|
|
||||||
- Verify environment variables are set correctly
|
|
||||||
- Ensure PostgreSQL is running and accessible
|
|
||||||
- Check CasaOS app logs for deployment issues
|
|
||||||
@@ -1,370 +0,0 @@
|
|||||||
# Docker Deployment Guide
|
|
||||||
|
|
||||||
This guide covers how to run EuchreCamp using Docker and Docker Compose.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker Engine 20.10+
|
|
||||||
- Docker Compose v2.0+
|
|
||||||
- Make (optional, for using Makefile commands)
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
### 1. Environment Setup
|
|
||||||
|
|
||||||
Create a `.env` file in the project root:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
Update the `.env` file with your configuration:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Database Configuration (PostgreSQL in Docker)
|
|
||||||
DATABASE_PROVIDER=postgresql
|
|
||||||
DATABASE_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp
|
|
||||||
DATABASE_SHADOW_URL=postgresql://euchre:euchrepassword@postgres:5432/euchre_camp_shadow
|
|
||||||
|
|
||||||
# Better Auth (generate a secure secret for production)
|
|
||||||
BETTER_AUTH_SECRET=your-secret-key-change-in-production
|
|
||||||
BETTER_AUTH_URL=http://localhost:3000
|
|
||||||
|
|
||||||
# Application
|
|
||||||
NODE_ENV=production
|
|
||||||
TRUSTED_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Build and Run
|
|
||||||
|
|
||||||
#### Production Mode
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build and start all services
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Stop all services
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
#### Development Mode
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use development override configuration
|
|
||||||
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
|
||||||
|
|
||||||
# Or use the shorthand
|
|
||||||
docker-compose --profile dev up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Access the Application
|
|
||||||
|
|
||||||
- **Web Application**: http://localhost:3000
|
|
||||||
- **PostgreSQL**: localhost:5432 (default) or 5433 in development
|
|
||||||
|
|
||||||
## Service Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────┐
|
|
||||||
│ EuchreCamp │
|
|
||||||
├─────────────────────────────────────────────┤
|
|
||||||
│ │
|
|
||||||
│ ┌──────────────┐ ┌──────────────────┐ │
|
|
||||||
│ │ Next.js │◄───│ PostgreSQL │ │
|
|
||||||
│ │ App │ │ Database │ │
|
|
||||||
│ │ │ │ │ │
|
|
||||||
│ │ Port: 3000 │ │ Port: 5432 │ │
|
|
||||||
│ └──────────────┘ └──────────────────┘ │
|
|
||||||
│ │
|
|
||||||
└─────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Service Details
|
|
||||||
|
|
||||||
### App Service
|
|
||||||
|
|
||||||
- **Image**: Multi-stage Next.js build
|
|
||||||
- **Port**: 3000
|
|
||||||
- **Environment**: Production or Development
|
|
||||||
- **Volumes**:
|
|
||||||
- `app_data`: Persistent storage for uploads
|
|
||||||
|
|
||||||
### PostgreSQL Service
|
|
||||||
|
|
||||||
- **Image**: postgres:15-alpine
|
|
||||||
- **Port**: 5432 (production) / 5433 (development)
|
|
||||||
- **Data Volume**: `postgres_data` (persistent)
|
|
||||||
- **Health Check**: Uses `pg_isready`
|
|
||||||
|
|
||||||
## Database Management
|
|
||||||
|
|
||||||
### View Database Data
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Access PostgreSQL shell
|
|
||||||
docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp
|
|
||||||
|
|
||||||
# List tables
|
|
||||||
\dt
|
|
||||||
|
|
||||||
# Exit
|
|
||||||
\q
|
|
||||||
```
|
|
||||||
|
|
||||||
### Backup Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create backup
|
|
||||||
docker exec euchre-camp-postgres pg_dump -U euchre euchre_camp > backup.sql
|
|
||||||
|
|
||||||
# Restore backup
|
|
||||||
docker exec -i euchre-camp-postgres psql -U euchre euchre_camp < backup.sql
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reset Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove all data and restart
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running Commands
|
|
||||||
|
|
||||||
### Run Database Migrations
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Apply migrations
|
|
||||||
docker-compose exec app npx prisma migrate deploy
|
|
||||||
|
|
||||||
# Generate Prisma client
|
|
||||||
docker-compose exec app npx prisma generate
|
|
||||||
|
|
||||||
# Open Prisma Studio
|
|
||||||
docker-compose exec app npx prisma studio
|
|
||||||
```
|
|
||||||
|
|
||||||
### Create Admin User
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create admin user via script
|
|
||||||
docker-compose exec app node scripts/create-admin-via-api.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### Seed Database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Run seed script
|
|
||||||
docker-compose exec app node scripts/seed.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development Workflow
|
|
||||||
|
|
||||||
### Hot Reloading
|
|
||||||
|
|
||||||
Development mode supports hot reloading:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start in development mode
|
|
||||||
docker-compose -f docker-compose.yml -f docker-compose.override.yml up -d
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker-compose logs -f app
|
|
||||||
```
|
|
||||||
|
|
||||||
### Modifying Code
|
|
||||||
|
|
||||||
Changes to source code are automatically reflected due to volume mounting:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Edit a file locally
|
|
||||||
nano src/app/page.tsx
|
|
||||||
|
|
||||||
# Next.js will automatically reload
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Common Issues
|
|
||||||
|
|
||||||
**1. Database Connection Failed**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if PostgreSQL is running
|
|
||||||
docker ps | grep postgres
|
|
||||||
|
|
||||||
# Check logs
|
|
||||||
docker logs euchre-camp-postgres
|
|
||||||
|
|
||||||
# Restart services
|
|
||||||
docker-compose restart postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
**2. Port Already in Use**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check what's using port 3000
|
|
||||||
lsof -i :3000
|
|
||||||
|
|
||||||
# Change port in docker-compose.yml
|
|
||||||
# ports:
|
|
||||||
# - "3001:3000"
|
|
||||||
```
|
|
||||||
|
|
||||||
**3. Permission Denied**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Ensure scripts are executable
|
|
||||||
chmod +x scripts/*.sh
|
|
||||||
|
|
||||||
# Check volume permissions
|
|
||||||
docker exec euchre-camp-app ls -la /app
|
|
||||||
```
|
|
||||||
|
|
||||||
**4. Migration Errors**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Reset database
|
|
||||||
docker-compose down -v
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Apply migrations manually
|
|
||||||
docker-compose exec app npx prisma migrate deploy
|
|
||||||
```
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# All services
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
# Specific service
|
|
||||||
docker-compose logs -f app
|
|
||||||
docker-compose logs -f postgres
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container Shell Access
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# App container
|
|
||||||
docker exec -it euchre-camp-app sh
|
|
||||||
|
|
||||||
# PostgreSQL container
|
|
||||||
docker exec -it euchre-camp-postgres sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
### Environment Variables
|
|
||||||
|
|
||||||
For production, ensure you set secure values:
|
|
||||||
|
|
||||||
```env
|
|
||||||
# Generate a secure secret
|
|
||||||
BETTER_AUTH_SECRET=$(openssl rand -base64 32)
|
|
||||||
|
|
||||||
# Use real domain
|
|
||||||
BETTER_AUTH_URL=https://euchrecamp.example.com
|
|
||||||
|
|
||||||
# Trusted origins
|
|
||||||
TRUSTED_ORIGINS=https://euchrecamp.example.com,https://www.euchrecamp.example.com
|
|
||||||
```
|
|
||||||
|
|
||||||
### Security Considerations
|
|
||||||
|
|
||||||
1. **Change default PostgreSQL password**:
|
|
||||||
```env
|
|
||||||
POSTGRES_PASSWORD=your-secure-password
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Use SSL/TLS for database connections**:
|
|
||||||
```env
|
|
||||||
DATABASE_URL=postgresql://euchre:password@postgres:5432/euchre_camp?sslmode=require
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Restrict network access**:
|
|
||||||
- Don't expose PostgreSQL port to public internet
|
|
||||||
- Use firewall rules to restrict access
|
|
||||||
|
|
||||||
4. **Regular backups**:
|
|
||||||
- Schedule automated database backups
|
|
||||||
- Test restoration process
|
|
||||||
|
|
||||||
### Docker Swarm / Kubernetes
|
|
||||||
|
|
||||||
For production orchestration, consider:
|
|
||||||
|
|
||||||
1. **Separate database service** - Use managed PostgreSQL (RDS, Cloud SQL)
|
|
||||||
2. **Secrets management** - Use Docker secrets or Kubernetes Secrets
|
|
||||||
3. **Health checks** - Configure proper health checks
|
|
||||||
4. **Resource limits** - Set CPU and memory limits
|
|
||||||
5. **Replicas** - Run multiple app instances for high availability
|
|
||||||
|
|
||||||
## Makefile (Optional)
|
|
||||||
|
|
||||||
Create a `Makefile` for convenient commands:
|
|
||||||
|
|
||||||
```makefile
|
|
||||||
.PHONY: up down logs build shell-migrate
|
|
||||||
|
|
||||||
up:
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
down:
|
|
||||||
docker-compose down
|
|
||||||
|
|
||||||
logs:
|
|
||||||
docker-compose logs -f
|
|
||||||
|
|
||||||
build:
|
|
||||||
docker-compose build --no-cache
|
|
||||||
|
|
||||||
shell-app:
|
|
||||||
docker exec -it euchre-camp-app sh
|
|
||||||
|
|
||||||
shell-db:
|
|
||||||
docker exec -it euchre-camp-postgres psql -U euchre -d euchre_camp
|
|
||||||
|
|
||||||
migrate:
|
|
||||||
docker-compose exec app npx prisma migrate deploy
|
|
||||||
|
|
||||||
seed:
|
|
||||||
docker-compose exec app node scripts/seed.js
|
|
||||||
```
|
|
||||||
|
|
||||||
## Performance Tuning
|
|
||||||
|
|
||||||
### PostgreSQL Configuration
|
|
||||||
|
|
||||||
Edit `docker-compose.yml` to tune PostgreSQL:
|
|
||||||
|
|
||||||
```yaml
|
|
||||||
postgres:
|
|
||||||
# ... other config ...
|
|
||||||
command: >
|
|
||||||
postgres
|
|
||||||
-c max_connections=200
|
|
||||||
-c shared_buffers=256MB
|
|
||||||
-c effective_cache_size=1GB
|
|
||||||
-c maintenance_work_mem=64MB
|
|
||||||
-c checkpoint_completion_target=0.9
|
|
||||||
-c wal_buffers=16MB
|
|
||||||
-c default_statistics_target=100
|
|
||||||
```
|
|
||||||
|
|
||||||
### Next.js Optimization
|
|
||||||
|
|
||||||
The Dockerfile uses multi-stage build for optimal size and performance:
|
|
||||||
|
|
||||||
- **Builder stage**: Installs all dependencies and builds the app
|
|
||||||
- **Runner stage**: Copies only the built artifacts and production dependencies
|
|
||||||
|
|
||||||
## References
|
|
||||||
|
|
||||||
- [Docker Compose Documentation](https://docs.docker.com/compose/)
|
|
||||||
- [PostgreSQL Docker Image](https://hub.docker.com/_/postgres)
|
|
||||||
- [Next.js Deployment](https://nextjs.org/docs/deployment)
|
|
||||||
- [Prisma Deployment](https://www.prisma.io/docs/concepts/components/prisma-migrate/deployment)
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
# EuchreCamp - Todo List
|
|
||||||
|
|
||||||
## Current Tasks
|
|
||||||
|
|
||||||
### Completed ✅
|
|
||||||
- [x] Add `site_admin` role to database schema and permissions system
|
|
||||||
- [x] Add `isCasual` boolean field to Match model (already existed)
|
|
||||||
- [x] Update match upload API to support casual matches
|
|
||||||
- [x] Update match upload UI to include casual checkbox
|
|
||||||
- [x] Add tournament deletion API endpoint with delete/orphan options
|
|
||||||
- [x] Add delete tournament button and modal to tournament detail page
|
|
||||||
- [x] Run tests and verify implementation (84 tests passing)
|
|
||||||
- [x] Fix session issues with tournament admin access
|
|
||||||
- [x] Fix Elo recalculation error for player merge (delete elo snapshots before deleting players)
|
|
||||||
- [x] Add admin player management page
|
|
||||||
- [x] Add player name editing functionality in admin UI
|
|
||||||
- [x] Add admin panel links to navigation header
|
|
||||||
- [x] Add tournament update API endpoint (PUT /api/tournaments/[id])
|
|
||||||
- [x] Consolidate delete endpoint from admin API to main tournaments API
|
|
||||||
- [x] Update database schema to add variant scoring fields (targetScore, allowTies)
|
|
||||||
- [x] Fix tie handling logic in partnership stats (ties now correctly tracked)
|
|
||||||
- [x] Fix test files for normalizedName field in Player model
|
|
||||||
- [x] Fix auth.ts to include normalizedName in Player creation
|
|
||||||
- [x] Write TODO list to repository file
|
|
||||||
- [x] Auto-create tournament when uploading matches without selecting one
|
|
||||||
|
|
||||||
### In Progress 🔄
|
|
||||||
- [ ] Update API routes to handle new variant scoring fields
|
|
||||||
- [ ] Update EditTournamentForm to add variant scoring controls
|
|
||||||
- [ ] Update MatchEditor to use tournament-specific target score
|
|
||||||
- [ ] Run tests and verify variant scoring implementation
|
|
||||||
|
|
||||||
### Recently Completed ✅
|
|
||||||
- [x] Add OpenSkill rating system support (src/lib/openskill-utils.ts)
|
|
||||||
- [x] Add Glicko2 rating system support (src/lib/glicko2-utils.ts)
|
|
||||||
- [x] Reset database and run all migrations from scratch
|
|
||||||
- [x] Regenerate Prisma client with new rating models
|
|
||||||
- [x] Update match upload page to auto-create tournament if none selected
|
|
||||||
- [x] Update all admin scripts to use PrismaPg adapter and dotenv
|
|
||||||
- [x] Fix match diagram player positioning
|
|
||||||
- [x] Add CasaOS deployment configuration and documentation
|
|
||||||
- [x] Create migration to add rating system tables (elo_ratings, glicko2_ratings, open_skill_ratings)
|
|
||||||
- [x] Add tabbed rankings page to display Elo, OpenSkill, and Glicko2 ratings
|
|
||||||
|
|
||||||
### Backlog 📋
|
|
||||||
- [ ] Add UI controls for variant scoring in tournament creation/edit
|
|
||||||
- [ ] Test variant tournament functionality end-to-end
|
|
||||||
- [ ] Add validation for tie scores based on tournament configuration
|
|
||||||
- [ ] Document variant tournament features
|
|
||||||
|
|
||||||
## Recently Completed (Detailed)
|
|
||||||
|
|
||||||
### Variant Euchre Scoring Support
|
|
||||||
- Added `targetScore` and `allowTies` fields to Event model
|
|
||||||
- Created database migration for new fields
|
|
||||||
- Fixed partnership stats tie handling (ties now increment neither wins nor losses)
|
|
||||||
- Updated Elo calculation functions to handle ties correctly (0.5 points for draw)
|
|
||||||
|
|
||||||
### Tournament Deletion
|
|
||||||
- Consolidated delete endpoint to `/api/tournaments/[id]`
|
|
||||||
- Added options to delete matches or orphan them
|
|
||||||
- Updated DeleteTournamentButton to use consolidated endpoint
|
|
||||||
|
|
||||||
### Player Management
|
|
||||||
- Added admin players page at `/admin/players`
|
|
||||||
- Added player name editing functionality via PATCH endpoint
|
|
||||||
- Added player merge functionality with automatic Elo recalculation
|
|
||||||
- Fixed foreign key constraint issues with elo_snapshots
|
|
||||||
|
|
||||||
### Permissions
|
|
||||||
- Added `site_admin` role as highest privilege level
|
|
||||||
- Updated all permission functions to include site_admin support
|
|
||||||
- Fixed session cache issues by reading roles from database
|
|
||||||
|
|
||||||
## Notes
|
|
||||||
- All 84 unit tests passing
|
|
||||||
- Database migrations applied successfully
|
|
||||||
- TypeScript compilation has pre-existing errors unrelated to our changes
|
|
||||||
|
|
||||||
### Completed After Commit 1729dac
|
|
||||||
|
|
||||||
#### Next.js 16 Breaking Change Fixes
|
|
||||||
- [x] Fixed `params.id` usage in all page components (must use `await params`)
|
|
||||||
- [x] Fixed `params.id` usage in all API routes (must use `await params`)
|
|
||||||
- [x] Updated client components to use `Promise<{ id: string }>` type
|
|
||||||
- [x] Added regression tests for Next.js 16 params Promise handling
|
|
||||||
- [x] Verified all 100 unit tests pass
|
|
||||||
|
|
||||||
#### Files Updated:
|
|
||||||
- Player pages: `profile.tsx`, `schedule.tsx`
|
|
||||||
- Tournament pages: `page.tsx`, `results.tsx`, `edit.tsx`, `entry.tsx`
|
|
||||||
- API routes: `admin/players/[id]/route.ts`, `users/[id]/route.ts`, `users/[id]/role/route.ts`
|
|
||||||
- Tournament API routes: `[id]/route.ts`, `[id]/participants/route.ts`, `[id]/games/bulk/route.ts`
|
|
||||||
|
|
||||||
#### Root Cause
|
|
||||||
Next.js 16 requires `params` to be awaited in both server components and API routes:
|
|
||||||
- Before: `const { id } = params`
|
|
||||||
- After: `const { id } = await params`
|
|
||||||
|
|
||||||
This was not caught by the unit test suite because:
|
|
||||||
- Unit tests test individual functions in isolation
|
|
||||||
- E2E tests (Playwright) would catch this but weren't run after the upgrade
|
|
||||||
@@ -1,138 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Generate 100+ games to test ELO rating calculations
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import random
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
DB_PATH = "prisma/prisma/dev.db"
|
|
||||||
|
|
||||||
|
|
||||||
def get_players():
|
|
||||||
"""Get all players from the database"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("SELECT id, name, currentElo FROM players")
|
|
||||||
players = cursor.fetchall()
|
|
||||||
conn.close()
|
|
||||||
return players
|
|
||||||
|
|
||||||
|
|
||||||
def generate_game(players, game_num, base_date):
|
|
||||||
"""Generate a single game with random players and scores"""
|
|
||||||
# Randomly select 4 different players
|
|
||||||
selected_players = random.sample(players, 4)
|
|
||||||
p1, p2, p3, p4 = selected_players
|
|
||||||
|
|
||||||
# Randomly assign teams (Team 1 vs Team 2)
|
|
||||||
team1_p1 = p1
|
|
||||||
team1_p2 = p2
|
|
||||||
team2_p1 = p3
|
|
||||||
team2_p2 = p4
|
|
||||||
|
|
||||||
# Generate realistic scores (Euchre games typically 10 points max)
|
|
||||||
# Team 1 wins 60% of the time for variety
|
|
||||||
team1_wins = random.random() < 0.6
|
|
||||||
if team1_wins:
|
|
||||||
# Team 1 wins - generate scores
|
|
||||||
team1_score = random.randint(10, 15)
|
|
||||||
team2_score = random.randint(0, 9)
|
|
||||||
else:
|
|
||||||
# Team 2 wins - generate scores
|
|
||||||
team2_score = random.randint(10, 15)
|
|
||||||
team1_score = random.randint(0, 9)
|
|
||||||
|
|
||||||
# Generate random date in the past 30 days
|
|
||||||
days_ago = random.randint(0, 30)
|
|
||||||
game_date = base_date - timedelta(
|
|
||||||
days=days_ago, hours=random.randint(0, 23), minutes=random.randint(0, 59)
|
|
||||||
)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"team1P1Id": team1_p1[0],
|
|
||||||
"team1P2Id": team1_p2[0],
|
|
||||||
"team2P1Id": team2_p1[0],
|
|
||||||
"team2P2Id": team2_p2[0],
|
|
||||||
"team1Score": team1_score,
|
|
||||||
"team2Score": team2_score,
|
|
||||||
"playedAt": game_date.isoformat() + "Z",
|
|
||||||
"status": "completed",
|
|
||||||
"eventId": None,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def insert_games(games):
|
|
||||||
"""Insert games into the database"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
for game in games:
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO matches
|
|
||||||
(team1P1Id, team1P2Id, team2P1Id, team2P2Id, team1Score, team2Score, playedAt, status, eventId, createdAt, updatedAt)
|
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
game["team1P1Id"],
|
|
||||||
game["team1P2Id"],
|
|
||||||
game["team2P1Id"],
|
|
||||||
game["team2P2Id"],
|
|
||||||
game["team1Score"],
|
|
||||||
game["team2Score"],
|
|
||||||
game["playedAt"],
|
|
||||||
game["status"],
|
|
||||||
game["eventId"],
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Generating 150 games to test ELO ratings...")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
players = get_players()
|
|
||||||
print(f"Found {len(players)} players in database")
|
|
||||||
|
|
||||||
# Generate 150 games
|
|
||||||
num_games = 150
|
|
||||||
base_date = datetime.now()
|
|
||||||
|
|
||||||
games = []
|
|
||||||
for i in range(num_games):
|
|
||||||
game = generate_game(players, i, base_date)
|
|
||||||
games.append(game)
|
|
||||||
|
|
||||||
print(f"Generated {len(games)} games")
|
|
||||||
|
|
||||||
# Insert games into database
|
|
||||||
print("Inserting games into database...")
|
|
||||||
insert_games(games)
|
|
||||||
print("Games inserted successfully!")
|
|
||||||
|
|
||||||
# Show sample games
|
|
||||||
print("\nSample games:")
|
|
||||||
print("-" * 80)
|
|
||||||
for i, game in enumerate(games[:3]):
|
|
||||||
print(
|
|
||||||
f"Game {i + 1}: Team 1 ({game['team1Score']}) vs Team 2 ({game['team2Score']})"
|
|
||||||
)
|
|
||||||
print(f" Team 1: Player {game['team1P1Id']} + Player {game['team1P2Id']}")
|
|
||||||
print(f" Team 2: Player {game['team2P1Id']} + Player {game['team2P2Id']}")
|
|
||||||
print(f" Date: {game['playedAt']}")
|
|
||||||
print()
|
|
||||||
|
|
||||||
print("=" * 60)
|
|
||||||
print(f"Total games generated: {num_games}")
|
|
||||||
print("Now check the ELO ratings by running the application!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,234 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Update partnership statistics based on matches in the database
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import math
|
|
||||||
from datetime import datetime
|
|
||||||
|
|
||||||
DB_PATH = "prisma/prisma/dev.db"
|
|
||||||
K_FACTOR = 32
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_matches():
|
|
||||||
"""Get all matches from the database"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id,
|
|
||||||
team1Score, team2Score, playedAt
|
|
||||||
FROM matches
|
|
||||||
ORDER BY playedAt
|
|
||||||
""")
|
|
||||||
matches = cursor.fetchall()
|
|
||||||
conn.close()
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
def get_or_create_partnership(player1_id, player2_id):
|
|
||||||
"""Get or create a partnership record"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# Sort IDs to ensure consistent partnership lookup
|
|
||||||
p1 = min(player1_id, player2_id)
|
|
||||||
p2 = max(player1_id, player2_id)
|
|
||||||
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
SELECT id, gamesPlayed, wins, losses, totalEloChange
|
|
||||||
FROM partnership_stats
|
|
||||||
WHERE player1Id = ? AND player2Id = ?
|
|
||||||
""",
|
|
||||||
(p1, p2),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = cursor.fetchone()
|
|
||||||
if result:
|
|
||||||
conn.close()
|
|
||||||
return result
|
|
||||||
|
|
||||||
# Create new partnership record
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt)
|
|
||||||
VALUES (?, ?, 0, 0, 0, 0, NULL, ?, ?)
|
|
||||||
""",
|
|
||||||
(p1, p2, datetime.now().isoformat() + "Z", datetime.now().isoformat() + "Z"),
|
|
||||||
)
|
|
||||||
|
|
||||||
partnership_id = cursor.lastrowid
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
return (partnership_id, 0, 0, 0, 0)
|
|
||||||
|
|
||||||
|
|
||||||
def update_partnership_stats(player1_id, player2_id, won, elo_change):
|
|
||||||
"""Update partnership statistics"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# Sort IDs to ensure consistent partnership lookup
|
|
||||||
p1 = min(player1_id, player2_id)
|
|
||||||
p2 = max(player1_id, player2_id)
|
|
||||||
|
|
||||||
# Get current partnership stats
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
SELECT id, gamesPlayed, wins, losses, totalEloChange
|
|
||||||
FROM partnership_stats
|
|
||||||
WHERE player1Id = ? AND player2Id = ?
|
|
||||||
""",
|
|
||||||
(p1, p2),
|
|
||||||
)
|
|
||||||
|
|
||||||
result = cursor.fetchone()
|
|
||||||
if not result:
|
|
||||||
# Create new partnership record
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
INSERT INTO partnership_stats (player1Id, player2Id, gamesPlayed, wins, losses, totalEloChange, lastPlayed, createdAt, updatedAt)
|
|
||||||
VALUES (?, ?, 1, ?, ?, ?, ?, ?, ?)
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
p1,
|
|
||||||
p2,
|
|
||||||
1 if won else 0,
|
|
||||||
0 if won else 1,
|
|
||||||
elo_change,
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
),
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
partnership_id, games_played, wins, losses, total_elo_change = result
|
|
||||||
|
|
||||||
# Update partnership stats
|
|
||||||
new_games = games_played + 1
|
|
||||||
new_wins = wins + 1 if won else wins
|
|
||||||
new_losses = losses if won else losses + 1
|
|
||||||
new_total_elo_change = total_elo_change + elo_change
|
|
||||||
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
UPDATE partnership_stats
|
|
||||||
SET gamesPlayed = ?, wins = ?, losses = ?, totalEloChange = ?, lastPlayed = ?, updatedAt = ?
|
|
||||||
WHERE id = ?
|
|
||||||
""",
|
|
||||||
(
|
|
||||||
new_games,
|
|
||||||
new_wins,
|
|
||||||
new_losses,
|
|
||||||
new_total_elo_change,
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
datetime.now().isoformat() + "Z",
|
|
||||||
partnership_id,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Updating partnership statistics based on matches...")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Get all matches
|
|
||||||
matches = get_all_matches()
|
|
||||||
print(f"Found {len(matches)} matches in database")
|
|
||||||
|
|
||||||
# Reset partnership stats
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("DELETE FROM partnership_stats")
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
print("Reset all partnership statistics")
|
|
||||||
|
|
||||||
# Process each match
|
|
||||||
match_count = 0
|
|
||||||
for (
|
|
||||||
match_id,
|
|
||||||
team1_p1,
|
|
||||||
team1_p2,
|
|
||||||
team2_p1,
|
|
||||||
team2_p2,
|
|
||||||
team1_score,
|
|
||||||
team2_score,
|
|
||||||
played_at,
|
|
||||||
) in matches:
|
|
||||||
# Determine winners
|
|
||||||
team1_won = team1_score > team2_score
|
|
||||||
team2_won = team2_score > team1_score
|
|
||||||
|
|
||||||
# Update partnership stats for Team 1
|
|
||||||
if team1_won:
|
|
||||||
update_partnership_stats(
|
|
||||||
team1_p1, team1_p2, True, 0
|
|
||||||
) # Elo change will be calculated separately
|
|
||||||
else:
|
|
||||||
update_partnership_stats(team1_p1, team1_p2, False, 0)
|
|
||||||
|
|
||||||
# Update partnership stats for Team 2
|
|
||||||
if team2_won:
|
|
||||||
update_partnership_stats(team2_p1, team2_p2, True, 0)
|
|
||||||
else:
|
|
||||||
update_partnership_stats(team2_p1, team2_p2, False, 0)
|
|
||||||
|
|
||||||
match_count += 1
|
|
||||||
if match_count % 20 == 0:
|
|
||||||
print(f"Processed {match_count}/{len(matches)} matches...")
|
|
||||||
|
|
||||||
print(f"Processed {match_count} matches")
|
|
||||||
|
|
||||||
# Display partnership stats for top players
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Partnership Stats for Top Players:")
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
|
|
||||||
# Get top players by ELO
|
|
||||||
cursor.execute("SELECT id, name FROM players ORDER BY currentElo DESC LIMIT 5")
|
|
||||||
top_players = cursor.fetchall()
|
|
||||||
|
|
||||||
for player_id, player_name in top_players:
|
|
||||||
print(f"\n{player_name}:")
|
|
||||||
|
|
||||||
# Get partnership stats for this player
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
SELECT
|
|
||||||
CASE
|
|
||||||
WHEN player1Id = ? THEN (SELECT name FROM players WHERE id = player2Id)
|
|
||||||
ELSE (SELECT name FROM players WHERE id = player1Id)
|
|
||||||
END as partner_name,
|
|
||||||
gamesPlayed, wins, losses, totalEloChange
|
|
||||||
FROM partnership_stats
|
|
||||||
WHERE player1Id = ? OR player2Id = ?
|
|
||||||
ORDER BY gamesPlayed DESC
|
|
||||||
LIMIT 3
|
|
||||||
""",
|
|
||||||
(player_id, player_id, player_id),
|
|
||||||
)
|
|
||||||
|
|
||||||
partnerships = cursor.fetchall()
|
|
||||||
for partner_name, games, wins, losses, elo_change in partnerships:
|
|
||||||
win_rate = (wins / games * 100) if games > 0 else 0
|
|
||||||
print(
|
|
||||||
f" - with {partner_name}: {games} games, {wins}/{losses} ({win_rate:.1f}%) ELO: {elo_change:+d}"
|
|
||||||
)
|
|
||||||
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Partnership statistics updated successfully!")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
@@ -1,195 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""
|
|
||||||
Update player statistics (ELO, gamesPlayed, wins, losses) based on matches in the database
|
|
||||||
"""
|
|
||||||
|
|
||||||
import sqlite3
|
|
||||||
import math
|
|
||||||
|
|
||||||
DB_PATH = "prisma/prisma/dev.db"
|
|
||||||
K_FACTOR = 32 # Standard K-factor for Elo calculations
|
|
||||||
|
|
||||||
|
|
||||||
def get_all_matches():
|
|
||||||
"""Get all matches from the database"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT id, team1P1Id, team1P2Id, team2P1Id, team2P2Id,
|
|
||||||
team1Score, team2Score, playedAt
|
|
||||||
FROM matches
|
|
||||||
ORDER BY playedAt
|
|
||||||
""")
|
|
||||||
matches = cursor.fetchall()
|
|
||||||
conn.close()
|
|
||||||
return matches
|
|
||||||
|
|
||||||
|
|
||||||
def get_player(player_id):
|
|
||||||
"""Get a player by ID"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(
|
|
||||||
"SELECT id, name, currentElo, gamesPlayed, wins, losses FROM players WHERE id = ?",
|
|
||||||
(player_id,),
|
|
||||||
)
|
|
||||||
player = cursor.fetchone()
|
|
||||||
conn.close()
|
|
||||||
return player
|
|
||||||
|
|
||||||
|
|
||||||
def update_player(player_id, current_elo, games_played, wins, losses):
|
|
||||||
"""Update a player's statistics"""
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(
|
|
||||||
"""
|
|
||||||
UPDATE players
|
|
||||||
SET currentElo = ?, gamesPlayed = ?, wins = ?, losses = ?
|
|
||||||
WHERE id = ?
|
|
||||||
""",
|
|
||||||
(current_elo, games_played, wins, losses, player_id),
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
def calculate_elo_change(rating_a, rating_b, score_a, score_b):
|
|
||||||
"""Calculate Elo change for a match"""
|
|
||||||
# Calculate expected scores
|
|
||||||
expected_a = 1 / (1 + math.pow(10, (rating_b - rating_a) / 400))
|
|
||||||
expected_b = 1 - expected_a
|
|
||||||
|
|
||||||
# Actual scores (1 for win, 0.5 for tie, 0 for loss)
|
|
||||||
actual_a = 0.5 if score_a == score_b else (1 if score_a > score_b else 0)
|
|
||||||
actual_b = 0.5 if score_a == score_b else (1 if score_b > score_a else 0)
|
|
||||||
|
|
||||||
# Calculate Elo change
|
|
||||||
elo_change_a = K_FACTOR * (actual_a - expected_a)
|
|
||||||
elo_change_b = K_FACTOR * (actual_b - expected_b)
|
|
||||||
|
|
||||||
return elo_change_a, elo_change_b
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
print("Updating player statistics based on matches in database...")
|
|
||||||
print("=" * 60)
|
|
||||||
|
|
||||||
# Get all matches
|
|
||||||
matches = get_all_matches()
|
|
||||||
print(f"Found {len(matches)} matches in database")
|
|
||||||
|
|
||||||
# Reset all player stats to 0 before recalculating
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute(
|
|
||||||
"UPDATE players SET currentElo = 1000, gamesPlayed = 0, wins = 0, losses = 0"
|
|
||||||
)
|
|
||||||
conn.commit()
|
|
||||||
conn.close()
|
|
||||||
print("Reset all player statistics to initial values (ELO: 1000, games: 0)")
|
|
||||||
|
|
||||||
# Process each match
|
|
||||||
match_count = 0
|
|
||||||
for (
|
|
||||||
match_id,
|
|
||||||
team1_p1,
|
|
||||||
team1_p2,
|
|
||||||
team2_p1,
|
|
||||||
team2_p2,
|
|
||||||
team1_score,
|
|
||||||
team2_score,
|
|
||||||
played_at,
|
|
||||||
) in matches:
|
|
||||||
# Get player data
|
|
||||||
p1 = get_player(team1_p1)
|
|
||||||
p2 = get_player(team1_p2)
|
|
||||||
p3 = get_player(team2_p1)
|
|
||||||
p4 = get_player(team2_p2)
|
|
||||||
|
|
||||||
if not all([p1, p2, p3, p4]):
|
|
||||||
print(f"Warning: Could not find all players for match {match_id}")
|
|
||||||
continue
|
|
||||||
|
|
||||||
# Calculate team ratings
|
|
||||||
team1_rating = (p1[2] + p2[2]) / 2 # currentElo
|
|
||||||
team2_rating = (p3[2] + p4[2]) / 2 # currentElo
|
|
||||||
|
|
||||||
# Calculate Elo changes
|
|
||||||
team1_elo_change, team2_elo_change = calculate_elo_change(
|
|
||||||
team1_rating, team2_rating, team1_score, team2_score
|
|
||||||
)
|
|
||||||
|
|
||||||
# Individual Elo changes (split evenly between team members)
|
|
||||||
p1_elo_change = team1_elo_change / 2
|
|
||||||
p2_elo_change = team1_elo_change / 2
|
|
||||||
p3_elo_change = team2_elo_change / 2
|
|
||||||
p4_elo_change = team2_elo_change / 2
|
|
||||||
|
|
||||||
# Determine winners
|
|
||||||
team1_won = team1_score > team2_score
|
|
||||||
team2_won = team2_score > team1_score
|
|
||||||
|
|
||||||
# Update player 1 stats
|
|
||||||
p1_new_elo = int(p1[2] + p1_elo_change)
|
|
||||||
p1_new_games = p1[3] + 1
|
|
||||||
p1_new_wins = p1[4] + 1 if team1_won else p1[4]
|
|
||||||
p1_new_losses = p1[5] if team1_won else p1[5] + 1
|
|
||||||
update_player(p1[0], p1_new_elo, p1_new_games, p1_new_wins, p1_new_losses)
|
|
||||||
|
|
||||||
# Update player 2 stats
|
|
||||||
p2_new_elo = int(p2[2] + p2_elo_change)
|
|
||||||
p2_new_games = p2[3] + 1
|
|
||||||
p2_new_wins = p2[4] + 1 if team1_won else p2[4]
|
|
||||||
p2_new_losses = p2[5] if team1_won else p2[5] + 1
|
|
||||||
update_player(p2[0], p2_new_elo, p2_new_games, p2_new_wins, p2_new_losses)
|
|
||||||
|
|
||||||
# Update player 3 stats
|
|
||||||
p3_new_elo = int(p3[2] + p3_elo_change)
|
|
||||||
p3_new_games = p3[3] + 1
|
|
||||||
p3_new_wins = p3[4] + 1 if team2_won else p3[4]
|
|
||||||
p3_new_losses = p3[5] if team2_won else p3[5] + 1
|
|
||||||
update_player(p3[0], p3_new_elo, p3_new_games, p3_new_wins, p3_new_losses)
|
|
||||||
|
|
||||||
# Update player 4 stats
|
|
||||||
p4_new_elo = int(p4[2] + p4_elo_change)
|
|
||||||
p4_new_games = p4[3] + 1
|
|
||||||
p4_new_wins = p4[4] + 1 if team2_won else p4[4]
|
|
||||||
p4_new_losses = p4[5] if team2_won else p4[5] + 1
|
|
||||||
update_player(p4[0], p4_new_elo, p4_new_games, p4_new_wins, p4_new_losses)
|
|
||||||
|
|
||||||
match_count += 1
|
|
||||||
if match_count % 20 == 0:
|
|
||||||
print(f"Processed {match_count}/{len(matches)} matches...")
|
|
||||||
|
|
||||||
print(f"Processed {match_count} matches")
|
|
||||||
|
|
||||||
# Display updated player rankings
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Top 10 Players by ELO Rating:")
|
|
||||||
print("-" * 60)
|
|
||||||
|
|
||||||
conn = sqlite3.connect(DB_PATH)
|
|
||||||
cursor = conn.cursor()
|
|
||||||
cursor.execute("""
|
|
||||||
SELECT id, name, currentElo, gamesPlayed, wins, losses
|
|
||||||
FROM players
|
|
||||||
ORDER BY currentElo DESC
|
|
||||||
LIMIT 10
|
|
||||||
""")
|
|
||||||
top_players = cursor.fetchall()
|
|
||||||
conn.close()
|
|
||||||
|
|
||||||
for rank, (player_id, name, elo, games, wins, losses) in enumerate(top_players, 1):
|
|
||||||
win_rate = (wins / games * 100) if games > 0 else 0
|
|
||||||
print(
|
|
||||||
f"{rank:2}. {name:15} | ELO: {elo:4} | Games: {games:3} | W/L: {wins}/{losses} ({win_rate:.1f}%)"
|
|
||||||
)
|
|
||||||
|
|
||||||
print("\n" + "=" * 60)
|
|
||||||
print("Player statistics updated successfully!")
|
|
||||||
print("Now run the application to see updated rankings.")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
main()
|
|
||||||
Reference in New Issue
Block a user