Compare commits
2 Commits
ad83fb553a
...
83ca9d4fb5
| Author | SHA1 | Date | |
|---|---|---|---|
| 83ca9d4fb5 | |||
| 225b9d41f5 |
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:
|
volumes:
|
||||||
- ${SEXY_FRONTEND_PATH:-./}:/home/node/app
|
directus_uploads:
|
||||||
|
|
||||||
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:
|
|
||||||
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": [
|
||||||
|
|||||||
@@ -45,7 +45,7 @@
|
|||||||
"add": "directus-extension add"
|
"add": "directus-extension add"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@directus/extensions-sdk": "16.0.2"
|
"@directus/extensions-sdk": "17.0.9"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@sindresorhus/slugify": "^3.0.0",
|
"@sindresorhus/slugify": "^3.0.0",
|
||||||
|
|||||||
@@ -13,13 +13,13 @@
|
|||||||
"build:wasm": "wasm-pack build --out-dir wasm --out-name index --target bundler --release"
|
"build:wasm": "wasm-pack build --out-dir wasm --out-name index --target bundler --release"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"eventemitter3": "^5.0.1",
|
"eventemitter3": "^5.0.4",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.4",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-wasm": "3.5.0",
|
"vite-plugin-wasm": "3.5.0",
|
||||||
"ws": "^8.18.3"
|
"ws": "^8.19.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"wasm-pack": "^0.13.1"
|
"wasm-pack": "^0.14.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
PUBLIC_API_URL=https://sexy.pivoine.art/api
|
|
||||||
PUBLIC_URL=https://sexy.pivoine.art
|
|
||||||
PUBLIC_UMAMI_ID=
|
|
||||||
PUBLIC_UMAMI_SCRIPT=
|
|
||||||
@@ -11,39 +11,39 @@
|
|||||||
"start": "node ./build"
|
"start": "node ./build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify-json/ri": "^1.2.5",
|
"@iconify-json/ri": "^1.2.10",
|
||||||
"@iconify/tailwind4": "^1.0.6",
|
"@iconify/tailwind4": "^1.2.1",
|
||||||
"@internationalized/date": "^3.8.2",
|
"@internationalized/date": "^3.11.0",
|
||||||
"@lucide/svelte": "^0.544.0",
|
"@lucide/svelte": "^0.577.0",
|
||||||
"@sveltejs/adapter-node": "^5.3.1",
|
"@sveltejs/adapter-node": "^5.5.4",
|
||||||
"@sveltejs/adapter-static": "^3.0.9",
|
"@sveltejs/adapter-static": "^3.0.10",
|
||||||
"@sveltejs/kit": "^2.37.0",
|
"@sveltejs/kit": "^2.53.4",
|
||||||
"@sveltejs/vite-plugin-svelte": "^6.1.4",
|
"@sveltejs/vite-plugin-svelte": "^6.2.4",
|
||||||
"@tailwindcss/forms": "^0.5.9",
|
"@tailwindcss/forms": "^0.5.11",
|
||||||
"@tailwindcss/typography": "^0.5.15",
|
"@tailwindcss/typography": "^0.5.19",
|
||||||
"@tailwindcss/vite": "^4.0.0",
|
"@tailwindcss/vite": "^4.2.1",
|
||||||
"@tsconfig/svelte": "^5.0.5",
|
"@tsconfig/svelte": "^5.0.8",
|
||||||
"bits-ui": "2.11.0",
|
"bits-ui": "2.16.2",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"glob": "^11.0.3",
|
"glob": "^13.0.6",
|
||||||
"mode-watcher": "^1.1.0",
|
"mode-watcher": "^1.1.0",
|
||||||
"prettier-plugin-svelte": "^3.4.0",
|
"prettier-plugin-svelte": "^3.5.1",
|
||||||
"super-sitemap": "^1.0.5",
|
"super-sitemap": "^1.0.7",
|
||||||
"svelte": "^5.38.6",
|
"svelte": "^5.53.7",
|
||||||
"svelte-sonner": "^1.0.5",
|
"svelte-sonner": "^1.0.8",
|
||||||
"tailwind-merge": "^3.3.1",
|
"tailwind-merge": "^3.5.0",
|
||||||
"tailwind-variants": "^1.0.0",
|
"tailwind-variants": "^3.2.2",
|
||||||
"tailwindcss": "^4.0.0",
|
"tailwindcss": "^4.2.1",
|
||||||
"tw-animate-css": "^1.3.8",
|
"tw-animate-css": "^1.4.0",
|
||||||
"typescript": "^5.9.2",
|
"typescript": "^5.9.3",
|
||||||
"vite": "^7.1.4",
|
"vite": "^7.3.1",
|
||||||
"vite-plugin-wasm": "3.5.0"
|
"vite-plugin-wasm": "3.5.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@directus/sdk": "^20.0.3",
|
"@directus/sdk": "^21.1.0",
|
||||||
"@sexy.pivoine.art/buttplug": "workspace:*",
|
"@sexy.pivoine.art/buttplug": "workspace:*",
|
||||||
"javascript-time-ago": "^2.5.11",
|
"javascript-time-ago": "^2.6.4",
|
||||||
"media-chrome": "^4.13.1",
|
"media-chrome": "^4.18.0",
|
||||||
"svelte-i18n": "^4.0.1"
|
"svelte-i18n": "^4.0.1"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4452
pnpm-lock.yaml
generated
4452
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
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