chore: clean up repo and fix docker compose configuration
- Remove outdated docs (COMPOSE.md, DOCKER.md, QUICKSTART.md, REBUILD_GUIDE.md) - Remove build.sh, compose.production.yml, gamification-schema.sql, directus.yaml - Simplify compose.yml for local dev (remove env var indirection) - Add directus.yml schema snapshot and schema.sql from VPS - Add schema:export and schema:import scripts to package.json - Ignore .env files (vars set via compose environment) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
3
.gitignore
vendored
3
.gitignore
vendored
@@ -3,5 +3,8 @@ dist/
|
|||||||
target/
|
target/
|
||||||
pkg/
|
pkg/
|
||||||
|
|
||||||
|
.env
|
||||||
.env.*
|
.env.*
|
||||||
|
|
||||||
|
|
||||||
|
.claude/
|
||||||
|
|||||||
424
COMPOSE.md
424
COMPOSE.md
@@ -1,424 +0,0 @@
|
|||||||
# Docker Compose Guide
|
|
||||||
|
|
||||||
This guide explains the Docker Compose setup for sexy.pivoine.art with local development and production configurations.
|
|
||||||
|
|
||||||
## Architecture Overview
|
|
||||||
|
|
||||||
The application uses a **multi-file compose setup** with two configurations:
|
|
||||||
|
|
||||||
1. **`compose.yml`** - Base configuration for local development
|
|
||||||
2. **`compose.production.yml`** - Production overrides with Traefik integration
|
|
||||||
|
|
||||||
### Service Architecture
|
|
||||||
|
|
||||||
```
|
|
||||||
┌─────────────────────────────────────────────────────────────┐
|
|
||||||
│ 🌐 Traefik Reverse Proxy (Production Only) │
|
|
||||||
│ ├─ HTTPS Termination │
|
|
||||||
│ ├─ Automatic Let's Encrypt │
|
|
||||||
│ └─ Routes traffic to frontend & Directus API │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ 💄 Frontend (SvelteKit) │
|
|
||||||
│ ├─ Port 3000 (internal) │
|
|
||||||
│ ├─ Serves on https://sexy.pivoine.art │
|
|
||||||
│ └─ Proxies /api to Directus │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ 🎭 Directus CMS │
|
|
||||||
│ ├─ Port 8055 (internal) │
|
|
||||||
│ ├─ Serves on https://sexy.pivoine.art/api │
|
|
||||||
│ ├─ Custom bundle extensions mounted │
|
|
||||||
│ └─ Uploads volume │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ 🗄️ PostgreSQL (Local) / External (Production) │
|
|
||||||
│ └─ Database for Directus │
|
|
||||||
├─────────────────────────────────────────────────────────────┤
|
|
||||||
│ 💾 Redis (Local) / External (Production) │
|
|
||||||
│ └─ Cache & session storage │
|
|
||||||
└─────────────────────────────────────────────────────────────┘
|
|
||||||
```
|
|
||||||
|
|
||||||
## Local Development Setup
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- Docker 20.10+
|
|
||||||
- Docker Compose 2.0+
|
|
||||||
|
|
||||||
### Quick Start
|
|
||||||
|
|
||||||
1. **Create environment file:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
# Edit .env with your local settings (defaults work fine)
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Start all services:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Access services:**
|
|
||||||
- Frontend: http://localhost:3000 (if enabled)
|
|
||||||
- Directus: http://localhost:8055
|
|
||||||
- Directus Admin: http://localhost:8055/admin
|
|
||||||
|
|
||||||
4. **View logs:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
5. **Stop services:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local Services
|
|
||||||
|
|
||||||
#### PostgreSQL
|
|
||||||
- **Image:** `postgres:16-alpine`
|
|
||||||
- **Port:** 5432 (internal only)
|
|
||||||
- **Volume:** `postgres-data`
|
|
||||||
- **Database:** `sexy`
|
|
||||||
|
|
||||||
#### Redis
|
|
||||||
- **Image:** `redis:7-alpine`
|
|
||||||
- **Port:** 6379 (internal only)
|
|
||||||
- **Volume:** `redis-data`
|
|
||||||
- **Persistence:** AOF enabled
|
|
||||||
|
|
||||||
#### Directus
|
|
||||||
- **Image:** `directus/directus:11`
|
|
||||||
- **Port:** 8055 (exposed)
|
|
||||||
- **Volumes:**
|
|
||||||
- `directus-uploads` - File uploads
|
|
||||||
- `./packages/bundle/dist` - Custom extensions
|
|
||||||
- **Features:**
|
|
||||||
- Auto-reload extensions
|
|
||||||
- WebSockets enabled
|
|
||||||
- CORS enabled for localhost
|
|
||||||
|
|
||||||
### Local Development Workflow
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Start infrastructure (Postgres, Redis, Directus)
|
|
||||||
docker-compose up -d
|
|
||||||
|
|
||||||
# Develop frontend locally with hot reload
|
|
||||||
cd packages/frontend
|
|
||||||
pnpm dev
|
|
||||||
|
|
||||||
# Build Directus bundle
|
|
||||||
pnpm --filter @sexy.pivoine.art/bundle build
|
|
||||||
|
|
||||||
# Restart Directus to load new bundle
|
|
||||||
docker-compose restart directus
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
### Prerequisites
|
|
||||||
|
|
||||||
- External PostgreSQL database
|
|
||||||
- External Redis instance
|
|
||||||
- Traefik reverse proxy configured
|
|
||||||
- External network: `compose_network`
|
|
||||||
|
|
||||||
### Setup
|
|
||||||
|
|
||||||
The production compose file now uses the `include` directive to automatically extend `compose.yml`, making deployment simpler.
|
|
||||||
|
|
||||||
1. **Create production environment file:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.production.example .env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Edit `.env.production` with your values:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Database (external)
|
|
||||||
CORE_DB_HOST=your-postgres-host
|
|
||||||
SEXY_DB_NAME=sexy_production
|
|
||||||
DB_USER=sexy
|
|
||||||
DB_PASSWORD=your-secure-password
|
|
||||||
|
|
||||||
# Redis (external)
|
|
||||||
CORE_REDIS_HOST=your-redis-host
|
|
||||||
|
|
||||||
# Directus
|
|
||||||
SEXY_DIRECTUS_SECRET=your-32-char-random-secret
|
|
||||||
ADMIN_PASSWORD=your-secure-admin-password
|
|
||||||
|
|
||||||
# Traefik
|
|
||||||
SEXY_TRAEFIK_HOST=sexy.pivoine.art
|
|
||||||
|
|
||||||
# Frontend
|
|
||||||
PUBLIC_API_URL=https://sexy.pivoine.art/api
|
|
||||||
PUBLIC_URL=https://sexy.pivoine.art
|
|
||||||
|
|
||||||
# Email (SMTP)
|
|
||||||
EMAIL_SMTP_HOST=smtp.your-provider.com
|
|
||||||
EMAIL_SMTP_USER=your-email@domain.com
|
|
||||||
EMAIL_SMTP_PASSWORD=your-smtp-password
|
|
||||||
```
|
|
||||||
|
|
||||||
3. **Deploy:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Simple deployment - compose.production.yml includes compose.yml automatically
|
|
||||||
docker-compose -f compose.production.yml --env-file .env.production up -d
|
|
||||||
|
|
||||||
# Or use the traditional multi-file approach (same result)
|
|
||||||
docker-compose -f compose.yml -f compose.production.yml --env-file .env.production up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production Services
|
|
||||||
|
|
||||||
#### Directus
|
|
||||||
- **Image:** `directus/directus:11` (configurable)
|
|
||||||
- **Network:** `compose_network` (external)
|
|
||||||
- **Volumes:**
|
|
||||||
- `/var/www/sexy.pivoine.art/uploads` - Persistent uploads
|
|
||||||
- `/var/www/sexy.pivoine.art/packages/bundle/dist` - Extensions
|
|
||||||
- **Traefik routing:**
|
|
||||||
- Domain: `sexy.pivoine.art/api`
|
|
||||||
- Strips `/api` prefix before forwarding
|
|
||||||
- HTTPS with auto-certificates
|
|
||||||
|
|
||||||
#### Frontend
|
|
||||||
- **Image:** `ghcr.io/valknarxxx/sexy:latest` (from GHCR)
|
|
||||||
- **Network:** `compose_network` (external)
|
|
||||||
- **Volume:** `/var/www/sexy.pivoine.art` - Application code
|
|
||||||
- **Traefik routing:**
|
|
||||||
- Domain: `sexy.pivoine.art`
|
|
||||||
- HTTPS with auto-certificates
|
|
||||||
|
|
||||||
### Traefik Integration
|
|
||||||
|
|
||||||
Both services are configured with Traefik labels for automatic routing:
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- HTTP → HTTPS redirect
|
|
||||||
- Routes `sexy.pivoine.art` to port 3000
|
|
||||||
- Gzip compression enabled
|
|
||||||
|
|
||||||
**Directus API:**
|
|
||||||
- HTTP → HTTPS redirect
|
|
||||||
- Routes `sexy.pivoine.art/api` to port 8055
|
|
||||||
- Strips `/api` prefix
|
|
||||||
- Gzip compression enabled
|
|
||||||
|
|
||||||
### Production Commands
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Deploy/update (simplified - uses include)
|
|
||||||
docker-compose -f compose.production.yml --env-file .env.production up -d
|
|
||||||
|
|
||||||
# View logs
|
|
||||||
docker-compose -f compose.production.yml logs -f
|
|
||||||
|
|
||||||
# Restart specific service
|
|
||||||
docker-compose -f compose.production.yml restart frontend
|
|
||||||
|
|
||||||
# Stop all services
|
|
||||||
docker-compose -f compose.production.yml down
|
|
||||||
|
|
||||||
# Update images
|
|
||||||
docker-compose -f compose.production.yml pull
|
|
||||||
docker-compose -f compose.production.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### Local Development (`.env`)
|
|
||||||
|
|
||||||
| Variable | Default | Description |
|
|
||||||
|----------|---------|-------------|
|
|
||||||
| `DB_DATABASE` | `sexy` | Database name |
|
|
||||||
| `DB_USER` | `sexy` | Database user |
|
|
||||||
| `DB_PASSWORD` | `sexy` | Database password |
|
|
||||||
| `DIRECTUS_SECRET` | - | Secret for Directus (min 32 chars) |
|
|
||||||
| `ADMIN_EMAIL` | `admin@sexy.pivoine.art` | Admin email |
|
|
||||||
| `ADMIN_PASSWORD` | `admin` | Admin password |
|
|
||||||
| `CORS_ORIGIN` | `http://localhost:3000` | CORS allowed origins |
|
|
||||||
|
|
||||||
See `.env.example` for full list.
|
|
||||||
|
|
||||||
### Production (`.env.production`)
|
|
||||||
|
|
||||||
| Variable | Description | Required |
|
|
||||||
|----------|-------------|----------|
|
|
||||||
| `CORE_DB_HOST` | External PostgreSQL host | ✅ |
|
|
||||||
| `SEXY_DB_NAME` | Database name | ✅ |
|
|
||||||
| `DB_PASSWORD` | Database password | ✅ |
|
|
||||||
| `CORE_REDIS_HOST` | External Redis host | ✅ |
|
|
||||||
| `SEXY_DIRECTUS_SECRET` | Directus secret key | ✅ |
|
|
||||||
| `SEXY_TRAEFIK_HOST` | Domain name | ✅ |
|
|
||||||
| `EMAIL_SMTP_HOST` | SMTP server | ✅ |
|
|
||||||
| `EMAIL_SMTP_PASSWORD` | SMTP password | ✅ |
|
|
||||||
| `SEXY_FRONTEND_PUBLIC_API_URL` | Frontend API URL | ✅ |
|
|
||||||
| `SEXY_FRONTEND_PUBLIC_URL` | Frontend public URL | ✅ |
|
|
||||||
|
|
||||||
See `.env.production.example` for full list.
|
|
||||||
|
|
||||||
**Note:** All frontend-specific variables are prefixed with `SEXY_FRONTEND_` for clarity.
|
|
||||||
|
|
||||||
## Volumes
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
- `postgres-data` - PostgreSQL database
|
|
||||||
- `redis-data` - Redis persistence
|
|
||||||
- `directus-uploads` - Uploaded files
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
- `/var/www/sexy.pivoine.art/uploads` - Directus uploads
|
|
||||||
- `/var/www/sexy.pivoine.art` - Application code (frontend)
|
|
||||||
|
|
||||||
## Networks
|
|
||||||
|
|
||||||
### Local: `sexy-network`
|
|
||||||
- Bridge network
|
|
||||||
- Internal communication only
|
|
||||||
- Directus exposed on 8055
|
|
||||||
|
|
||||||
### Production: `compose_network`
|
|
||||||
- External network (pre-existing)
|
|
||||||
- Connects to Traefik
|
|
||||||
- No exposed ports (Traefik handles routing)
|
|
||||||
|
|
||||||
## Health Checks
|
|
||||||
|
|
||||||
All services include health checks:
|
|
||||||
|
|
||||||
**PostgreSQL:**
|
|
||||||
- Command: `pg_isready`
|
|
||||||
- Interval: 10s
|
|
||||||
|
|
||||||
**Redis:**
|
|
||||||
- Command: `redis-cli ping`
|
|
||||||
- Interval: 10s
|
|
||||||
|
|
||||||
**Directus:**
|
|
||||||
- Endpoint: `/server/health`
|
|
||||||
- Interval: 30s
|
|
||||||
|
|
||||||
**Frontend:**
|
|
||||||
- HTTP GET: `localhost:3000`
|
|
||||||
- Interval: 30s
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
|
|
||||||
**Problem:** Directus won't start
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check logs
|
|
||||||
docker-compose logs directus
|
|
||||||
|
|
||||||
# Common issues:
|
|
||||||
# 1. Database not ready - wait for postgres to be healthy
|
|
||||||
# 2. Wrong secret - check DIRECTUS_SECRET is at least 32 chars
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Can't connect to database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if postgres is running
|
|
||||||
docker-compose ps postgres
|
|
||||||
|
|
||||||
# Verify health
|
|
||||||
docker-compose exec postgres pg_isready -U sexy
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Extensions not loading
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Rebuild bundle
|
|
||||||
pnpm --filter @sexy.pivoine.art/bundle build
|
|
||||||
|
|
||||||
# Verify volume mount
|
|
||||||
docker-compose exec directus ls -la /directus/extensions/
|
|
||||||
|
|
||||||
# Restart Directus
|
|
||||||
docker-compose restart directus
|
|
||||||
```
|
|
||||||
|
|
||||||
### Production
|
|
||||||
|
|
||||||
**Problem:** Services not accessible via domain
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check Traefik labels
|
|
||||||
docker inspect sexy_frontend | grep traefik
|
|
||||||
|
|
||||||
# Verify compose_network exists
|
|
||||||
docker network ls | grep compose_network
|
|
||||||
|
|
||||||
# Check Traefik is running
|
|
||||||
docker ps | grep traefik
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Can't connect to external database
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Test connection from Directus container
|
|
||||||
docker-compose exec directus sh
|
|
||||||
apk add postgresql-client
|
|
||||||
psql -h $CORE_DB_HOST -U $DB_USER -d $SEXY_DB_NAME
|
|
||||||
```
|
|
||||||
|
|
||||||
**Problem:** Frontend can't reach Directus API
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check Directus is accessible
|
|
||||||
curl https://sexy.pivoine.art/api/server/health
|
|
||||||
|
|
||||||
# Verify CORS settings
|
|
||||||
# PUBLIC_API_URL should match the public Directus URL
|
|
||||||
```
|
|
||||||
|
|
||||||
## Migration from Old Setup
|
|
||||||
|
|
||||||
If migrating from `docker-compose.production.yml`:
|
|
||||||
|
|
||||||
1. **Rename environment variables** according to `.env.production.example`
|
|
||||||
2. **Update command** to use both compose files
|
|
||||||
3. **Verify Traefik labels** match your setup
|
|
||||||
4. **Test** with `docker-compose config` to see merged configuration
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Validate configuration
|
|
||||||
docker-compose -f compose.yml -f compose.production.yml --env-file .env.production config
|
|
||||||
|
|
||||||
# Deploy
|
|
||||||
docker-compose -f compose.yml -f compose.production.yml --env-file .env.production up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
## Best Practices
|
|
||||||
|
|
||||||
### Local Development
|
|
||||||
1. Use default credentials (they're fine for local)
|
|
||||||
2. Keep `EXTENSIONS_AUTO_RELOAD=true` for quick iteration
|
|
||||||
3. Run frontend via `pnpm dev` for hot reload
|
|
||||||
4. Restart Directus after bundle changes
|
|
||||||
|
|
||||||
### Production
|
|
||||||
1. Use strong passwords for database and admin
|
|
||||||
2. Set `EXTENSIONS_AUTO_RELOAD=false` for stability
|
|
||||||
3. Use GHCR images for frontend
|
|
||||||
4. Enable Gzip compression via Traefik
|
|
||||||
5. Monitor logs regularly
|
|
||||||
6. Keep backups of uploads and database
|
|
||||||
|
|
||||||
## See Also
|
|
||||||
|
|
||||||
- [DOCKER.md](DOCKER.md) - Docker image documentation
|
|
||||||
- [QUICKSTART.md](QUICKSTART.md) - Quick start guide
|
|
||||||
- [CLAUDE.md](CLAUDE.md) - Development guide
|
|
||||||
374
DOCKER.md
374
DOCKER.md
@@ -1,374 +0,0 @@
|
|||||||
# Docker Deployment Guide
|
|
||||||
|
|
||||||
This guide covers building and deploying sexy.pivoine.art using Docker.
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
The Dockerfile uses a multi-stage build process:
|
|
||||||
|
|
||||||
1. **Base stage**: Sets up Node.js and pnpm
|
|
||||||
2. **Builder stage**: Installs Rust, compiles WASM, builds all packages
|
|
||||||
3. **Runner stage**: Minimal production image with only runtime dependencies
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker 20.10+ with BuildKit support
|
|
||||||
- Docker Compose 2.0+ (optional, for orchestration)
|
|
||||||
|
|
||||||
## Building the Image
|
|
||||||
|
|
||||||
### Basic Build
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build -t sexy.pivoine.art:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build with Build Arguments
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker build \
|
|
||||||
--build-arg NODE_ENV=production \
|
|
||||||
-t sexy.pivoine.art:latest \
|
|
||||||
.
|
|
||||||
```
|
|
||||||
|
|
||||||
### Multi-platform Build (for ARM64 and AMD64)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker buildx build \
|
|
||||||
--platform linux/amd64,linux/arm64 \
|
|
||||||
-t sexy.pivoine.art:latest \
|
|
||||||
--push \
|
|
||||||
.
|
|
||||||
```
|
|
||||||
|
|
||||||
## Running the Container
|
|
||||||
|
|
||||||
### Run with Environment Variables
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name sexy-pivoine-frontend \
|
|
||||||
-p 3000:3000 \
|
|
||||||
-e PUBLIC_API_URL=https://api.pivoine.art \
|
|
||||||
-e PUBLIC_URL=https://sexy.pivoine.art \
|
|
||||||
-e PUBLIC_UMAMI_ID=your-umami-id \
|
|
||||||
-e PUBLIC_UMAMI_SCRIPT=https://umami.pivoine.art/script.js \
|
|
||||||
sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Run with Environment File
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create .env.production from template
|
|
||||||
cp .env.production.example .env.production
|
|
||||||
|
|
||||||
# Edit .env.production with your values
|
|
||||||
nano .env.production
|
|
||||||
|
|
||||||
# Run container
|
|
||||||
docker run -d \
|
|
||||||
--name sexy-pivoine-frontend \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--env-file .env.production \
|
|
||||||
sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Docker Compose Deployment
|
|
||||||
|
|
||||||
### Using docker-compose.production.yml
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# 1. Create environment file
|
|
||||||
cp .env.production.example .env.production
|
|
||||||
|
|
||||||
# 2. Edit environment variables
|
|
||||||
nano .env.production
|
|
||||||
|
|
||||||
# 3. Build and start
|
|
||||||
docker-compose -f docker-compose.production.yml up -d --build
|
|
||||||
|
|
||||||
# 4. View logs
|
|
||||||
docker-compose -f docker-compose.production.yml logs -f frontend
|
|
||||||
|
|
||||||
# 5. Stop services
|
|
||||||
docker-compose -f docker-compose.production.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Scale the Application
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.production.yml up -d --scale frontend=3
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables Reference
|
|
||||||
|
|
||||||
### Required Variables
|
|
||||||
|
|
||||||
| Variable | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `PUBLIC_API_URL` | Directus API backend URL | `https://api.pivoine.art` |
|
|
||||||
| `PUBLIC_URL` | Frontend application URL | `https://sexy.pivoine.art` |
|
|
||||||
|
|
||||||
### Optional Variables
|
|
||||||
|
|
||||||
| Variable | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `PUBLIC_UMAMI_ID` | Umami analytics tracking ID | `abc123def-456` |
|
|
||||||
| `PUBLIC_UMAMI_SCRIPT` | Umami script URL | `https://umami.pivoine.art/script.js` |
|
|
||||||
| `PORT` | Application port (inside container) | `3000` |
|
|
||||||
| `HOST` | Host binding | `0.0.0.0` |
|
|
||||||
| `NODE_ENV` | Node environment | `production` |
|
|
||||||
|
|
||||||
## Health Checks
|
|
||||||
|
|
||||||
The container includes a built-in health check that pings the HTTP server every 30 seconds:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check container health
|
|
||||||
docker inspect --format='{{.State.Health.Status}}' sexy-pivoine-frontend
|
|
||||||
|
|
||||||
# View health check logs
|
|
||||||
docker inspect --format='{{json .State.Health}}' sexy-pivoine-frontend | jq
|
|
||||||
```
|
|
||||||
|
|
||||||
## Logs and Debugging
|
|
||||||
|
|
||||||
### View Container Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Follow logs
|
|
||||||
docker logs -f sexy-pivoine-frontend
|
|
||||||
|
|
||||||
# Last 100 lines
|
|
||||||
docker logs --tail 100 sexy-pivoine-frontend
|
|
||||||
|
|
||||||
# With timestamps
|
|
||||||
docker logs -f --timestamps sexy-pivoine-frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
### Execute Commands in Running Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Open shell
|
|
||||||
docker exec -it sexy-pivoine-frontend sh
|
|
||||||
|
|
||||||
# Check Node.js version
|
|
||||||
docker exec sexy-pivoine-frontend node --version
|
|
||||||
|
|
||||||
# Check environment variables
|
|
||||||
docker exec sexy-pivoine-frontend env
|
|
||||||
```
|
|
||||||
|
|
||||||
### Debug Build Issues
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Build with no cache
|
|
||||||
docker build --no-cache -t sexy.pivoine.art:latest .
|
|
||||||
|
|
||||||
# Build specific stage for debugging
|
|
||||||
docker build --target builder -t sexy.pivoine.art:builder .
|
|
||||||
|
|
||||||
# Inspect builder stage
|
|
||||||
docker run -it --rm sexy.pivoine.art:builder sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production Best Practices
|
|
||||||
|
|
||||||
### 1. Use Specific Tags
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Tag with version
|
|
||||||
docker build -t sexy.pivoine.art:1.0.0 .
|
|
||||||
docker tag sexy.pivoine.art:1.0.0 sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Image Scanning
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Scan for vulnerabilities (requires Docker Scout or Trivy)
|
|
||||||
docker scout cves sexy.pivoine.art:latest
|
|
||||||
|
|
||||||
# Or with Trivy
|
|
||||||
trivy image sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Resource Limits
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name sexy-pivoine-frontend \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--memory="2g" \
|
|
||||||
--cpus="2" \
|
|
||||||
--env-file .env.production \
|
|
||||||
sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Restart Policies
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name sexy-pivoine-frontend \
|
|
||||||
--restart=unless-stopped \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--env-file .env.production \
|
|
||||||
sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. Use Docker Secrets (Docker Swarm)
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Create secrets
|
|
||||||
echo "your-db-password" | docker secret create db_password -
|
|
||||||
|
|
||||||
# Deploy with secrets
|
|
||||||
docker service create \
|
|
||||||
--name sexy-pivoine-frontend \
|
|
||||||
--secret db_password \
|
|
||||||
-p 3000:3000 \
|
|
||||||
sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Optimization Tips
|
|
||||||
|
|
||||||
### Reduce Build Time
|
|
||||||
|
|
||||||
1. **Use BuildKit cache mounts** (already enabled in Dockerfile)
|
|
||||||
2. **Leverage layer caching** - structure Dockerfile to cache dependencies
|
|
||||||
3. **Use `.dockerignore`** - exclude unnecessary files from build context
|
|
||||||
|
|
||||||
### Reduce Image Size
|
|
||||||
|
|
||||||
Current optimizations:
|
|
||||||
- Multi-stage build (builder artifacts not in final image)
|
|
||||||
- Production-only dependencies (`pnpm install --prod`)
|
|
||||||
- Minimal base image (`node:20.19.1-slim`)
|
|
||||||
- Only necessary build artifacts copied to runner
|
|
||||||
|
|
||||||
Image size breakdown:
|
|
||||||
```bash
|
|
||||||
docker images sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## CI/CD Integration
|
|
||||||
|
|
||||||
### GitHub Actions (Automated)
|
|
||||||
|
|
||||||
This repository includes automated GitHub Actions workflows for building, scanning, and managing Docker images.
|
|
||||||
|
|
||||||
**Pre-configured workflows:**
|
|
||||||
- **Build & Push** (`.github/workflows/docker-build-push.yml`)
|
|
||||||
- Automatically builds and pushes to `ghcr.io/valknarxxx/sexy`
|
|
||||||
- Triggers on push to main/develop, version tags, and PRs
|
|
||||||
- Multi-platform builds (AMD64 + ARM64)
|
|
||||||
- Smart tagging: latest, branch names, semver, commit SHAs
|
|
||||||
|
|
||||||
- **Security Scan** (`.github/workflows/docker-scan.yml`)
|
|
||||||
- Daily vulnerability scans with Trivy
|
|
||||||
- Reports to GitHub Security tab
|
|
||||||
- Scans on every release
|
|
||||||
|
|
||||||
- **Cleanup** (`.github/workflows/cleanup-images.yml`)
|
|
||||||
- Weekly cleanup of old untagged images
|
|
||||||
- Keeps last 10 versions
|
|
||||||
|
|
||||||
**Using pre-built images:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull latest from GitHub Container Registry
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:latest
|
|
||||||
|
|
||||||
# Pull specific version
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:v1.0.0
|
|
||||||
|
|
||||||
# Run the image
|
|
||||||
docker run -d -p 3000:3000 --env-file .env.production ghcr.io/valknarxxx/sexy:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
**Triggering builds:**
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Push to main → builds 'latest' tag
|
|
||||||
git push origin main
|
|
||||||
|
|
||||||
# Create version tag → builds semver tags
|
|
||||||
git tag v1.0.0 && git push origin v1.0.0
|
|
||||||
|
|
||||||
# Pull request → builds but doesn't push
|
|
||||||
```
|
|
||||||
|
|
||||||
See `.github/workflows/README.md` for detailed workflow documentation.
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Build Fails at Rust Installation
|
|
||||||
|
|
||||||
**Problem**: Rust installation fails or times out
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
- Check internet connectivity
|
|
||||||
- Use a Rust mirror if in restricted network
|
|
||||||
- Increase build timeout
|
|
||||||
|
|
||||||
### WASM Build Fails
|
|
||||||
|
|
||||||
**Problem**: `wasm-bindgen-cli` version mismatch
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```dockerfile
|
|
||||||
# In Dockerfile, pin wasm-bindgen-cli version
|
|
||||||
RUN cargo install wasm-bindgen-cli --version 0.2.103
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container Exits Immediately
|
|
||||||
|
|
||||||
**Problem**: Container starts then exits
|
|
||||||
|
|
||||||
**Solution**: Check logs and verify:
|
|
||||||
```bash
|
|
||||||
docker logs sexy-pivoine-frontend
|
|
||||||
|
|
||||||
# Verify build output exists
|
|
||||||
docker run -it --rm sexy.pivoine.art:latest ls -la packages/frontend/build
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port Already in Use
|
|
||||||
|
|
||||||
**Problem**: Port 3000 already bound
|
|
||||||
|
|
||||||
**Solution**:
|
|
||||||
```bash
|
|
||||||
# Use different host port
|
|
||||||
docker run -d -p 8080:3000 sexy.pivoine.art:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Maintenance
|
|
||||||
|
|
||||||
### Clean Up
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Remove stopped containers
|
|
||||||
docker container prune
|
|
||||||
|
|
||||||
# Remove unused images
|
|
||||||
docker image prune -a
|
|
||||||
|
|
||||||
# Remove build cache
|
|
||||||
docker builder prune
|
|
||||||
|
|
||||||
# Complete cleanup (use with caution)
|
|
||||||
docker system prune -a --volumes
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update Base Image
|
|
||||||
|
|
||||||
Regularly update the base Node.js image:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Pull latest Node 20 LTS
|
|
||||||
docker pull node:20.19.1-slim
|
|
||||||
|
|
||||||
# Rebuild
|
|
||||||
docker build --pull -t sexy.pivoine.art:latest .
|
|
||||||
```
|
|
||||||
330
QUICKSTART.md
330
QUICKSTART.md
@@ -1,330 +0,0 @@
|
|||||||
# Quick Start Guide
|
|
||||||
|
|
||||||
Get sexy.pivoine.art running in under 5 minutes using pre-built Docker images.
|
|
||||||
|
|
||||||
## Prerequisites
|
|
||||||
|
|
||||||
- Docker 20.10+
|
|
||||||
- Docker Compose 2.0+ (optional)
|
|
||||||
|
|
||||||
## Option 1: Docker Run (Fastest)
|
|
||||||
|
|
||||||
### Step 1: Pull the Image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Create Environment File
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cat > .env.production << EOF
|
|
||||||
PUBLIC_API_URL=https://api.your-domain.com
|
|
||||||
PUBLIC_URL=https://your-domain.com
|
|
||||||
PUBLIC_UMAMI_ID=
|
|
||||||
PUBLIC_UMAMI_SCRIPT=
|
|
||||||
EOF
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Run the Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name sexy-pivoine \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--env-file .env.production \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/valknarxxx/sexy:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Verify
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if running
|
|
||||||
docker ps | grep sexy-pivoine
|
|
||||||
|
|
||||||
# Check logs
|
|
||||||
docker logs -f sexy-pivoine
|
|
||||||
|
|
||||||
# Test the application
|
|
||||||
curl http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
Your application is now running at `http://localhost:3000` 🎉
|
|
||||||
|
|
||||||
## Option 2: Docker Compose (Recommended)
|
|
||||||
|
|
||||||
### Step 1: Download docker-compose.production.yml
|
|
||||||
|
|
||||||
```bash
|
|
||||||
curl -O https://raw.githubusercontent.com/valknarxxx/sexy/main/docker-compose.production.yml
|
|
||||||
```
|
|
||||||
|
|
||||||
Or if you have the repository:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd /path/to/sexy.pivoine.art
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 2: Create Environment File
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cp .env.production.example .env.production
|
|
||||||
nano .env.production # Edit with your values
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Start Services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker-compose -f docker-compose.production.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 4: Monitor
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# View logs
|
|
||||||
docker-compose -f docker-compose.production.yml logs -f
|
|
||||||
|
|
||||||
# Check status
|
|
||||||
docker-compose -f docker-compose.production.yml ps
|
|
||||||
```
|
|
||||||
|
|
||||||
Your application is now running at `http://localhost:3000` 🎉
|
|
||||||
|
|
||||||
## Accessing Private Images
|
|
||||||
|
|
||||||
If the image is in a private registry:
|
|
||||||
|
|
||||||
### Step 1: Create GitHub Personal Access Token
|
|
||||||
|
|
||||||
1. Go to https://github.com/settings/tokens
|
|
||||||
2. Click "Generate new token (classic)"
|
|
||||||
3. Select scope: `read:packages`
|
|
||||||
4. Generate and copy the token
|
|
||||||
|
|
||||||
### Step 2: Login to GitHub Container Registry
|
|
||||||
|
|
||||||
```bash
|
|
||||||
echo YOUR_GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin
|
|
||||||
```
|
|
||||||
|
|
||||||
### Step 3: Pull and Run
|
|
||||||
|
|
||||||
Now you can pull private images:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Environment Variables
|
|
||||||
|
|
||||||
### Required
|
|
||||||
|
|
||||||
| Variable | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `PUBLIC_API_URL` | Directus API endpoint | `https://api.pivoine.art` |
|
|
||||||
| `PUBLIC_URL` | Frontend URL | `https://sexy.pivoine.art` |
|
|
||||||
|
|
||||||
### Optional
|
|
||||||
|
|
||||||
| Variable | Description | Example |
|
|
||||||
|----------|-------------|---------|
|
|
||||||
| `PUBLIC_UMAMI_ID` | Analytics tracking ID | `abc-123-def` |
|
|
||||||
| `PUBLIC_UMAMI_SCRIPT` | Umami script URL | `https://umami.example.com/script.js` |
|
|
||||||
|
|
||||||
## Common Commands
|
|
||||||
|
|
||||||
### View Logs
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Follow logs (Docker Run)
|
|
||||||
docker logs -f sexy-pivoine
|
|
||||||
|
|
||||||
# Follow logs (Docker Compose)
|
|
||||||
docker-compose -f docker-compose.production.yml logs -f
|
|
||||||
```
|
|
||||||
|
|
||||||
### Restart Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker Run
|
|
||||||
docker restart sexy-pivoine
|
|
||||||
|
|
||||||
# Docker Compose
|
|
||||||
docker-compose -f docker-compose.production.yml restart
|
|
||||||
```
|
|
||||||
|
|
||||||
### Stop Container
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker Run
|
|
||||||
docker stop sexy-pivoine
|
|
||||||
|
|
||||||
# Docker Compose
|
|
||||||
docker-compose -f docker-compose.production.yml down
|
|
||||||
```
|
|
||||||
|
|
||||||
### Update to Latest Version
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker Run
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:latest
|
|
||||||
docker stop sexy-pivoine
|
|
||||||
docker rm sexy-pivoine
|
|
||||||
# Then re-run the docker run command from Step 3
|
|
||||||
|
|
||||||
# Docker Compose
|
|
||||||
docker-compose -f docker-compose.production.yml pull
|
|
||||||
docker-compose -f docker-compose.production.yml up -d
|
|
||||||
```
|
|
||||||
|
|
||||||
### Shell Access
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Docker Run
|
|
||||||
docker exec -it sexy-pivoine sh
|
|
||||||
|
|
||||||
# Docker Compose
|
|
||||||
docker-compose -f docker-compose.production.yml exec frontend sh
|
|
||||||
```
|
|
||||||
|
|
||||||
## Available Image Tags
|
|
||||||
|
|
||||||
| Tag | Description | Use Case |
|
|
||||||
|-----|-------------|----------|
|
|
||||||
| `latest` | Latest stable build from main | Production |
|
|
||||||
| `v1.0.0` | Specific version | Production (pinned) |
|
|
||||||
| `develop` | Latest from develop branch | Staging |
|
|
||||||
| `main-abc123` | Specific commit | Testing |
|
|
||||||
|
|
||||||
**Best Practice:** Use version tags in production for predictable deployments.
|
|
||||||
|
|
||||||
## Production Deployment
|
|
||||||
|
|
||||||
### 1. Use Version Tags
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Instead of :latest
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Add Resource Limits
|
|
||||||
|
|
||||||
```bash
|
|
||||||
docker run -d \
|
|
||||||
--name sexy-pivoine \
|
|
||||||
-p 3000:3000 \
|
|
||||||
--env-file .env.production \
|
|
||||||
--memory="2g" \
|
|
||||||
--cpus="2" \
|
|
||||||
--restart unless-stopped \
|
|
||||||
ghcr.io/valknarxxx/sexy:v1.0.0
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Use a Reverse Proxy
|
|
||||||
|
|
||||||
Example with nginx:
|
|
||||||
|
|
||||||
```nginx
|
|
||||||
server {
|
|
||||||
listen 80;
|
|
||||||
server_name sexy.pivoine.art;
|
|
||||||
|
|
||||||
location / {
|
|
||||||
proxy_pass http://localhost:3000;
|
|
||||||
proxy_http_version 1.1;
|
|
||||||
proxy_set_header Upgrade $http_upgrade;
|
|
||||||
proxy_set_header Connection 'upgrade';
|
|
||||||
proxy_set_header Host $host;
|
|
||||||
proxy_cache_bypass $http_upgrade;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. Enable HTTPS
|
|
||||||
|
|
||||||
Use Certbot or similar:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
certbot --nginx -d sexy.pivoine.art
|
|
||||||
```
|
|
||||||
|
|
||||||
## Health Check
|
|
||||||
|
|
||||||
The container includes a built-in health check:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check container health
|
|
||||||
docker inspect --format='{{.State.Health.Status}}' sexy-pivoine
|
|
||||||
```
|
|
||||||
|
|
||||||
Possible statuses:
|
|
||||||
- `starting` - Container just started
|
|
||||||
- `healthy` - Application is responding
|
|
||||||
- `unhealthy` - Application is not responding
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Container Exits Immediately
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check logs
|
|
||||||
docker logs sexy-pivoine
|
|
||||||
|
|
||||||
# Common issues:
|
|
||||||
# - Missing environment variables
|
|
||||||
# - Port 3000 already in use
|
|
||||||
# - Invalid environment variable values
|
|
||||||
```
|
|
||||||
|
|
||||||
### Cannot Pull Image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# For private images, ensure you're logged in
|
|
||||||
docker login ghcr.io
|
|
||||||
|
|
||||||
# Check if image exists
|
|
||||||
docker pull ghcr.io/valknarxxx/sexy:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
### Port Already in Use
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Use a different port
|
|
||||||
docker run -d -p 8080:3000 ghcr.io/valknarxxx/sexy:latest
|
|
||||||
|
|
||||||
# Or find what's using port 3000
|
|
||||||
lsof -i :3000
|
|
||||||
```
|
|
||||||
|
|
||||||
### Application Not Accessible
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if container is running
|
|
||||||
docker ps | grep sexy-pivoine
|
|
||||||
|
|
||||||
# Check logs
|
|
||||||
docker logs sexy-pivoine
|
|
||||||
|
|
||||||
# Verify port mapping
|
|
||||||
docker port sexy-pivoine
|
|
||||||
|
|
||||||
# Test from inside container
|
|
||||||
docker exec sexy-pivoine wget -O- http://localhost:3000
|
|
||||||
```
|
|
||||||
|
|
||||||
## Next Steps
|
|
||||||
|
|
||||||
- **Production setup:** See [DOCKER.md](DOCKER.md)
|
|
||||||
- **Development:** See [CLAUDE.md](CLAUDE.md)
|
|
||||||
- **CI/CD:** See [.github/workflows/README.md](.github/workflows/README.md)
|
|
||||||
|
|
||||||
## Support
|
|
||||||
|
|
||||||
- **Issues:** https://github.com/valknarxxx/sexy/issues
|
|
||||||
- **Discussions:** https://github.com/valknarxxx/sexy/discussions
|
|
||||||
- **Security:** Report privately via GitHub Security tab
|
|
||||||
|
|
||||||
## License
|
|
||||||
|
|
||||||
See [LICENSE](LICENSE) file for details.
|
|
||||||
262
REBUILD_GUIDE.md
262
REBUILD_GUIDE.md
@@ -1,262 +0,0 @@
|
|||||||
# 🔄 Rebuild Guide - When You Need to Rebuild the Image
|
|
||||||
|
|
||||||
## Why Rebuild?
|
|
||||||
|
|
||||||
SvelteKit's `PUBLIC_*` environment variables are **baked into the JavaScript** at build time. You need to rebuild when:
|
|
||||||
|
|
||||||
1. ✅ Changing `PUBLIC_API_URL`
|
|
||||||
2. ✅ Changing `PUBLIC_URL`
|
|
||||||
3. ❌ NOT needed for `PUBLIC_UMAMI_ID` or `PUBLIC_UMAMI_SCRIPT` (those are runtime)
|
|
||||||
4. ❌ NOT needed for Directus env vars (those are runtime)
|
|
||||||
|
|
||||||
## Quick Rebuild Process
|
|
||||||
|
|
||||||
### 1. Update Frontend Environment Variables
|
|
||||||
|
|
||||||
Edit the frontend `.env` file:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
nano packages/frontend/.env
|
|
||||||
```
|
|
||||||
|
|
||||||
Set your production values:
|
|
||||||
```bash
|
|
||||||
PUBLIC_API_URL=https://sexy.pivoine.art/api
|
|
||||||
PUBLIC_URL=https://sexy.pivoine.art
|
|
||||||
# Note: PUBLIC_UMAMI_* can also be set at runtime in docker-compose
|
|
||||||
PUBLIC_UMAMI_ID=your-umami-id
|
|
||||||
PUBLIC_UMAMI_SCRIPT=https://umami.pivoine.art/script.js
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. Rebuild the Image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# From the project root
|
|
||||||
docker build -t ghcr.io/valknarxxx/sexy:latest -t sexy.pivoine.art:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
**Expected Time:** 30-45 minutes (first build), 10-15 minutes (cached rebuild)
|
|
||||||
|
|
||||||
### 3. Restart Services
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# If using docker-compose
|
|
||||||
cd /home/valknar/Projects/docker-compose/sexy
|
|
||||||
docker compose down
|
|
||||||
docker compose up -d
|
|
||||||
|
|
||||||
# Or directly
|
|
||||||
docker stop sexy_frontend
|
|
||||||
docker rm sexy_frontend
|
|
||||||
docker compose up -d frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
## Monitoring the Build
|
|
||||||
|
|
||||||
### Check Build Progress
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Watch build output
|
|
||||||
docker build -t ghcr.io/valknarxxx/sexy:latest .
|
|
||||||
|
|
||||||
# Build stages:
|
|
||||||
# 1. Base (~30s) - Node.js setup
|
|
||||||
# 2. Builder (~25-40min) - Rust + WASM + packages
|
|
||||||
# - Rust installation: ~2-3 min
|
|
||||||
# - wasm-bindgen-cli: ~10-15 min
|
|
||||||
# - WASM build: ~5-10 min
|
|
||||||
# - Package builds: ~5-10 min
|
|
||||||
# 3. Runner (~2min) - Final image assembly
|
|
||||||
```
|
|
||||||
|
|
||||||
### Verify Environment Variables in Built Image
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check what PUBLIC_API_URL is baked in
|
|
||||||
docker run --rm ghcr.io/valknarxxx/sexy:latest sh -c \
|
|
||||||
"grep -r 'PUBLIC_API_URL' /home/node/app/packages/frontend/build/ | head -3"
|
|
||||||
|
|
||||||
# Should show: https://sexy.pivoine.art/api
|
|
||||||
```
|
|
||||||
|
|
||||||
## Push to GitHub Container Registry
|
|
||||||
|
|
||||||
After successful build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Login to GHCR (first time only)
|
|
||||||
echo $GITHUB_TOKEN | docker login ghcr.io -u valknarxxx --password-stdin
|
|
||||||
|
|
||||||
# Push the image
|
|
||||||
docker push ghcr.io/valknarxxx/sexy:latest
|
|
||||||
```
|
|
||||||
|
|
||||||
## Alternative: Build Arguments (Future Enhancement)
|
|
||||||
|
|
||||||
To avoid rebuilding for every env change, consider adding build arguments:
|
|
||||||
|
|
||||||
```dockerfile
|
|
||||||
# In Dockerfile, before building frontend:
|
|
||||||
ARG PUBLIC_API_URL=https://sexy.pivoine.art/api
|
|
||||||
ARG PUBLIC_URL=https://sexy.pivoine.art
|
|
||||||
ARG PUBLIC_UMAMI_ID=
|
|
||||||
|
|
||||||
# Create .env.production dynamically
|
|
||||||
RUN echo "PUBLIC_API_URL=${PUBLIC_API_URL}" > packages/frontend/.env.production && \
|
|
||||||
echo "PUBLIC_URL=${PUBLIC_URL}" >> packages/frontend/.env.production && \
|
|
||||||
echo "PUBLIC_UMAMI_ID=${PUBLIC_UMAMI_ID}" >> packages/frontend/.env.production
|
|
||||||
```
|
|
||||||
|
|
||||||
Then build with:
|
|
||||||
```bash
|
|
||||||
docker build \
|
|
||||||
--build-arg PUBLIC_API_URL=https://sexy.pivoine.art/api \
|
|
||||||
--build-arg PUBLIC_URL=https://sexy.pivoine.art \
|
|
||||||
-t ghcr.io/valknarxxx/sexy:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
## Troubleshooting
|
|
||||||
|
|
||||||
### Build Fails at Rust Installation
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check network connectivity
|
|
||||||
ping -c 3 sh.rustup.rs
|
|
||||||
|
|
||||||
# Build with verbose output
|
|
||||||
docker build --progress=plain -t ghcr.io/valknarxxx/sexy:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Build Fails at WASM
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Check if wasm-bindgen-cli matches package.json version
|
|
||||||
docker run --rm rust:latest cargo install wasm-bindgen-cli --version 0.2.103
|
|
||||||
```
|
|
||||||
|
|
||||||
### Frontend Still Shows Wrong URL
|
|
||||||
|
|
||||||
```bash
|
|
||||||
# Verify .env file is correct
|
|
||||||
cat packages/frontend/.env
|
|
||||||
|
|
||||||
# Check if old image is cached
|
|
||||||
docker images | grep sexy
|
|
||||||
docker rmi ghcr.io/valknarxxx/sexy:old-tag
|
|
||||||
|
|
||||||
# Force rebuild without cache
|
|
||||||
docker build --no-cache -t ghcr.io/valknarxxx/sexy:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Container Starts But Can't Connect to API
|
|
||||||
|
|
||||||
1. Check Traefik routing:
|
|
||||||
```bash
|
|
||||||
docker logs traefik | grep sexy
|
|
||||||
```
|
|
||||||
|
|
||||||
2. Check if Directus is accessible:
|
|
||||||
```bash
|
|
||||||
curl -I https://sexy.pivoine.art/api/server/health
|
|
||||||
```
|
|
||||||
|
|
||||||
3. Check frontend logs:
|
|
||||||
```bash
|
|
||||||
docker logs sexy_frontend
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development vs Production
|
|
||||||
|
|
||||||
### Development (Local)
|
|
||||||
- Use `pnpm dev` for hot reload
|
|
||||||
- No rebuild needed for code changes
|
|
||||||
- Env vars from `.env` or shell
|
|
||||||
|
|
||||||
### Production (Docker)
|
|
||||||
- Rebuild required for PUBLIC_* changes
|
|
||||||
- Changes baked into JavaScript
|
|
||||||
- Env vars from `packages/frontend/.env`
|
|
||||||
|
|
||||||
## Optimization Tips
|
|
||||||
|
|
||||||
### Speed Up Rebuilds
|
|
||||||
|
|
||||||
1. **Use BuildKit cache:**
|
|
||||||
```bash
|
|
||||||
export DOCKER_BUILDKIT=1
|
|
||||||
docker build --build-arg BUILDKIT_INLINE_CACHE=1 -t ghcr.io/valknarxxx/sexy:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **Multi-stage caching:**
|
|
||||||
- Dockerfile already optimized with multi-stage build
|
|
||||||
- Dependencies cached separately from code
|
|
||||||
|
|
||||||
3. **Parallel builds:**
|
|
||||||
```bash
|
|
||||||
# Build with more CPU cores
|
|
||||||
docker build --cpus 4 -t ghcr.io/valknarxxx/sexy:latest .
|
|
||||||
```
|
|
||||||
|
|
||||||
### Reduce Image Size
|
|
||||||
|
|
||||||
Current optimizations:
|
|
||||||
- ✅ Multi-stage build
|
|
||||||
- ✅ Production dependencies only
|
|
||||||
- ✅ Minimal base image
|
|
||||||
- ✅ No dev tools in final image
|
|
||||||
|
|
||||||
Expected sizes:
|
|
||||||
- Base: ~100MB
|
|
||||||
- Builder: ~2-3GB (not shipped)
|
|
||||||
- Runner: ~300-500MB (final)
|
|
||||||
|
|
||||||
## Automation
|
|
||||||
|
|
||||||
### GitHub Actions (Already Set Up)
|
|
||||||
|
|
||||||
The `.github/workflows/docker-build-push.yml` automatically:
|
|
||||||
1. Builds on push to main
|
|
||||||
2. Creates version tags
|
|
||||||
3. Pushes to GHCR
|
|
||||||
4. Caches layers for faster builds
|
|
||||||
|
|
||||||
**Trigger a rebuild:**
|
|
||||||
```bash
|
|
||||||
git tag v1.0.1
|
|
||||||
git push origin v1.0.1
|
|
||||||
```
|
|
||||||
|
|
||||||
### Local Build Script
|
|
||||||
|
|
||||||
Use the provided `build.sh`:
|
|
||||||
```bash
|
|
||||||
./build.sh -t v1.0.0 -p
|
|
||||||
```
|
|
||||||
|
|
||||||
## When NOT to Rebuild
|
|
||||||
|
|
||||||
You DON'T need to rebuild for:
|
|
||||||
- ❌ Directus configuration changes
|
|
||||||
- ❌ Database credentials
|
|
||||||
- ❌ Redis settings
|
|
||||||
- ❌ SMTP settings
|
|
||||||
- ❌ Session cookie settings
|
|
||||||
- ❌ Traefik labels
|
|
||||||
|
|
||||||
These are runtime environment variables and can be changed in docker-compose.
|
|
||||||
|
|
||||||
## Summary
|
|
||||||
|
|
||||||
| Change | Rebuild Needed | How to Apply |
|
|
||||||
|--------|----------------|--------------|
|
|
||||||
| `PUBLIC_API_URL` | ✅ Yes | Rebuild image |
|
|
||||||
| `PUBLIC_URL` | ✅ Yes | Rebuild image |
|
|
||||||
| `PUBLIC_UMAMI_*` | ❌ No | Restart container |
|
|
||||||
| `SEXY_DIRECTUS_*` | ❌ No | Restart container |
|
|
||||||
| `DB_*` | ❌ No | Restart container |
|
|
||||||
| `EMAIL_*` | ❌ No | Restart container |
|
|
||||||
| Traefik labels | ❌ No | Restart container |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**Remember:** The key difference is **build-time** (compiled into JS) vs **runtime** (read from environment).
|
|
||||||
130
build.sh
130
build.sh
@@ -1,130 +0,0 @@
|
|||||||
#!/bin/bash
|
|
||||||
# Build script for sexy.pivoine.art Docker image
|
|
||||||
|
|
||||||
set -e # Exit on error
|
|
||||||
|
|
||||||
# Colors for output
|
|
||||||
RED='\033[0;31m'
|
|
||||||
GREEN='\033[0;32m'
|
|
||||||
YELLOW='\033[1;33m'
|
|
||||||
NC='\033[0m' # No Color
|
|
||||||
|
|
||||||
# Default values
|
|
||||||
IMAGE_NAME="sexy.pivoine.art"
|
|
||||||
TAG="latest"
|
|
||||||
PUSH=false
|
|
||||||
PLATFORM=""
|
|
||||||
|
|
||||||
# Parse arguments
|
|
||||||
while [[ $# -gt 0 ]]; do
|
|
||||||
case $1 in
|
|
||||||
-t|--tag)
|
|
||||||
TAG="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-n|--name)
|
|
||||||
IMAGE_NAME="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-p|--push)
|
|
||||||
PUSH=true
|
|
||||||
shift
|
|
||||||
;;
|
|
||||||
--platform)
|
|
||||||
PLATFORM="$2"
|
|
||||||
shift 2
|
|
||||||
;;
|
|
||||||
-h|--help)
|
|
||||||
echo "Usage: $0 [OPTIONS]"
|
|
||||||
echo ""
|
|
||||||
echo "Options:"
|
|
||||||
echo " -t, --tag TAG Set image tag (default: latest)"
|
|
||||||
echo " -n, --name NAME Set image name (default: sexy.pivoine.art)"
|
|
||||||
echo " -p, --push Push image after build"
|
|
||||||
echo " --platform PLATFORM Build for specific platform (e.g., linux/amd64,linux/arm64)"
|
|
||||||
echo " -h, --help Show this help message"
|
|
||||||
echo ""
|
|
||||||
echo "Examples:"
|
|
||||||
echo " $0 # Build with defaults"
|
|
||||||
echo " $0 -t v1.0.0 # Build with version tag"
|
|
||||||
echo " $0 --platform linux/amd64,linux/arm64 -p # Multi-platform build and push"
|
|
||||||
exit 0
|
|
||||||
;;
|
|
||||||
*)
|
|
||||||
echo -e "${RED}Unknown option: $1${NC}"
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
|
|
||||||
FULL_IMAGE="${IMAGE_NAME}:${TAG}"
|
|
||||||
|
|
||||||
echo -e "${GREEN}=== Building Docker Image ===${NC}"
|
|
||||||
echo "Image: ${FULL_IMAGE}"
|
|
||||||
echo "Platform: ${PLATFORM:-default}"
|
|
||||||
echo ""
|
|
||||||
|
|
||||||
# Check if Docker is running
|
|
||||||
if ! docker info > /dev/null 2>&1; then
|
|
||||||
echo -e "${RED}Error: Docker is not running${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Build command
|
|
||||||
BUILD_CMD="docker build"
|
|
||||||
|
|
||||||
if [ -n "$PLATFORM" ]; then
|
|
||||||
# Multi-platform build requires buildx
|
|
||||||
echo -e "${YELLOW}Using buildx for multi-platform build${NC}"
|
|
||||||
BUILD_CMD="docker buildx build --platform ${PLATFORM}"
|
|
||||||
|
|
||||||
if [ "$PUSH" = true ]; then
|
|
||||||
BUILD_CMD="${BUILD_CMD} --push"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
# Regular build
|
|
||||||
if [ "$PUSH" = true ]; then
|
|
||||||
echo -e "${YELLOW}Note: --push only works with multi-platform builds. Use 'docker push' after build.${NC}"
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Execute build
|
|
||||||
echo -e "${GREEN}Building...${NC}"
|
|
||||||
$BUILD_CMD -t "${FULL_IMAGE}" .
|
|
||||||
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}✓ Build successful!${NC}"
|
|
||||||
echo "Image: ${FULL_IMAGE}"
|
|
||||||
|
|
||||||
# Show image size
|
|
||||||
if [ -z "$PLATFORM" ]; then
|
|
||||||
SIZE=$(docker images "${FULL_IMAGE}" --format "{{.Size}}")
|
|
||||||
echo "Size: ${SIZE}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
# Push if requested and not multi-platform
|
|
||||||
if [ "$PUSH" = true ] && [ -z "$PLATFORM" ]; then
|
|
||||||
echo -e "${GREEN}Pushing image...${NC}"
|
|
||||||
docker push "${FULL_IMAGE}"
|
|
||||||
if [ $? -eq 0 ]; then
|
|
||||||
echo -e "${GREEN}✓ Push successful!${NC}"
|
|
||||||
else
|
|
||||||
echo -e "${RED}✗ Push failed${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo -e "${GREEN}Next steps:${NC}"
|
|
||||||
echo "1. Run locally:"
|
|
||||||
echo " docker run -d -p 3000:3000 --env-file .env.production ${FULL_IMAGE}"
|
|
||||||
echo ""
|
|
||||||
echo "2. Run with docker-compose:"
|
|
||||||
echo " docker-compose -f docker-compose.production.yml up -d"
|
|
||||||
echo ""
|
|
||||||
echo "3. View logs:"
|
|
||||||
echo " docker logs -f <container-name>"
|
|
||||||
else
|
|
||||||
echo -e "${RED}✗ Build failed${NC}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
include:
|
|
||||||
- compose.yml
|
|
||||||
|
|
||||||
# Production compose file - extends base compose.yml
|
|
||||||
# Usage: docker-compose -f compose.production.yml up -d
|
|
||||||
|
|
||||||
networks:
|
|
||||||
compose_network:
|
|
||||||
external: true
|
|
||||||
name: compose_network
|
|
||||||
|
|
||||||
services:
|
|
||||||
# Disable local postgres for production (use external DB)
|
|
||||||
postgres:
|
|
||||||
deploy:
|
|
||||||
replicas: 0
|
|
||||||
|
|
||||||
# Disable local redis for production (use external Redis)
|
|
||||||
redis:
|
|
||||||
deploy:
|
|
||||||
replicas: 0
|
|
||||||
|
|
||||||
# Override Directus for production
|
|
||||||
directus:
|
|
||||||
networks:
|
|
||||||
- compose_network
|
|
||||||
ports: [] # Remove exposed ports, use Traefik instead
|
|
||||||
|
|
||||||
# Override volumes for production paths
|
|
||||||
volumes:
|
|
||||||
- ${SEXY_DIRECTUS_UPLOADS:-./uploads}:/directus/uploads
|
|
||||||
- ${SEXY_DIRECTUS_BUNDLE:-./packages/bundle/dist}:/directus/extensions/sexy.pivoine.art
|
|
||||||
|
|
||||||
# Override environment for production settings
|
|
||||||
environment:
|
|
||||||
# Database (external)
|
|
||||||
DB_HOST: ${CORE_DB_HOST}
|
|
||||||
DB_PORT: ${CORE_DB_PORT:-5432}
|
|
||||||
DB_DATABASE: ${SEXY_DB_NAME}
|
|
||||||
DB_USER: ${DB_USER}
|
|
||||||
DB_PASSWORD: ${DB_PASSWORD}
|
|
||||||
|
|
||||||
# General
|
|
||||||
SECRET: ${SEXY_DIRECTUS_SECRET}
|
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL}
|
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD}
|
|
||||||
PUBLIC_URL: ${SEXY_PUBLIC_URL}
|
|
||||||
|
|
||||||
# Cache (external Redis)
|
|
||||||
REDIS: redis://${CORE_REDIS_HOST}:${CORE_REDIS_PORT:-6379}
|
|
||||||
|
|
||||||
# CORS
|
|
||||||
CORS_ORIGIN: ${SEXY_CORS_ORIGIN}
|
|
||||||
|
|
||||||
# Security (production settings)
|
|
||||||
SESSION_COOKIE_SECURE: ${SEXY_SESSION_COOKIE_SECURE:-true}
|
|
||||||
SESSION_COOKIE_SAME_SITE: ${SEXY_SESSION_COOKIE_SAME_SITE:-strict}
|
|
||||||
SESSION_COOKIE_DOMAIN: ${SEXY_SESSION_COOKIE_DOMAIN}
|
|
||||||
|
|
||||||
# Extensions
|
|
||||||
EXTENSIONS_AUTO_RELOAD: ${SEXY_EXTENSIONS_AUTO_RELOAD:-false}
|
|
||||||
|
|
||||||
# Email (production SMTP)
|
|
||||||
EMAIL_TRANSPORT: ${EMAIL_TRANSPORT:-smtp}
|
|
||||||
EMAIL_FROM: ${EMAIL_FROM}
|
|
||||||
EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST}
|
|
||||||
EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-587}
|
|
||||||
EMAIL_SMTP_USER: ${EMAIL_SMTP_USER}
|
|
||||||
EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD}
|
|
||||||
|
|
||||||
# User URLs
|
|
||||||
USER_REGISTER_URL_ALLOW_LIST: ${SEXY_USER_REGISTER_URL_ALLOW_LIST}
|
|
||||||
PASSWORD_RESET_URL_ALLOW_LIST: ${SEXY_PASSWORD_RESET_URL_ALLOW_LIST}
|
|
||||||
|
|
||||||
# Remove local dependencies
|
|
||||||
depends_on: []
|
|
||||||
|
|
||||||
labels:
|
|
||||||
# Traefik labels for reverse proxy
|
|
||||||
- 'traefik.enable=${SEXY_TRAEFIK_ENABLED:-true}'
|
|
||||||
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-redirect-web-secure.redirectscheme.scheme=https'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-redirect-web-secure'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web.rule=Host(`${SEXY_TRAEFIK_HOST}`) && PathPrefix(`/api`)'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web.entrypoints=web'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.rule=Host(`${SEXY_TRAEFIK_HOST}`) && PathPrefix(`/api`)'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.tls.certresolver=resolver'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.entrypoints=web-secure'
|
|
||||||
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure-compress.compress=true'
|
|
||||||
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-strip.stripprefix.prefixes=/api'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-strip,${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure-compress'
|
|
||||||
- 'traefik.http.services.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-api-web-secure.loadbalancer.server.port=8055'
|
|
||||||
- 'traefik.docker.network=compose_network'
|
|
||||||
|
|
||||||
# Override Frontend for production
|
|
||||||
frontend:
|
|
||||||
networks:
|
|
||||||
- compose_network
|
|
||||||
ports: [] # Remove exposed ports, use Traefik instead
|
|
||||||
|
|
||||||
# Override environment for production
|
|
||||||
environment:
|
|
||||||
NODE_ENV: production
|
|
||||||
PUBLIC_API_URL: ${SEXY_FRONTEND_PUBLIC_API_URL}
|
|
||||||
PUBLIC_URL: ${SEXY_FRONTEND_PUBLIC_URL}
|
|
||||||
PUBLIC_UMAMI_ID: ${SEXY_FRONTEND_PUBLIC_UMAMI_ID:-}
|
|
||||||
PUBLIC_UMAMI_SCRIPT: ${SEXY_FRONTEND_PUBLIC_UMAMI_SCRIPT:-}
|
|
||||||
|
|
||||||
# Override volume for production path
|
|
||||||
volumes:
|
|
||||||
- ${SEXY_FRONTEND_PATH:-/var/www/sexy.pivoine.art}:/home/node/app
|
|
||||||
|
|
||||||
# Remove local dependency
|
|
||||||
depends_on: []
|
|
||||||
|
|
||||||
labels:
|
|
||||||
# Traefik labels for reverse proxy
|
|
||||||
- 'traefik.enable=${SEXY_TRAEFIK_ENABLED:-true}'
|
|
||||||
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-redirect-web-secure.redirectscheme.scheme=https'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-redirect-web-secure'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web.rule=Host(`${SEXY_TRAEFIK_HOST}`)'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web.entrypoints=web'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.rule=Host(`${SEXY_TRAEFIK_HOST}`)'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.tls.certresolver=resolver'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.entrypoints=web-secure'
|
|
||||||
- 'traefik.http.middlewares.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure-compress.compress=true'
|
|
||||||
- 'traefik.http.routers.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.middlewares=${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure-compress'
|
|
||||||
- 'traefik.http.services.${SEXY_COMPOSE_PROJECT_NAME:-sexy}-frontend-web-secure.loadbalancer.server.port=3000'
|
|
||||||
- 'traefik.docker.network=compose_network'
|
|
||||||
173
compose.yml
173
compose.yml
@@ -1,112 +1,71 @@
|
|||||||
|
name: sexy
|
||||||
services:
|
services:
|
||||||
# PostgreSQL Database (local only)
|
|
||||||
postgres:
|
postgres:
|
||||||
image: postgres:16-alpine
|
image: postgres:16-alpine
|
||||||
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_postgres
|
container_name: sexy_postgres
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- sexy-network
|
|
||||||
volumes:
|
volumes:
|
||||||
- postgres-data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_DB: ${DB_DATABASE:-sexy}
|
POSTGRES_DB: sexy
|
||||||
POSTGRES_USER: ${DB_USER:-sexy}
|
POSTGRES_USER: sexy
|
||||||
POSTGRES_PASSWORD: ${DB_PASSWORD:-sexy}
|
POSTGRES_PASSWORD: sexy
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U ${DB_USER:-sexy}"]
|
test: ["CMD-SHELL", "pg_isready -U sexy"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
# Redis Cache (local only)
|
|
||||||
redis:
|
redis:
|
||||||
image: redis:7-alpine
|
image: redis:7-alpine
|
||||||
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_redis
|
container_name: sexy_redis
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- sexy-network
|
|
||||||
volumes:
|
volumes:
|
||||||
- redis-data:/data
|
- redis_data:/data
|
||||||
command: redis-server --appendonly yes
|
command: redis-server --appendonly yes
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "redis-cli", "ping"]
|
test: ["CMD", "redis-cli", "ping"]
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|
||||||
# Directus CMS
|
|
||||||
directus:
|
directus:
|
||||||
image: ${SEXY_DIRECTUS_IMAGE:-directus/directus:11}
|
image: directus/directus:11
|
||||||
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_api
|
container_name: sexy_directus
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
networks:
|
|
||||||
- sexy-network
|
|
||||||
ports:
|
ports:
|
||||||
- "8055:8055"
|
- "8055:8055"
|
||||||
volumes:
|
volumes:
|
||||||
- directus-uploads:/directus/uploads
|
- directus_uploads:/directus/uploads
|
||||||
- ${SEXY_DIRECTUS_BUNDLE:-./packages/bundle}:/directus/extensions/sexy.pivoine.art
|
- ./packages/bundle:/directus/extensions/sexy.pivoine.art
|
||||||
environment:
|
environment:
|
||||||
# Database
|
|
||||||
DB_CLIENT: pg
|
DB_CLIENT: pg
|
||||||
DB_HOST: ${CORE_DB_HOST:-postgres}
|
DB_HOST: sexy_postgres
|
||||||
DB_PORT: ${CORE_DB_PORT:-5432}
|
DB_PORT: 5432
|
||||||
DB_DATABASE: ${SEXY_DB_NAME:-sexy}
|
DB_DATABASE: sexy
|
||||||
DB_USER: ${DB_USER:-sexy}
|
DB_USER: sexy
|
||||||
DB_PASSWORD: ${DB_PASSWORD:-sexy}
|
DB_PASSWORD: sexy
|
||||||
|
ADMIN_EMAIL: admin@sexy
|
||||||
# General
|
ADMIN_PASSWORD: admin
|
||||||
SECRET: ${SEXY_DIRECTUS_SECRET:-replace-with-random-secret-min-32-chars}
|
PUBLIC_URL: http://localhost:3000/api
|
||||||
ADMIN_EMAIL: ${ADMIN_EMAIL:-admin@sexy.pivoine.art}
|
CACHE_ENABLED: true
|
||||||
ADMIN_PASSWORD: ${ADMIN_PASSWORD:-admin}
|
CACHE_AUTO_PURGE: true
|
||||||
PUBLIC_URL: ${SEXY_PUBLIC_URL:-http://localhost:8055}
|
|
||||||
|
|
||||||
# Cache
|
|
||||||
CACHE_ENABLED: ${SEXY_CACHE_ENABLED:-true}
|
|
||||||
CACHE_AUTO_PURGE: ${SEXY_CACHE_AUTO_PURGE:-true}
|
|
||||||
CACHE_STORE: redis
|
CACHE_STORE: redis
|
||||||
REDIS: redis://${CORE_REDIS_HOST:-redis}:${CORE_REDIS_PORT:-6379}
|
REDIS: redis://sexy_redis:6379
|
||||||
|
CORS_ENABLED: true
|
||||||
# CORS
|
CORS_ORIGIN: http://localhost:3000
|
||||||
CORS_ENABLED: ${SEXY_CORS_ENABLED:-true}
|
SESSION_COOKIE_SECURE: false
|
||||||
CORS_ORIGIN: ${SEXY_CORS_ORIGIN:-http://localhost:3000}
|
SESSION_COOKIE_SAME_SITE: lax
|
||||||
|
SESSION_COOKIE_DOMAIN: localhost
|
||||||
# Security
|
EXTENSIONS_PATH: /directus/extensions
|
||||||
SESSION_COOKIE_SECURE: ${SEXY_SESSION_COOKIE_SECURE:-false}
|
EXTENSIONS_AUTO_RELOAD: true
|
||||||
SESSION_COOKIE_SAME_SITE: ${SEXY_SESSION_COOKIE_SAME_SITE:-lax}
|
WEBSOCKETS_ENABLED: true
|
||||||
SESSION_COOKIE_DOMAIN: ${SEXY_SESSION_COOKIE_DOMAIN:-localhost}
|
USER_REGISTER_URL_ALLOW_LIST: http://localhost:3000
|
||||||
|
PASSWORD_RESET_URL_ALLOW_LIST: http://localhost:3000
|
||||||
# Extensions
|
TZ: Europe/Amsterdam
|
||||||
EXTENSIONS_PATH: ${SEXY_EXTENSIONS_PATH:-/directus/extensions}
|
|
||||||
EXTENSIONS_AUTO_RELOAD: ${SEXY_EXTENSIONS_AUTO_RELOAD:-true}
|
|
||||||
|
|
||||||
# WebSockets
|
|
||||||
WEBSOCKETS_ENABLED: ${SEXY_WEBSOCKETS_ENABLED:-true}
|
|
||||||
|
|
||||||
# Email (optional for local dev)
|
|
||||||
EMAIL_TRANSPORT: ${EMAIL_TRANSPORT:-sendmail}
|
|
||||||
EMAIL_FROM: ${EMAIL_FROM:-noreply@sexy.pivoine.art}
|
|
||||||
EMAIL_SMTP_HOST: ${EMAIL_SMTP_HOST:-}
|
|
||||||
EMAIL_SMTP_PORT: ${EMAIL_SMTP_PORT:-587}
|
|
||||||
EMAIL_SMTP_USER: ${EMAIL_SMTP_USER:-}
|
|
||||||
EMAIL_SMTP_PASSWORD: ${EMAIL_SMTP_PASSWORD:-}
|
|
||||||
|
|
||||||
# User Registration & Password Reset URLs
|
|
||||||
USER_REGISTER_URL_ALLOW_LIST: ${SEXY_USER_REGISTER_URL_ALLOW_LIST:-http://localhost:3000}
|
|
||||||
PASSWORD_RESET_URL_ALLOW_LIST: ${SEXY_PASSWORD_RESET_URL_ALLOW_LIST:-http://localhost:3000}
|
|
||||||
|
|
||||||
# Content Security Policy
|
|
||||||
CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC: ${SEXY_CONTENT_SECURITY_POLICY_DIRECTIVES__FRAME_SRC:-}
|
|
||||||
|
|
||||||
# Timezone
|
|
||||||
TZ: ${TIMEZONE:-Europe/Amsterdam}
|
|
||||||
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
redis:
|
redis:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8055/server/health"]
|
test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8055/server/health"]
|
||||||
interval: 30s
|
interval: 30s
|
||||||
@@ -114,66 +73,10 @@ services:
|
|||||||
retries: 3
|
retries: 3
|
||||||
start_period: 40s
|
start_period: 40s
|
||||||
|
|
||||||
# Frontend (local development - optional, usually run via pnpm dev)
|
|
||||||
frontend:
|
|
||||||
image: ${SEXY_FRONTEND_IMAGE:-ghcr.io/valknarxxx/sexy:latest}
|
|
||||||
container_name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_frontend
|
|
||||||
restart: unless-stopped
|
|
||||||
user: node
|
|
||||||
working_dir: /home/node/app/packages/frontend
|
|
||||||
networks:
|
|
||||||
- sexy-network
|
|
||||||
ports:
|
|
||||||
- "3000:3000"
|
|
||||||
environment:
|
|
||||||
# Node
|
|
||||||
NODE_ENV: ${NODE_ENV:-development}
|
|
||||||
PORT: 3000
|
|
||||||
HOST: 0.0.0.0
|
|
||||||
|
|
||||||
# Public environment variables
|
|
||||||
PUBLIC_API_URL: ${SEXY_FRONTEND_PUBLIC_API_URL:-http://localhost:8055}
|
|
||||||
PUBLIC_URL: ${SEXY_FRONTEND_PUBLIC_URL:-http://localhost:3000}
|
|
||||||
PUBLIC_UMAMI_ID: ${SEXY_FRONTEND_PUBLIC_UMAMI_ID:-}
|
|
||||||
PUBLIC_UMAMI_SCRIPT: ${SEXY_FRONTEND_PUBLIC_UMAMI_SCRIPT:-}
|
|
||||||
|
|
||||||
# Timezone
|
|
||||||
TZ: ${TIMEZONE:-Europe/Amsterdam}
|
|
||||||
|
|
||||||
volumes:
|
|
||||||
- ${SEXY_FRONTEND_PATH:-./}:/home/node/app
|
|
||||||
|
|
||||||
command: ["node", "build/index.js"]
|
|
||||||
|
|
||||||
depends_on:
|
|
||||||
- directus
|
|
||||||
|
|
||||||
healthcheck:
|
|
||||||
test: ["CMD", "node", "-e", "require('http').get('http://localhost:3000/', (r) => {process.exit(r.statusCode === 200 ? 0 : 1)})"]
|
|
||||||
interval: 30s
|
|
||||||
timeout: 3s
|
|
||||||
retries: 3
|
|
||||||
start_period: 40s
|
|
||||||
|
|
||||||
# Uncomment to run frontend in development mode with live reload
|
|
||||||
# build:
|
|
||||||
# context: .
|
|
||||||
# dockerfile: Dockerfile
|
|
||||||
# volumes:
|
|
||||||
# - ./packages/frontend:/home/node/app/packages/frontend
|
|
||||||
# - /home/node/app/packages/frontend/node_modules
|
|
||||||
# environment:
|
|
||||||
# NODE_ENV: development
|
|
||||||
|
|
||||||
networks:
|
|
||||||
sexy-network:
|
|
||||||
driver: bridge
|
|
||||||
name: ${SEXY_COMPOSE_PROJECT_NAME:-sexy}_network
|
|
||||||
|
|
||||||
volumes:
|
volumes:
|
||||||
directus-uploads:
|
directus_uploads:
|
||||||
driver: local
|
driver: local
|
||||||
postgres-data:
|
postgres_data:
|
||||||
driver: local
|
driver: local
|
||||||
redis-data:
|
redis_data:
|
||||||
driver: local
|
driver: local
|
||||||
|
|||||||
@@ -102,34 +102,6 @@ collections:
|
|||||||
versioning: false
|
versioning: false
|
||||||
schema:
|
schema:
|
||||||
name: sexy_videos_directus_users
|
name: sexy_videos_directus_users
|
||||||
- collection: sexy_recordings
|
|
||||||
meta:
|
|
||||||
accountability: all
|
|
||||||
archive_app_filter: true
|
|
||||||
archive_field: status
|
|
||||||
archive_value: archived
|
|
||||||
collapse: open
|
|
||||||
collection: sexy_recordings
|
|
||||||
color: null
|
|
||||||
display_template: null
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
icon: fiber_manual_record
|
|
||||||
item_duplication_fields: null
|
|
||||||
note: null
|
|
||||||
preview_url: null
|
|
||||||
singleton: false
|
|
||||||
sort: null
|
|
||||||
sort_field: null
|
|
||||||
translations:
|
|
||||||
- language: en-US
|
|
||||||
plural: Recordings
|
|
||||||
singular: Recording
|
|
||||||
translation: Sexy Recordings
|
|
||||||
unarchive_value: draft
|
|
||||||
versioning: false
|
|
||||||
schema:
|
|
||||||
name: sexy_recordings
|
|
||||||
fields:
|
fields:
|
||||||
- collection: directus_users
|
- collection: directus_users
|
||||||
field: website
|
field: website
|
||||||
@@ -206,7 +178,7 @@ fields:
|
|||||||
max_length: 255
|
max_length: 255
|
||||||
numeric_precision: null
|
numeric_precision: null
|
||||||
numeric_scale: null
|
numeric_scale: null
|
||||||
is_nullable: true
|
is_nullable: false
|
||||||
is_unique: true
|
is_unique: true
|
||||||
is_indexed: true
|
is_indexed: true
|
||||||
is_primary_key: false
|
is_primary_key: false
|
||||||
@@ -1908,639 +1880,6 @@ fields:
|
|||||||
has_auto_increment: false
|
has_auto_increment: false
|
||||||
foreign_key_table: directus_users
|
foreign_key_table: directus_users
|
||||||
foreign_key_column: id
|
foreign_key_column: id
|
||||||
- collection: sexy_recordings
|
|
||||||
field: id
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: id
|
|
||||||
group: null
|
|
||||||
hidden: true
|
|
||||||
interface: input
|
|
||||||
note: null
|
|
||||||
options: null
|
|
||||||
readonly: true
|
|
||||||
required: false
|
|
||||||
sort: 1
|
|
||||||
special:
|
|
||||||
- uuid
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: id
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: uuid
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: false
|
|
||||||
is_unique: true
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: true
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: status
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: labels
|
|
||||||
display_options:
|
|
||||||
choices:
|
|
||||||
- background: var(--theme--primary-background)
|
|
||||||
color: var(--theme--primary)
|
|
||||||
foreground: var(--theme--primary)
|
|
||||||
text: $t:published
|
|
||||||
value: published
|
|
||||||
- background: var(--theme--background-normal)
|
|
||||||
color: var(--theme--foreground)
|
|
||||||
foreground: var(--theme--foreground)
|
|
||||||
text: $t:draft
|
|
||||||
value: draft
|
|
||||||
- background: var(--theme--warning-background)
|
|
||||||
color: var(--theme--warning)
|
|
||||||
foreground: var(--theme--warning)
|
|
||||||
text: $t:archived
|
|
||||||
value: archived
|
|
||||||
showAsDot: true
|
|
||||||
field: status
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: select-dropdown
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
choices:
|
|
||||||
- color: var(--theme--primary)
|
|
||||||
text: $t:published
|
|
||||||
value: published
|
|
||||||
- color: var(--theme--foreground)
|
|
||||||
text: $t:draft
|
|
||||||
value: draft
|
|
||||||
- color: var(--theme--warning)
|
|
||||||
text: $t:archived
|
|
||||||
value: archived
|
|
||||||
readonly: false
|
|
||||||
required: false
|
|
||||||
sort: 2
|
|
||||||
special: null
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: status
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: character varying
|
|
||||||
default_value: draft
|
|
||||||
max_length: 255
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: false
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: user_created
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: user
|
|
||||||
display_options: null
|
|
||||||
field: user_created
|
|
||||||
group: null
|
|
||||||
hidden: true
|
|
||||||
interface: select-dropdown-m2o
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
template: '{{avatar}} {{first_name}} {{last_name}}'
|
|
||||||
readonly: true
|
|
||||||
required: false
|
|
||||||
sort: 3
|
|
||||||
special:
|
|
||||||
- user-created
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: half
|
|
||||||
schema:
|
|
||||||
name: user_created
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: uuid
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: directus_users
|
|
||||||
foreign_key_column: id
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: date_created
|
|
||||||
type: timestamp
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: datetime
|
|
||||||
display_options:
|
|
||||||
relative: true
|
|
||||||
field: date_created
|
|
||||||
group: null
|
|
||||||
hidden: true
|
|
||||||
interface: datetime
|
|
||||||
note: null
|
|
||||||
options: null
|
|
||||||
readonly: true
|
|
||||||
required: false
|
|
||||||
sort: 4
|
|
||||||
special:
|
|
||||||
- date-created
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: half
|
|
||||||
schema:
|
|
||||||
name: date_created
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: timestamp with time zone
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: date_updated
|
|
||||||
type: timestamp
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: datetime
|
|
||||||
display_options:
|
|
||||||
relative: true
|
|
||||||
field: date_updated
|
|
||||||
group: null
|
|
||||||
hidden: true
|
|
||||||
interface: datetime
|
|
||||||
note: null
|
|
||||||
options: null
|
|
||||||
readonly: true
|
|
||||||
required: false
|
|
||||||
sort: 5
|
|
||||||
special:
|
|
||||||
- date-updated
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: half
|
|
||||||
schema:
|
|
||||||
name: date_updated
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: timestamp with time zone
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: title
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: title
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: input
|
|
||||||
note: null
|
|
||||||
options: null
|
|
||||||
readonly: false
|
|
||||||
required: true
|
|
||||||
sort: 6
|
|
||||||
special: null
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: title
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: character varying
|
|
||||||
default_value: null
|
|
||||||
max_length: 255
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: description
|
|
||||||
type: text
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: description
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: input-multiline
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
trim: true
|
|
||||||
readonly: false
|
|
||||||
required: false
|
|
||||||
sort: 7
|
|
||||||
special: null
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: description
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: text
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: slug
|
|
||||||
type: string
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: slug
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: input
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
slug: true
|
|
||||||
trim: true
|
|
||||||
readonly: false
|
|
||||||
required: true
|
|
||||||
sort: 8
|
|
||||||
special: null
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: slug
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: character varying
|
|
||||||
default_value: null
|
|
||||||
max_length: 255
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: false
|
|
||||||
is_unique: true
|
|
||||||
is_indexed: true
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: duration
|
|
||||||
type: float
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: duration
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: input
|
|
||||||
note: Duration in milliseconds
|
|
||||||
options: null
|
|
||||||
readonly: false
|
|
||||||
required: true
|
|
||||||
sort: 9
|
|
||||||
special: null
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: duration
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: double precision
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: 53
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: false
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: events
|
|
||||||
type: json
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: events
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: input-code
|
|
||||||
note: Array of recorded events with timestamps
|
|
||||||
options:
|
|
||||||
language: json
|
|
||||||
readonly: false
|
|
||||||
required: true
|
|
||||||
sort: 10
|
|
||||||
special:
|
|
||||||
- cast-json
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: events
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: json
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: false
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: device_info
|
|
||||||
type: json
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: device_info
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: input-code
|
|
||||||
note: Array of device metadata
|
|
||||||
options:
|
|
||||||
language: json
|
|
||||||
readonly: false
|
|
||||||
required: true
|
|
||||||
sort: 11
|
|
||||||
special:
|
|
||||||
- cast-json
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: device_info
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: json
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: false
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: tags
|
|
||||||
type: json
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: tags
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: tags
|
|
||||||
note: null
|
|
||||||
options: null
|
|
||||||
readonly: false
|
|
||||||
required: false
|
|
||||||
sort: 12
|
|
||||||
special:
|
|
||||||
- cast-json
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: tags
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: json
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: linked_video
|
|
||||||
type: uuid
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: linked_video
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: select-dropdown-m2o
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
enableLink: true
|
|
||||||
readonly: false
|
|
||||||
required: false
|
|
||||||
sort: 13
|
|
||||||
special:
|
|
||||||
- m2o
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: linked_video
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: uuid
|
|
||||||
default_value: null
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: sexy_videos
|
|
||||||
foreign_key_column: id
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: featured
|
|
||||||
type: boolean
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: featured
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: boolean
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
label: Featured
|
|
||||||
readonly: false
|
|
||||||
required: false
|
|
||||||
sort: 14
|
|
||||||
special:
|
|
||||||
- cast-boolean
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: featured
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: boolean
|
|
||||||
default_value: false
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: public
|
|
||||||
type: boolean
|
|
||||||
meta:
|
|
||||||
collection: sexy_recordings
|
|
||||||
conditions: null
|
|
||||||
display: null
|
|
||||||
display_options: null
|
|
||||||
field: public
|
|
||||||
group: null
|
|
||||||
hidden: false
|
|
||||||
interface: boolean
|
|
||||||
note: null
|
|
||||||
options:
|
|
||||||
label: Public
|
|
||||||
readonly: false
|
|
||||||
required: false
|
|
||||||
sort: 15
|
|
||||||
special:
|
|
||||||
- cast-boolean
|
|
||||||
translations: null
|
|
||||||
validation: null
|
|
||||||
validation_message: null
|
|
||||||
width: full
|
|
||||||
schema:
|
|
||||||
name: public
|
|
||||||
table: sexy_recordings
|
|
||||||
data_type: boolean
|
|
||||||
default_value: false
|
|
||||||
max_length: null
|
|
||||||
numeric_precision: null
|
|
||||||
numeric_scale: null
|
|
||||||
is_nullable: true
|
|
||||||
is_unique: false
|
|
||||||
is_indexed: false
|
|
||||||
is_primary_key: false
|
|
||||||
is_generated: false
|
|
||||||
generation_expression: null
|
|
||||||
has_auto_increment: false
|
|
||||||
foreign_key_table: null
|
|
||||||
foreign_key_column: null
|
|
||||||
relations:
|
relations:
|
||||||
- collection: directus_users
|
- collection: directus_users
|
||||||
field: banner
|
field: banner
|
||||||
@@ -2773,45 +2112,3 @@ relations:
|
|||||||
constraint_name: sexy_videos_directus_users_sexy_videos_id_foreign
|
constraint_name: sexy_videos_directus_users_sexy_videos_id_foreign
|
||||||
on_update: NO ACTION
|
on_update: NO ACTION
|
||||||
on_delete: SET NULL
|
on_delete: SET NULL
|
||||||
- collection: sexy_recordings
|
|
||||||
field: user_created
|
|
||||||
related_collection: directus_users
|
|
||||||
meta:
|
|
||||||
junction_field: null
|
|
||||||
many_collection: sexy_recordings
|
|
||||||
many_field: user_created
|
|
||||||
one_allowed_collections: null
|
|
||||||
one_collection: directus_users
|
|
||||||
one_collection_field: null
|
|
||||||
one_deselect_action: nullify
|
|
||||||
one_field: null
|
|
||||||
sort_field: null
|
|
||||||
schema:
|
|
||||||
table: sexy_recordings
|
|
||||||
column: user_created
|
|
||||||
foreign_key_table: directus_users
|
|
||||||
foreign_key_column: id
|
|
||||||
constraint_name: sexy_recordings_user_created_foreign
|
|
||||||
on_update: NO ACTION
|
|
||||||
on_delete: NO ACTION
|
|
||||||
- collection: sexy_recordings
|
|
||||||
field: linked_video
|
|
||||||
related_collection: sexy_videos
|
|
||||||
meta:
|
|
||||||
junction_field: null
|
|
||||||
many_collection: sexy_recordings
|
|
||||||
many_field: linked_video
|
|
||||||
one_allowed_collections: null
|
|
||||||
one_collection: sexy_videos
|
|
||||||
one_collection_field: null
|
|
||||||
one_deselect_action: nullify
|
|
||||||
one_field: null
|
|
||||||
sort_field: null
|
|
||||||
schema:
|
|
||||||
table: sexy_recordings
|
|
||||||
column: linked_video
|
|
||||||
foreign_key_table: sexy_videos
|
|
||||||
foreign_key_column: id
|
|
||||||
constraint_name: sexy_recordings_linked_video_foreign
|
|
||||||
on_update: NO ACTION
|
|
||||||
on_delete: SET NULL
|
|
||||||
@@ -1,177 +0,0 @@
|
|||||||
-- Gamification System Schema for Sexy Recordings Platform
|
|
||||||
-- Created: 2025-10-28
|
|
||||||
-- Description: Recording-focused gamification with time-weighted scoring
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Table: sexy_recording_plays
|
|
||||||
-- ====================
|
|
||||||
-- Tracks when users play recordings (similar to video plays)
|
|
||||||
CREATE TABLE IF NOT EXISTS sexy_recording_plays (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL REFERENCES directus_users(id) ON DELETE CASCADE,
|
|
||||||
recording_id UUID NOT NULL REFERENCES sexy_recordings(id) ON DELETE CASCADE,
|
|
||||||
duration_played INTEGER, -- Duration played in milliseconds
|
|
||||||
completed BOOLEAN DEFAULT FALSE, -- True if >= 90% watched
|
|
||||||
date_created TIMESTAMP WITH TIME ZONE DEFAULT NOW(),
|
|
||||||
date_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recording_plays_user ON sexy_recording_plays(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recording_plays_recording ON sexy_recording_plays(recording_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_recording_plays_date ON sexy_recording_plays(date_created);
|
|
||||||
|
|
||||||
COMMENT ON TABLE sexy_recording_plays IS 'Tracks user playback of recordings for analytics and gamification';
|
|
||||||
COMMENT ON COLUMN sexy_recording_plays.completed IS 'True if user watched at least 90% of the recording';
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Table: sexy_user_points
|
|
||||||
-- ====================
|
|
||||||
-- Tracks individual point-earning actions with timestamps for time-weighted scoring
|
|
||||||
CREATE TABLE IF NOT EXISTS sexy_user_points (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL REFERENCES directus_users(id) ON DELETE CASCADE,
|
|
||||||
action VARCHAR(50) NOT NULL, -- e.g., "RECORDING_CREATE", "RECORDING_PLAY", "COMMENT_CREATE"
|
|
||||||
points INTEGER NOT NULL, -- Raw points earned
|
|
||||||
recording_id UUID REFERENCES sexy_recordings(id) ON DELETE SET NULL, -- Optional reference
|
|
||||||
date_created TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_points_user ON sexy_user_points(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_points_date ON sexy_user_points(date_created);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_points_action ON sexy_user_points(action);
|
|
||||||
|
|
||||||
COMMENT ON TABLE sexy_user_points IS 'Individual point-earning actions for gamification system';
|
|
||||||
COMMENT ON COLUMN sexy_user_points.action IS 'Type of action: RECORDING_CREATE, RECORDING_PLAY, RECORDING_COMPLETE, COMMENT_CREATE, RECORDING_FEATURED';
|
|
||||||
COMMENT ON COLUMN sexy_user_points.points IS 'Raw points before time-weighted decay calculation';
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Table: sexy_achievements
|
|
||||||
-- ====================
|
|
||||||
-- Predefined achievement definitions
|
|
||||||
CREATE TABLE IF NOT EXISTS sexy_achievements (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
code VARCHAR(50) UNIQUE NOT NULL, -- Unique identifier (e.g., "first_recording", "recording_100")
|
|
||||||
name VARCHAR(255) NOT NULL, -- Display name
|
|
||||||
description TEXT, -- Achievement description
|
|
||||||
icon VARCHAR(255), -- Icon identifier or emoji
|
|
||||||
category VARCHAR(50) NOT NULL, -- e.g., "recordings", "playback", "social", "special"
|
|
||||||
required_count INTEGER, -- Number of actions needed to unlock
|
|
||||||
points_reward INTEGER DEFAULT 0, -- Bonus points awarded upon unlock
|
|
||||||
sort INTEGER DEFAULT 0, -- Display order
|
|
||||||
status VARCHAR(20) DEFAULT 'published' -- published, draft, archived
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_achievements_category ON sexy_achievements(category);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_achievements_code ON sexy_achievements(code);
|
|
||||||
|
|
||||||
COMMENT ON TABLE sexy_achievements IS 'Predefined achievement definitions for gamification';
|
|
||||||
COMMENT ON COLUMN sexy_achievements.code IS 'Unique code used in backend logic (e.g., first_recording, play_100)';
|
|
||||||
COMMENT ON COLUMN sexy_achievements.category IS 'Achievement category: recordings, playback, social, special';
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Table: sexy_user_achievements
|
|
||||||
-- ====================
|
|
||||||
-- Junction table tracking unlocked achievements per user
|
|
||||||
CREATE TABLE IF NOT EXISTS sexy_user_achievements (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID NOT NULL REFERENCES directus_users(id) ON DELETE CASCADE,
|
|
||||||
achievement_id UUID NOT NULL REFERENCES sexy_achievements(id) ON DELETE CASCADE,
|
|
||||||
progress INTEGER DEFAULT 0, -- Current progress toward unlocking
|
|
||||||
date_unlocked TIMESTAMP WITH TIME ZONE, -- NULL if not yet unlocked
|
|
||||||
UNIQUE(user_id, achievement_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_achievements_user ON sexy_user_achievements(user_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_achievements_achievement ON sexy_user_achievements(achievement_id);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_achievements_unlocked ON sexy_user_achievements(date_unlocked) WHERE date_unlocked IS NOT NULL;
|
|
||||||
|
|
||||||
COMMENT ON TABLE sexy_user_achievements IS 'Tracks which achievements users have unlocked';
|
|
||||||
COMMENT ON COLUMN sexy_user_achievements.progress IS 'Current progress (e.g., 7/10 recordings created)';
|
|
||||||
COMMENT ON COLUMN sexy_user_achievements.date_unlocked IS 'NULL if achievement not yet unlocked';
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Table: sexy_user_stats
|
|
||||||
-- ====================
|
|
||||||
-- Cached aggregate statistics for efficient leaderboard queries
|
|
||||||
CREATE TABLE IF NOT EXISTS sexy_user_stats (
|
|
||||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
||||||
user_id UUID UNIQUE NOT NULL REFERENCES directus_users(id) ON DELETE CASCADE,
|
|
||||||
total_raw_points INTEGER DEFAULT 0, -- Sum of all points (no decay)
|
|
||||||
total_weighted_points NUMERIC(10,2) DEFAULT 0, -- Time-weighted score for rankings
|
|
||||||
recordings_count INTEGER DEFAULT 0, -- Number of published recordings
|
|
||||||
playbacks_count INTEGER DEFAULT 0, -- Number of recordings played
|
|
||||||
comments_count INTEGER DEFAULT 0, -- Number of comments on recordings
|
|
||||||
achievements_count INTEGER DEFAULT 0, -- Number of unlocked achievements
|
|
||||||
last_updated TIMESTAMP WITH TIME ZONE DEFAULT NOW() -- Cache timestamp
|
|
||||||
);
|
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_stats_weighted ON sexy_user_stats(total_weighted_points DESC);
|
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_stats_user ON sexy_user_stats(user_id);
|
|
||||||
|
|
||||||
COMMENT ON TABLE sexy_user_stats IS 'Cached user statistics for fast leaderboard queries';
|
|
||||||
COMMENT ON COLUMN sexy_user_stats.total_raw_points IS 'Sum of all points without time decay';
|
|
||||||
COMMENT ON COLUMN sexy_user_stats.total_weighted_points IS 'Time-weighted score using exponential decay (λ=0.005)';
|
|
||||||
COMMENT ON COLUMN sexy_user_stats.last_updated IS 'Timestamp for cache invalidation';
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Insert Initial Achievements
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
-- 🎬 Recordings (Creation)
|
|
||||||
INSERT INTO sexy_achievements (code, name, description, icon, category, required_count, points_reward, sort) VALUES
|
|
||||||
('first_recording', 'First Recording', 'Create your first recording', '🎬', 'recordings', 1, 50, 1),
|
|
||||||
('recording_10', 'Recording Enthusiast', 'Create 10 recordings', '📹', 'recordings', 10, 100, 2),
|
|
||||||
('recording_50', 'Prolific Creator', 'Create 50 recordings', '🎥', 'recordings', 50, 500, 3),
|
|
||||||
('recording_100', 'Recording Master', 'Create 100 recordings', '🏆', 'recordings', 100, 1000, 4),
|
|
||||||
('featured_recording', 'Featured Creator', 'Get a recording featured', '⭐', 'recordings', 1, 200, 5)
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- ▶️ Playback (Consumption)
|
|
||||||
INSERT INTO sexy_achievements (code, name, description, icon, category, required_count, points_reward, sort) VALUES
|
|
||||||
('first_play', 'First Play', 'Play your first recording', '▶️', 'playback', 1, 25, 10),
|
|
||||||
('play_100', 'Active Player', 'Play 100 recordings', '🎮', 'playback', 100, 250, 11),
|
|
||||||
('play_500', 'Playback Enthusiast', 'Play 500 recordings', '🔥', 'playback', 500, 1000, 12),
|
|
||||||
('completionist_10', 'Completionist', 'Complete 10 recordings to 90%+', '✅', 'playback', 10, 100, 13),
|
|
||||||
('completionist_100', 'Super Completionist', 'Complete 100 recordings', '💯', 'playback', 100, 500, 14)
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- 💬 Social (Community)
|
|
||||||
INSERT INTO sexy_achievements (code, name, description, icon, category, required_count, points_reward, sort) VALUES
|
|
||||||
('first_comment', 'First Comment', 'Leave your first comment', '💬', 'social', 1, 25, 20),
|
|
||||||
('comment_50', 'Conversationalist', 'Leave 50 comments', '💭', 'social', 50, 200, 21),
|
|
||||||
('comment_250', 'Community Voice', 'Leave 250 comments', '📣', 'social', 250, 750, 22)
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- ⭐ Special (Milestones)
|
|
||||||
INSERT INTO sexy_achievements (code, name, description, icon, category, required_count, points_reward, sort) VALUES
|
|
||||||
('early_adopter', 'Early Adopter', 'Join in the first month', '🚀', 'special', 1, 500, 30),
|
|
||||||
('one_year', 'One Year Anniversary', 'Be a member for 1 year', '🎂', 'special', 1, 1000, 31),
|
|
||||||
('balanced_creator', 'Balanced Creator', '50 recordings + 100 plays', '⚖️', 'special', 1, 500, 32),
|
|
||||||
('top_10_rank', 'Top 10 Leaderboard', 'Reach top 10 on leaderboard', '🏅', 'special', 1, 2000, 33)
|
|
||||||
ON CONFLICT (code) DO NOTHING;
|
|
||||||
|
|
||||||
-- ====================
|
|
||||||
-- Verification Queries
|
|
||||||
-- ====================
|
|
||||||
|
|
||||||
-- Count tables created
|
|
||||||
SELECT
|
|
||||||
'sexy_recording_plays' as table_name,
|
|
||||||
COUNT(*) as row_count
|
|
||||||
FROM sexy_recording_plays
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'sexy_user_points', COUNT(*) FROM sexy_user_points
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'sexy_achievements', COUNT(*) FROM sexy_achievements
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'sexy_user_achievements', COUNT(*) FROM sexy_user_achievements
|
|
||||||
UNION ALL
|
|
||||||
SELECT 'sexy_user_stats', COUNT(*) FROM sexy_user_stats;
|
|
||||||
|
|
||||||
-- Show created achievements
|
|
||||||
SELECT
|
|
||||||
category,
|
|
||||||
COUNT(*) as achievement_count
|
|
||||||
FROM sexy_achievements
|
|
||||||
GROUP BY category
|
|
||||||
ORDER BY category;
|
|
||||||
13
package.json
13
package.json
@@ -7,13 +7,16 @@
|
|||||||
"test": "echo \"Error: no test specified\" && exit 1",
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
"build:bundle": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/bundle build",
|
"build:bundle": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/bundle build",
|
||||||
"build:frontend": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/frontend build",
|
"build:frontend": "git pull && pnpm install && pnpm --filter @sexy.pivoine.art/frontend build",
|
||||||
"dev:data": "cd ../compose/data && docker compose up -d",
|
"dev": "pnpm build:bundle && docker compose up -d && pnpm --filter @sexy.pivoine.art/frontend dev",
|
||||||
"dev:directus": "cd ../compose/sexy && docker compose --env-file=.env.local up -d directus",
|
"schema:export": "docker compose exec directus node /directus/cli.js schema snapshot --yes /tmp/snapshot.yml && docker compose cp directus:/tmp/snapshot.yml ./directus.yml && docker compose exec db pg_dump -U sexy --schema-only sexy > schema.sql",
|
||||||
"dev": "pnpm dev:data && pnpm dev:directus && pnpm --filter @sexy.pivoine.art/frontend dev"
|
"schema:import": "docker compose exec -T postgres psql -U sexy sexy < schema.sql && docker compose cp ./directus.yml directus:/tmp/snapshot.yml && docker compose exec directus node /directus/cli.js schema apply --yes /tmp/snapshot.yml"
|
||||||
},
|
},
|
||||||
"keywords": [],
|
"keywords": [],
|
||||||
"author": "",
|
"author": {
|
||||||
"license": "ISC",
|
"name": "Valknar",
|
||||||
|
"email": "valknar@pivoine.art"
|
||||||
|
},
|
||||||
|
"license": "MIT",
|
||||||
"packageManager": "pnpm@10.19.0",
|
"packageManager": "pnpm@10.19.0",
|
||||||
"pnpm": {
|
"pnpm": {
|
||||||
"onlyBuiltDependencies": [
|
"onlyBuiltDependencies": [
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
PUBLIC_API_URL=https://sexy.pivoine.art/api
|
|
||||||
PUBLIC_URL=https://sexy.pivoine.art
|
|
||||||
PUBLIC_UMAMI_ID=
|
|
||||||
PUBLIC_UMAMI_SCRIPT=
|
|
||||||
2667
schema.sql
Normal file
2667
schema.sql
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user