env vars
This commit is contained in:
@@ -1,45 +1,84 @@
|
||||
# ==========================================
|
||||
# GridPilot Development Environment
|
||||
# ==========================================
|
||||
# Used by `docker-compose.dev.yml` via `env_file: .env.development`.
|
||||
|
||||
# Node Environment
|
||||
# ------------------------------------------
|
||||
# Runtime
|
||||
# ------------------------------------------
|
||||
NODE_ENV=development
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# ------------------------------------------
|
||||
# API (NestJS)
|
||||
# ------------------------------------------
|
||||
# API persistence is inferred from DATABASE_URL by default.
|
||||
# GRIDPILOT_API_PERSISTENCE=postgres
|
||||
|
||||
# ==========================================
|
||||
# Database (PostgreSQL)
|
||||
# ==========================================
|
||||
DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev
|
||||
|
||||
# Postgres container vars (used by `docker-compose.dev.yml` -> `db`)
|
||||
POSTGRES_DB=gridpilot_dev
|
||||
POSTGRES_USER=gridpilot_user
|
||||
POSTGRES_PASSWORD=gridpilot_dev_pass
|
||||
|
||||
# ==========================================
|
||||
# API Configuration
|
||||
# ==========================================
|
||||
API_PORT=3000
|
||||
API_HOST=0.0.0.0
|
||||
|
||||
# ==========================================
|
||||
# Website Configuration
|
||||
# ==========================================
|
||||
# ------------------------------------------
|
||||
# Website (Next.js) - public (exposed to browser)
|
||||
# ------------------------------------------
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||
NEXT_PUBLIC_API_URL=http://localhost:3001
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# ==========================================
|
||||
# Vercel KV (Optional in Development)
|
||||
# ==========================================
|
||||
# Browser → API base URL (host port 3001 -> container port 3000)
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
|
||||
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
NEXT_PUBLIC_X_URL=https://x.com/your-handle
|
||||
|
||||
# Optional site/legal metadata (defaults used when unset)
|
||||
# NEXT_PUBLIC_SITE_NAME=GridPilot
|
||||
# NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
|
||||
# NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@example.com
|
||||
# NEXT_PUBLIC_LEGAL_COMPANY_NAME=
|
||||
# NEXT_PUBLIC_LEGAL_VAT_ID=
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY=
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS=
|
||||
|
||||
# ------------------------------------------
|
||||
# Vercel KV (optional in dev)
|
||||
# ------------------------------------------
|
||||
# If unset, the Website uses an in-memory fallback with a warning.
|
||||
# KV_REST_API_URL=
|
||||
# KV_REST_API_TOKEN=
|
||||
|
||||
# ==========================================
|
||||
# Automation Mode
|
||||
# ==========================================
|
||||
AUTOMATION_MODE=dev
|
||||
# ------------------------------------------
|
||||
# Automation / Companion (advanced)
|
||||
# ------------------------------------------
|
||||
# Prefer using NODE_ENV=development/test. `AUTOMATION_MODE` is legacy & deprecated.
|
||||
# AUTOMATION_MODE=dev
|
||||
CHROME_DEBUG_PORT=9222
|
||||
# CHROME_WS_ENDPOINT=ws://127.0.0.1:9222/devtools/browser/<id>
|
||||
|
||||
# Native automation tuning (optional)
|
||||
# IRACING_WINDOW_TITLE=iRacing
|
||||
# TEMPLATE_PATH=./resources/templates/iracing
|
||||
# OCR_CONFIDENCE=0.9
|
||||
# NUTJS_MOUSE_SPEED=1000
|
||||
# NUTJS_KEYBOARD_DELAY=50
|
||||
# AUTOMATION_MAX_RETRIES=3
|
||||
# AUTOMATION_BASE_DELAY_MS=500
|
||||
# AUTOMATION_MAX_DELAY_MS=5000
|
||||
# AUTOMATION_BACKOFF_MULTIPLIER=2
|
||||
# AUTOMATION_PAGE_LOAD_WAIT_MS=5000
|
||||
# AUTOMATION_INTER_ACTION_DELAY_MS=200
|
||||
# AUTOMATION_POST_CLICK_DELAY_MS=300
|
||||
# AUTOMATION_PRE_STEP_DELAY_MS=100
|
||||
|
||||
AUTOMATION_TIMEOUT=30000
|
||||
RETRY_ATTEMPTS=3
|
||||
SCREENSHOT_ON_ERROR=true
|
||||
|
||||
# Logging (automation/adapters)
|
||||
# LOG_LEVEL=debug
|
||||
# LOG_FILE_PATH=./logs/gridpilot
|
||||
# LOG_MAX_FILES=7
|
||||
# LOG_MAX_SIZE=10m
|
||||
|
||||
@@ -1,18 +1,89 @@
|
||||
# Development Environment Configuration
|
||||
# Copy this file to .env.development and adjust values as needed
|
||||
# ==========================================
|
||||
# GridPilot Development Environment Example
|
||||
# ==========================================
|
||||
# Copy to `.env.development` and adjust values as needed.
|
||||
#
|
||||
# This file is consumed by `docker-compose.dev.yml` (API, Website, Postgres).
|
||||
|
||||
# Automation mode: 'dev' | 'production' | 'mock'
|
||||
AUTOMATION_MODE=dev
|
||||
# ------------------------------------------
|
||||
# Runtime
|
||||
# ------------------------------------------
|
||||
NODE_ENV=development
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# Chrome DevTools settings (for dev mode)
|
||||
# ------------------------------------------
|
||||
# API (NestJS)
|
||||
# ------------------------------------------
|
||||
# API persistence is inferred from DATABASE_URL by default.
|
||||
# GRIDPILOT_API_PERSISTENCE=postgres
|
||||
|
||||
DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev
|
||||
|
||||
# Postgres container vars (used by docker `db` service)
|
||||
POSTGRES_DB=gridpilot_dev
|
||||
POSTGRES_USER=gridpilot_user
|
||||
POSTGRES_PASSWORD=gridpilot_dev_pass
|
||||
|
||||
# ------------------------------------------
|
||||
# Website (Next.js) - public (exposed to browser)
|
||||
# ------------------------------------------
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:3000
|
||||
|
||||
# Browser → API base URL (host port 3001 -> container port 3000)
|
||||
NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
|
||||
|
||||
# Optional links / metadata
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
NEXT_PUBLIC_X_URL=https://x.com/your-handle
|
||||
# NEXT_PUBLIC_SITE_NAME=GridPilot
|
||||
# NEXT_PUBLIC_SUPPORT_EMAIL=support@example.com
|
||||
# NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@example.com
|
||||
# NEXT_PUBLIC_LEGAL_COMPANY_NAME=
|
||||
# NEXT_PUBLIC_LEGAL_VAT_ID=
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY=
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS=
|
||||
|
||||
# ------------------------------------------
|
||||
# Vercel KV (optional in dev)
|
||||
# ------------------------------------------
|
||||
# If unset, the Website uses an in-memory fallback with a warning.
|
||||
# KV_REST_API_URL=
|
||||
# KV_REST_API_TOKEN=
|
||||
|
||||
# ------------------------------------------
|
||||
# Automation / Companion (advanced)
|
||||
# ------------------------------------------
|
||||
# Prefer using NODE_ENV=development/test. `AUTOMATION_MODE` is legacy & deprecated.
|
||||
# AUTOMATION_MODE=dev
|
||||
CHROME_DEBUG_PORT=9222
|
||||
# CHROME_WS_ENDPOINT=ws://127.0.0.1:9222/devtools/browser/<id>
|
||||
|
||||
# Shared automation settings
|
||||
# Native automation tuning (optional)
|
||||
# IRACING_WINDOW_TITLE=iRacing
|
||||
# TEMPLATE_PATH=./resources/templates/iracing
|
||||
# OCR_CONFIDENCE=0.9
|
||||
# NUTJS_MOUSE_SPEED=1000
|
||||
# NUTJS_KEYBOARD_DELAY=50
|
||||
# AUTOMATION_MAX_RETRIES=3
|
||||
# AUTOMATION_BASE_DELAY_MS=500
|
||||
# AUTOMATION_MAX_DELAY_MS=5000
|
||||
# AUTOMATION_BACKOFF_MULTIPLIER=2
|
||||
# AUTOMATION_PAGE_LOAD_WAIT_MS=5000
|
||||
# AUTOMATION_INTER_ACTION_DELAY_MS=200
|
||||
# AUTOMATION_POST_CLICK_DELAY_MS=300
|
||||
# AUTOMATION_PRE_STEP_DELAY_MS=100
|
||||
|
||||
AUTOMATION_TIMEOUT=30000
|
||||
RETRY_ATTEMPTS=3
|
||||
SCREENSHOT_ON_ERROR=true
|
||||
|
||||
# Logging (automation/adapters)
|
||||
# LOG_LEVEL=debug
|
||||
# LOG_FILE_PATH=./logs/gridpilot
|
||||
# LOG_MAX_FILES=7
|
||||
# LOG_MAX_SIZE=10m
|
||||
|
||||
# Start Chrome with debugging enabled:
|
||||
# /Applications/Google Chrome.app/Contents/MacOS/Google Chrome --remote-debugging-port=9222 --user-data-dir=/tmp/chrome-debug
|
||||
# Or use: npm run chrome:debug
|
||||
@@ -1,55 +1,78 @@
|
||||
# ==========================================
|
||||
# GridPilot Production Environment
|
||||
# ==========================================
|
||||
# Used by `docker-compose.prod.yml` via `env_file: .env.production`.
|
||||
#
|
||||
# Security:
|
||||
# - Do NOT commit real secrets (use a secret manager / CI vars).
|
||||
# - Replace all `CHANGE_ME_...` values before deploying.
|
||||
|
||||
# Node Environment
|
||||
# ------------------------------------------
|
||||
# Runtime
|
||||
# ------------------------------------------
|
||||
NODE_ENV=production
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# ==========================================
|
||||
# Database (PostgreSQL)
|
||||
# ==========================================
|
||||
# IMPORTANT: Change these credentials in production!
|
||||
# ------------------------------------------
|
||||
# API (NestJS)
|
||||
# ------------------------------------------
|
||||
# Controls whether the API uses Postgres or runs in-memory.
|
||||
# Values: `postgres` | `inmemory`
|
||||
GRIDPILOT_API_PERSISTENCE=postgres
|
||||
|
||||
# Optional: bootstrap demo data (default: true)
|
||||
# GRIDPILOT_API_BOOTSTRAP=true
|
||||
|
||||
# Database connection for the API (preferred: single URL)
|
||||
DATABASE_URL=postgres://gridpilot_user:CHANGE_ME_IN_PRODUCTION@db:5432/gridpilot_prod
|
||||
|
||||
# Postgres container vars (used by `docker-compose.prod.yml` -> `db` service)
|
||||
POSTGRES_DB=gridpilot_prod
|
||||
POSTGRES_USER=gridpilot_user
|
||||
POSTGRES_PASSWORD=CHANGE_ME_IN_PRODUCTION
|
||||
|
||||
# ==========================================
|
||||
# Redis Cache
|
||||
# ==========================================
|
||||
# IMPORTANT: Change password in production!
|
||||
REDIS_URL=redis://:CHANGE_ME_IN_PRODUCTION@redis:6379
|
||||
# ------------------------------------------
|
||||
# Redis container vars (used by `docker-compose.prod.yml` -> `redis` service)
|
||||
# ------------------------------------------
|
||||
REDIS_PASSWORD=CHANGE_ME_IN_PRODUCTION
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ==========================================
|
||||
# API Configuration
|
||||
# ==========================================
|
||||
API_PORT=3000
|
||||
API_HOST=0.0.0.0
|
||||
|
||||
# ==========================================
|
||||
# Website Configuration
|
||||
# ==========================================
|
||||
# ------------------------------------------
|
||||
# Website (Next.js) - public (exposed to browser)
|
||||
# ------------------------------------------
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
NEXT_PUBLIC_SITE_URL=http://localhost:80
|
||||
NEXT_PUBLIC_API_URL=http://localhost:80/api
|
||||
NEXT_PUBLIC_SITE_URL=https://your-domain.com
|
||||
|
||||
# Browser → API base URL.
|
||||
# If nginx proxies `/api` to the API service, this is typically:
|
||||
# https://your-domain.com/api
|
||||
NEXT_PUBLIC_API_BASE_URL=https://your-domain.com/api
|
||||
|
||||
# Optional links / metadata (defaults used when unset)
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
NEXT_PUBLIC_X_URL=https://x.com/your-handle
|
||||
# NEXT_PUBLIC_SITE_NAME=GridPilot
|
||||
# NEXT_PUBLIC_SUPPORT_EMAIL=support@your-domain.com
|
||||
# NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@your-domain.com
|
||||
# NEXT_PUBLIC_LEGAL_COMPANY_NAME=Your Company GmbH
|
||||
# NEXT_PUBLIC_LEGAL_VAT_ID=DE123456789
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY=DE
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS=Street 1, 12345 City
|
||||
|
||||
# ==========================================
|
||||
# Vercel KV (REQUIRED in Production)
|
||||
# ==========================================
|
||||
# For local testing, these can be left as placeholders
|
||||
# In production, get these from: https://vercel.com/dashboard -> Storage -> KV
|
||||
KV_REST_API_URL=https://placeholder-kv.vercel-storage.com
|
||||
KV_REST_API_TOKEN=placeholder_kv_token
|
||||
# ------------------------------------------
|
||||
# Website (Next.js) - server (NOT exposed to browser)
|
||||
# ------------------------------------------
|
||||
# SSR/server-to-server base URL inside Docker network
|
||||
API_BASE_URL=http://api:3000
|
||||
|
||||
# ==========================================
|
||||
# Automation Mode
|
||||
# ==========================================
|
||||
AUTOMATION_MODE=production
|
||||
# Vercel KV (required for email signup storage/rate limit in production)
|
||||
KV_REST_API_URL=https://your-kv-rest-api-url.vercel-storage.com
|
||||
KV_REST_API_TOKEN=CHANGE_ME_IN_PRODUCTION
|
||||
|
||||
# ------------------------------------------
|
||||
# Automation / Companion (advanced)
|
||||
# ------------------------------------------
|
||||
# NOTE: `AUTOMATION_MODE` is deprecated (see `getAutomationMode()` in adapters).
|
||||
# AUTOMATION_MODE=production
|
||||
AUTOMATION_TIMEOUT=30000
|
||||
RETRY_ATTEMPTS=3
|
||||
SCREENSHOT_ON_ERROR=false
|
||||
@@ -1,62 +1,70 @@
|
||||
# ==========================================
|
||||
# GridPilot Production Environment Example
|
||||
# ==========================================
|
||||
# Copy this file to .env.production and update with real credentials
|
||||
# Copy to `.env.production` and replace placeholders with real values.
|
||||
#
|
||||
# Do NOT commit real secrets.
|
||||
|
||||
# Node Environment
|
||||
# ------------------------------------------
|
||||
# Runtime
|
||||
# ------------------------------------------
|
||||
NODE_ENV=production
|
||||
|
||||
# ==========================================
|
||||
# Database (PostgreSQL)
|
||||
# ==========================================
|
||||
# Update these with your production database credentials
|
||||
DATABASE_URL=postgres://gridpilot_user:YOUR_SECURE_PASSWORD@db:5432/gridpilot_prod
|
||||
POSTGRES_DB=gridpilot_prod
|
||||
POSTGRES_USER=gridpilot_user
|
||||
POSTGRES_PASSWORD=YOUR_SECURE_PASSWORD
|
||||
|
||||
# ==========================================
|
||||
# Redis Cache
|
||||
# ==========================================
|
||||
# Update with your production Redis password
|
||||
REDIS_URL=redis://:YOUR_REDIS_PASSWORD@redis:6379
|
||||
REDIS_PASSWORD=YOUR_REDIS_PASSWORD
|
||||
REDIS_HOST=redis
|
||||
REDIS_PORT=6379
|
||||
|
||||
# ==========================================
|
||||
# API Configuration
|
||||
# ==========================================
|
||||
API_PORT=3000
|
||||
API_HOST=0.0.0.0
|
||||
|
||||
# ==========================================
|
||||
# Website Configuration
|
||||
# ==========================================
|
||||
# Update with your actual domain
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
NEXT_PUBLIC_SITE_URL=https://your-domain.com
|
||||
NEXT_PUBLIC_API_URL=https://api.your-domain.com
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
NEXT_TELEMETRY_DISABLED=1
|
||||
|
||||
# ==========================================
|
||||
# Vercel KV (REQUIRED in Production)
|
||||
# ==========================================
|
||||
# Get these from: https://vercel.com/dashboard -> Storage -> KV
|
||||
KV_REST_API_URL=https://your-kv-rest-api-url.vercel-storage.com
|
||||
KV_REST_API_TOKEN=your_kv_rest_api_token_here
|
||||
# ------------------------------------------
|
||||
# API (NestJS)
|
||||
# ------------------------------------------
|
||||
GRIDPILOT_API_PERSISTENCE=postgres
|
||||
# GRIDPILOT_API_BOOTSTRAP=true
|
||||
|
||||
# ==========================================
|
||||
# Automation Mode
|
||||
# ==========================================
|
||||
AUTOMATION_MODE=production
|
||||
# Prefer a single connection URL (Docker: host `db`)
|
||||
DATABASE_URL=postgres://gridpilot_user:CHANGE_ME@db:5432/gridpilot_prod
|
||||
|
||||
# Postgres container vars (used by `docker-compose.prod.yml` -> `db`)
|
||||
POSTGRES_DB=gridpilot_prod
|
||||
POSTGRES_USER=gridpilot_user
|
||||
POSTGRES_PASSWORD=CHANGE_ME
|
||||
|
||||
# Redis container vars (used by `docker-compose.prod.yml` -> `redis`)
|
||||
REDIS_PASSWORD=CHANGE_ME
|
||||
|
||||
# ------------------------------------------
|
||||
# Website (Next.js) - public (exposed to browser)
|
||||
# ------------------------------------------
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
NEXT_PUBLIC_SITE_URL=https://your-domain.com
|
||||
|
||||
# Browser → API base URL.
|
||||
# If nginx proxies `/api` to the API service, this is typically:
|
||||
# https://your-domain.com/api
|
||||
NEXT_PUBLIC_API_BASE_URL=https://your-domain.com/api
|
||||
|
||||
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
|
||||
NEXT_PUBLIC_X_URL=https://x.com/your-handle
|
||||
|
||||
# Optional site/legal metadata (defaults used when unset)
|
||||
# NEXT_PUBLIC_SITE_NAME=GridPilot
|
||||
# NEXT_PUBLIC_SUPPORT_EMAIL=support@your-domain.com
|
||||
# NEXT_PUBLIC_SPONSOR_EMAIL=sponsors@your-domain.com
|
||||
# NEXT_PUBLIC_LEGAL_COMPANY_NAME=Your Company GmbH
|
||||
# NEXT_PUBLIC_LEGAL_VAT_ID=DE123456789
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY=DE
|
||||
# NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS=Street 1, 12345 City
|
||||
|
||||
# ------------------------------------------
|
||||
# Website (Next.js) - server (NOT exposed to browser)
|
||||
# ------------------------------------------
|
||||
API_BASE_URL=http://api:3000
|
||||
|
||||
# Vercel KV (required in production)
|
||||
KV_REST_API_URL=https://your-kv-rest-api-url.vercel-storage.com
|
||||
KV_REST_API_TOKEN=CHANGE_ME
|
||||
|
||||
# ------------------------------------------
|
||||
# Automation / Companion (advanced)
|
||||
# ------------------------------------------
|
||||
# NOTE: `AUTOMATION_MODE` is deprecated.
|
||||
# AUTOMATION_MODE=production
|
||||
AUTOMATION_TIMEOUT=30000
|
||||
RETRY_ATTEMPTS=3
|
||||
SCREENSHOT_ON_ERROR=false
|
||||
|
||||
# ==========================================
|
||||
# Security & Performance
|
||||
# ==========================================
|
||||
# Add any additional production-specific variables here
|
||||
# Example: API keys, webhook URLs, etc.
|
||||
SCREENSHOT_ON_ERROR=false
|
||||
@@ -1,10 +1,28 @@
|
||||
# Test Environment Configuration
|
||||
# Copy this file to .env.test and adjust values as needed
|
||||
# Test Environment Configuration (automation stack)
|
||||
# Copy this file to `.env.test` and adjust values as needed.
|
||||
#
|
||||
# Scope:
|
||||
# - This file is for the hosted-session automation stack (companion/automation).
|
||||
# - It is NOT used by the Website ↔ API Docker smoke stack (that stack is driven by env vars in
|
||||
# [`docker-compose.test.yml`](docker-compose.test.yml:1) and scripts in [`package.json`](package.json:92)).
|
||||
#
|
||||
# Note on legacy config:
|
||||
# - `AUTOMATION_MODE` is deprecated in favor of `NODE_ENV` mapping in [`getAutomationMode()`](adapters/automation/config/AutomationConfig.ts:104).
|
||||
# - Keep `AUTOMATION_MODE` only if you need to force legacy behavior.
|
||||
|
||||
# Use mock adapter for testing (no real browser automation)
|
||||
# Prefer this:
|
||||
NODE_ENV=test
|
||||
|
||||
# Optional legacy override (deprecated): `dev` | `mock` | `production`
|
||||
AUTOMATION_MODE=mock
|
||||
|
||||
# Test timeouts (can be shorter for faster tests)
|
||||
# Shared automation settings
|
||||
AUTOMATION_TIMEOUT=5000
|
||||
RETRY_ATTEMPTS=1
|
||||
SCREENSHOT_ON_ERROR=false
|
||||
SCREENSHOT_ON_ERROR=false
|
||||
|
||||
# Logging (automation/adapters)
|
||||
# LOG_LEVEL=warn
|
||||
# LOG_FILE_PATH=./logs/gridpilot
|
||||
# LOG_MAX_FILES=7
|
||||
# LOG_MAX_SIZE=10m
|
||||
@@ -115,8 +115,8 @@ Before deploying to production:
|
||||
- Update `DATABASE_URL` with production database
|
||||
|
||||
4. **Redis Password**:
|
||||
- Update `REDIS_PASSWORD` with strong password
|
||||
- Update `REDIS_URL` accordingly
|
||||
- Update `REDIS_PASSWORD` with a strong password
|
||||
- No `REDIS_URL` is required (the Redis container is configured via `REDIS_PASSWORD` in `docker-compose.prod.yml`)
|
||||
|
||||
5. **Vercel KV** (if using):
|
||||
- Get credentials from Vercel dashboard
|
||||
@@ -124,7 +124,7 @@ Before deploying to production:
|
||||
|
||||
6. **Domain Configuration**:
|
||||
- Update `NEXT_PUBLIC_SITE_URL` with your domain
|
||||
- Update `NEXT_PUBLIC_API_URL` with your API domain
|
||||
- Update `NEXT_PUBLIC_API_BASE_URL` with your public API base (often `https://your-domain.com/api` when nginx proxies `/api`)
|
||||
|
||||
7. **Build & Deploy**:
|
||||
```bash
|
||||
|
||||
@@ -13,13 +13,13 @@ npm run docker:dev:build
|
||||
|
||||
This will:
|
||||
- Start PostgreSQL database on port 5432
|
||||
- Start API on port 3000 (with debugger on 9229)
|
||||
- Start Website on port 3001
|
||||
- Start API on port 3001 (container port 3000, debugger 9229)
|
||||
- Start Website on port 3000
|
||||
- Enable hot-reloading for both apps
|
||||
|
||||
Access:
|
||||
- Website: http://localhost:3001
|
||||
- API: http://localhost:3000
|
||||
- Website: http://localhost:3000
|
||||
- API: http://localhost:3001
|
||||
- Database: localhost:5432
|
||||
|
||||
### Production
|
||||
@@ -54,16 +54,93 @@ Access:
|
||||
- `npm run docker:prod:logs` - View logs
|
||||
- `npm run docker:prod:clean` - Stop and remove volumes
|
||||
|
||||
### Testing (Docker)
|
||||
The goal of Docker-backed tests is to catch *wiring* issues between Website ↔ API (wrong hostnames/ports/env vars, missing CORS for credentialed requests, etc.) in a deterministic environment.
|
||||
|
||||
- `npm run test:docker:website` - Start a dedicated test stack (ports `3100/3101`) and run Playwright smoke tests against it.
|
||||
- Uses [`docker-compose.test.yml`](docker-compose.test.yml:1).
|
||||
- Runs the Website in Docker and talks to an **API mock** container.
|
||||
- Validates that pages render and that core API fetches succeed (hostname + CORS + routing).
|
||||
|
||||
Supporting scripts:
|
||||
- `npm run docker:test:deps` - Install monorepo deps inside a reusable Docker volume (one-shot).
|
||||
- `npm run docker:test:up` - Bring up the test stack.
|
||||
- `npm run docker:test:wait` - Wait for `http://localhost:3100` and `http://localhost:3101/health` to be ready.
|
||||
- `npm run docker:test:down` - Tear the test stack down (including volumes).
|
||||
|
||||
## Environment Variables
|
||||
|
||||
### “Mock vs Real” (Website & API)
|
||||
|
||||
There is **no** `AUTOMATION_MODE` equivalent for the Website/API runtime.
|
||||
|
||||
- **Website “mock vs real”** is controlled purely by *which API base URL you point it at* via [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6):
|
||||
- Browser calls use `NEXT_PUBLIC_API_BASE_URL`
|
||||
- Server/Next.js calls use `API_BASE_URL ?? NEXT_PUBLIC_API_BASE_URL`
|
||||
|
||||
- **API “mock vs real”** is controlled by API runtime env:
|
||||
- Persistence: `GRIDPILOT_API_PERSISTENCE=postgres|inmemory` in [`AppModule`](apps/api/src/app.module.ts:25)
|
||||
- Optional bootstrapping: `GRIDPILOT_API_BOOTSTRAP=0|1` in [`AppModule`](apps/api/src/app.module.ts:35)
|
||||
|
||||
Practical presets:
|
||||
- **Website + real API (Docker dev)**: `npm run docker:dev:build` (Website `3000`, API `3001`, Postgres required).
|
||||
- Website browser → API: `NEXT_PUBLIC_API_BASE_URL=http://localhost:3001`
|
||||
- Website container → API container: `API_BASE_URL=http://api:3000`
|
||||
- **Website + mock API (Docker smoke)**: `npm run test:docker:website` (Website `3100`, API mock `3101`).
|
||||
- API mock is defined inline in [`docker-compose.test.yml`](docker-compose.test.yml:24)
|
||||
- Website browser → API mock: `NEXT_PUBLIC_API_BASE_URL=http://localhost:3101`
|
||||
- Website container → API mock container: `API_BASE_URL=http://api:3000`
|
||||
|
||||
### Website ↔ API Connection
|
||||
|
||||
The website talks to the API via `fetch()` in [`BaseApiClient`](apps/website/lib/api/base/BaseApiClient.ts:11), and it always includes cookies (`credentials: 'include'`). That means:
|
||||
|
||||
- The **browser** must be pointed at a host-accessible API URL via `NEXT_PUBLIC_API_BASE_URL`
|
||||
- The **server** (Next.js / Node) must be pointed at a container-network API URL via `API_BASE_URL` (when running in Docker)
|
||||
|
||||
The single source of truth for “what base URL should I use?” is [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6):
|
||||
- Browser: reads `NEXT_PUBLIC_API_BASE_URL`
|
||||
- Server: reads `API_BASE_URL ?? NEXT_PUBLIC_API_BASE_URL`
|
||||
- In Docker/CI/test: throws if missing (no silent localhost fallback)
|
||||
|
||||
#### Dev Docker defaults (docker-compose.dev.yml)
|
||||
- Website: `http://localhost:3000`
|
||||
- API: `http://localhost:3001` (maps to container `api:3000`)
|
||||
- `NEXT_PUBLIC_API_BASE_URL=http://localhost:3001` (browser → host port)
|
||||
- `API_BASE_URL=http://api:3000` (website container → api container)
|
||||
|
||||
#### Test Docker defaults (docker-compose.test.yml)
|
||||
This stack is intended for deterministic smoke tests and uses different host ports to avoid colliding with `docker:dev`:
|
||||
|
||||
- Website: `http://localhost:3100`
|
||||
- API mock: `http://localhost:3101` (maps to container `api:3000`)
|
||||
- `NEXT_PUBLIC_API_BASE_URL=http://localhost:3101` (browser → host port)
|
||||
- `API_BASE_URL=http://api:3000` (website container → api container)
|
||||
|
||||
Important: the test stack’s API is a mock server defined inline in [`docker-compose.test.yml`](docker-compose.test.yml:24). It exists to validate Website ↔ API wiring, not domain correctness.
|
||||
|
||||
#### Troubleshooting
|
||||
- If `docker:dev` is running, use `npm run docker:dev:down` before `npm run test:docker:website` to avoid port conflicts.
|
||||
- If Docker volumes get stuck, run `npm run docker:test:down` (it uses `--remove-orphans` + `rm -f`).
|
||||
|
||||
### API “Real vs In-Memory” Mode
|
||||
|
||||
The API can now be run either:
|
||||
- **postgres**: loads [`DatabaseModule`](apps/api/src/domain/database/DatabaseModule.ts:1) (requires Postgres)
|
||||
- **inmemory**: does not load `DatabaseModule` (no Postgres required)
|
||||
|
||||
Control it with:
|
||||
- `GRIDPILOT_API_PERSISTENCE=postgres|inmemory` (defaults to `postgres` if `DATABASE_URL` is set, otherwise `inmemory`)
|
||||
- Optional: `GRIDPILOT_API_BOOTSTRAP=0` to skip [`BootstrapModule`](apps/api/src/domain/bootstrap/BootstrapModule.ts:1)
|
||||
|
||||
### Development (.env.development)
|
||||
Copy and customize as needed. Default values work out of the box.
|
||||
|
||||
### Production (.env.production)
|
||||
**IMPORTANT**: Update these before deploying:
|
||||
- Database credentials (POSTGRES_PASSWORD, DATABASE_URL)
|
||||
- API URLs (NEXT_PUBLIC_API_URL, NEXT_PUBLIC_SITE_URL)
|
||||
- Vercel KV credentials (required for production)
|
||||
- Database credentials (`POSTGRES_PASSWORD`, `DATABASE_URL`)
|
||||
- Website/API URLs (`NEXT_PUBLIC_API_BASE_URL`, `NEXT_PUBLIC_SITE_URL`)
|
||||
- Vercel KV credentials (`KV_REST_API_URL`, `KV_REST_API_TOKEN`) (required for production email signups/rate limit)
|
||||
|
||||
## Architecture
|
||||
|
||||
|
||||
44
adapters/env.d.ts
vendored
Normal file
44
adapters/env.d.ts
vendored
Normal file
@@ -0,0 +1,44 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
|
||||
// Automation (legacy + current)
|
||||
AUTOMATION_MODE?: 'dev' | 'production' | 'mock';
|
||||
AUTOMATION_TIMEOUT?: string;
|
||||
RETRY_ATTEMPTS?: string;
|
||||
SCREENSHOT_ON_ERROR?: string;
|
||||
|
||||
// Chrome / DevTools
|
||||
CHROME_DEBUG_PORT?: string;
|
||||
CHROME_WS_ENDPOINT?: string;
|
||||
|
||||
// Native automation tuning
|
||||
NUTJS_MOUSE_SPEED?: string;
|
||||
NUTJS_KEYBOARD_DELAY?: string;
|
||||
IRACING_WINDOW_TITLE?: string;
|
||||
TEMPLATE_PATH?: string;
|
||||
OCR_CONFIDENCE?: string;
|
||||
|
||||
// Retry tuning
|
||||
AUTOMATION_MAX_RETRIES?: string;
|
||||
AUTOMATION_BASE_DELAY_MS?: string;
|
||||
AUTOMATION_MAX_DELAY_MS?: string;
|
||||
AUTOMATION_BACKOFF_MULTIPLIER?: string;
|
||||
|
||||
// Timing tuning
|
||||
AUTOMATION_PAGE_LOAD_WAIT_MS?: string;
|
||||
AUTOMATION_INTER_ACTION_DELAY_MS?: string;
|
||||
AUTOMATION_POST_CLICK_DELAY_MS?: string;
|
||||
AUTOMATION_PRE_STEP_DELAY_MS?: string;
|
||||
|
||||
// Logging
|
||||
LOG_LEVEL?: 'debug' | 'info' | 'warn' | 'error' | 'fatal';
|
||||
LOG_FILE_PATH?: string;
|
||||
LOG_MAX_FILES?: string;
|
||||
LOG_MAX_SIZE?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -7,6 +7,6 @@
|
||||
"sourceMap": true,
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"include": ["**/*.ts", "../core/**/*.ts"],
|
||||
"include": ["**/*.ts", "**/*.d.ts", "../core/**/*.ts", "../core/**/*.d.ts"],
|
||||
"exclude": ["node_modules", "dist"]
|
||||
}
|
||||
@@ -17,12 +17,20 @@ import { RaceModule } from './domain/race/RaceModule';
|
||||
import { SponsorModule } from './domain/sponsor/SponsorModule';
|
||||
import { TeamModule } from './domain/team/TeamModule';
|
||||
|
||||
import { getApiPersistence, getEnableBootstrap } from './env';
|
||||
|
||||
const API_PERSISTENCE = getApiPersistence();
|
||||
const USE_DATABASE = API_PERSISTENCE === 'postgres';
|
||||
|
||||
// Keep bootstrap on by default; tests can disable explicitly.
|
||||
const ENABLE_BOOTSTRAP = getEnableBootstrap();
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
HelloModule,
|
||||
DatabaseModule,
|
||||
...(USE_DATABASE ? [DatabaseModule] : []),
|
||||
LoggingModule,
|
||||
BootstrapModule,
|
||||
...(ENABLE_BOOTSTRAP ? [BootstrapModule] : []),
|
||||
AnalyticsModule,
|
||||
AuthModule,
|
||||
DashboardModule,
|
||||
|
||||
34
apps/api/src/env.d.ts
vendored
Normal file
34
apps/api/src/env.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
|
||||
// API runtime toggles
|
||||
GRIDPILOT_API_PERSISTENCE?: 'postgres' | 'inmemory';
|
||||
GRIDPILOT_API_BOOTSTRAP?: string;
|
||||
GENERATE_OPENAPI?: string;
|
||||
|
||||
// Database (TypeORM)
|
||||
DATABASE_URL?: string;
|
||||
DATABASE_HOST?: string;
|
||||
DATABASE_PORT?: string;
|
||||
DATABASE_USER?: string;
|
||||
DATABASE_PASSWORD?: string;
|
||||
DATABASE_NAME?: string;
|
||||
|
||||
// Policy / operational mode
|
||||
GRIDPILOT_POLICY_CACHE_MS?: string;
|
||||
GRIDPILOT_POLICY_PATH?: string;
|
||||
GRIDPILOT_OPERATIONAL_MODE?: string;
|
||||
GRIDPILOT_FEATURES_JSON?: string;
|
||||
GRIDPILOT_MAINTENANCE_ALLOW_VIEW?: string;
|
||||
GRIDPILOT_MAINTENANCE_ALLOW_MUTATE?: string;
|
||||
|
||||
// Authorization
|
||||
GRIDPILOT_AUTHZ_CACHE_MS?: string;
|
||||
GRIDPILOT_USER_ROLES_JSON?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
62
apps/api/src/env.ts
Normal file
62
apps/api/src/env.ts
Normal file
@@ -0,0 +1,62 @@
|
||||
export type ApiPersistence = 'postgres' | 'inmemory';
|
||||
|
||||
function isTruthyEnv(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
return value !== '0' && value.toLowerCase() !== 'false';
|
||||
}
|
||||
|
||||
function isSet(value: string | undefined): boolean {
|
||||
return value !== undefined;
|
||||
}
|
||||
|
||||
function readLower(name: string): string | undefined {
|
||||
const raw = process.env[name];
|
||||
if (raw === undefined) return undefined;
|
||||
return raw.toLowerCase();
|
||||
}
|
||||
|
||||
function requireOneOf<T extends string>(name: string, value: string, allowed: readonly T[]): T {
|
||||
if ((allowed as readonly string[]).includes(value)) {
|
||||
return value as T;
|
||||
}
|
||||
|
||||
const valid = allowed.join(', ');
|
||||
throw new Error(`Invalid ${name}: "${value}". Must be one of: ${valid}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Controls whether the API uses Postgres or runs in-memory.
|
||||
*
|
||||
* If `GRIDPILOT_API_PERSISTENCE` is set, it must be `postgres|inmemory`.
|
||||
* Otherwise, it falls back to: `postgres` when `DATABASE_URL` exists, else `inmemory`.
|
||||
*/
|
||||
export function getApiPersistence(): ApiPersistence {
|
||||
const configured = readLower('GRIDPILOT_API_PERSISTENCE');
|
||||
if (configured) {
|
||||
return requireOneOf('GRIDPILOT_API_PERSISTENCE', configured, ['postgres', 'inmemory'] as const);
|
||||
}
|
||||
|
||||
return process.env.DATABASE_URL ? 'postgres' : 'inmemory';
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep bootstrap on by default; tests can disable explicitly.
|
||||
*
|
||||
* `GRIDPILOT_API_BOOTSTRAP` uses "truthy" parsing:
|
||||
* - false when unset / "0" / "false"
|
||||
* - true otherwise
|
||||
*/
|
||||
export function getEnableBootstrap(): boolean {
|
||||
const raw = process.env.GRIDPILOT_API_BOOTSTRAP;
|
||||
if (raw === undefined) return true;
|
||||
return isTruthyEnv(raw);
|
||||
}
|
||||
|
||||
/**
|
||||
* When set, the API will generate `openapi.json` and optionally reduce logging noise.
|
||||
*
|
||||
* Matches previous behavior: any value (even "0") counts as enabled if the var is present.
|
||||
*/
|
||||
export function getGenerateOpenapi(): boolean {
|
||||
return isSet(process.env.GENERATE_OPENAPI);
|
||||
}
|
||||
@@ -11,8 +11,11 @@ import { AuthenticationGuard } from './domain/auth/AuthenticationGuard';
|
||||
import { AuthorizationGuard } from './domain/auth/AuthorizationGuard';
|
||||
import { FeatureAvailabilityGuard } from './domain/policy/FeatureAvailabilityGuard';
|
||||
|
||||
import { getGenerateOpenapi } from './env';
|
||||
|
||||
async function bootstrap() {
|
||||
const app = await NestFactory.create(AppModule, process.env.GENERATE_OPENAPI ? { logger: false } : undefined);
|
||||
const generateOpenapi = getGenerateOpenapi();
|
||||
const app = await NestFactory.create(AppModule, generateOpenapi ? { logger: false } : undefined);
|
||||
|
||||
// Website runs on a different origin in dev/docker (e.g. http://localhost:3000 -> http://localhost:3001),
|
||||
// and our website HTTP client uses `credentials: 'include'`, so we must support CORS with credentials.
|
||||
@@ -64,7 +67,7 @@ async function bootstrap() {
|
||||
SwaggerModule.setup('api/docs', app as any, document);
|
||||
|
||||
// Export OpenAPI spec as JSON file when GENERATE_OPENAPI env var is set
|
||||
if (process.env.GENERATE_OPENAPI) {
|
||||
if (generateOpenapi) {
|
||||
const outputPath = join(__dirname, '../openapi.json');
|
||||
writeFileSync(outputPath, JSON.stringify(document, null, 2));
|
||||
console.log(`✅ OpenAPI spec generated at: ${outputPath}`);
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
],
|
||||
"extends": "../../tsconfig.base.json",
|
||||
"include": [
|
||||
"src/**/*"
|
||||
"src/**/*",
|
||||
"src/**/*.d.ts"
|
||||
]
|
||||
}
|
||||
25
apps/website/env.d.ts
vendored
25
apps/website/env.d.ts
vendored
@@ -47,8 +47,33 @@ declare module 'react/compiler-runtime' {
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
|
||||
// Website (public, exposed to browser)
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE?: 'pre-launch' | 'alpha';
|
||||
NEXT_PUBLIC_SITE_URL?: string;
|
||||
NEXT_PUBLIC_API_BASE_URL?: string;
|
||||
NEXT_PUBLIC_SITE_NAME?: string;
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL?: string;
|
||||
NEXT_PUBLIC_SPONSOR_EMAIL?: string;
|
||||
NEXT_PUBLIC_LEGAL_COMPANY_NAME?: string;
|
||||
NEXT_PUBLIC_LEGAL_VAT_ID?: string;
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY?: string;
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS?: string;
|
||||
NEXT_PUBLIC_DISCORD_URL?: string;
|
||||
NEXT_PUBLIC_X_URL?: string;
|
||||
NEXT_TELEMETRY_DISABLED?: string;
|
||||
|
||||
// Website (server-only)
|
||||
API_BASE_URL?: string;
|
||||
|
||||
// Vercel KV (server-only)
|
||||
KV_REST_API_URL?: string;
|
||||
KV_REST_API_TOKEN?: string;
|
||||
|
||||
// Build/CI toggles (server-only)
|
||||
CI?: string;
|
||||
DOCKER?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
96
apps/website/lib/config/env.ts
Normal file
96
apps/website/lib/config/env.ts
Normal file
@@ -0,0 +1,96 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
const urlOptional = z.string().url().optional();
|
||||
const stringOptional = z.string().optional();
|
||||
|
||||
const publicEnvSchema = z.object({
|
||||
NEXT_PUBLIC_GRIDPILOT_MODE: z.enum(['pre-launch', 'alpha']).optional(),
|
||||
NEXT_PUBLIC_SITE_URL: urlOptional,
|
||||
NEXT_PUBLIC_API_BASE_URL: urlOptional,
|
||||
|
||||
NEXT_PUBLIC_SITE_NAME: stringOptional,
|
||||
NEXT_PUBLIC_SUPPORT_EMAIL: stringOptional,
|
||||
NEXT_PUBLIC_SPONSOR_EMAIL: stringOptional,
|
||||
|
||||
NEXT_PUBLIC_LEGAL_COMPANY_NAME: stringOptional,
|
||||
NEXT_PUBLIC_LEGAL_VAT_ID: stringOptional,
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_COUNTRY: stringOptional,
|
||||
NEXT_PUBLIC_LEGAL_REGISTERED_ADDRESS: stringOptional,
|
||||
|
||||
NEXT_PUBLIC_DISCORD_URL: stringOptional,
|
||||
NEXT_PUBLIC_X_URL: stringOptional,
|
||||
});
|
||||
|
||||
const serverEnvSchema = z.object({
|
||||
API_BASE_URL: urlOptional,
|
||||
KV_REST_API_URL: urlOptional,
|
||||
KV_REST_API_TOKEN: stringOptional,
|
||||
|
||||
CI: stringOptional,
|
||||
DOCKER: stringOptional,
|
||||
NODE_ENV: z.enum(['development', 'production', 'test']).optional(),
|
||||
});
|
||||
|
||||
export type WebsitePublicEnv = z.infer<typeof publicEnvSchema>;
|
||||
export type WebsiteServerEnv = z.infer<typeof serverEnvSchema>;
|
||||
|
||||
function formatZodIssues(issues: z.ZodIssue[]): string {
|
||||
return issues
|
||||
.map((issue) => {
|
||||
const path = issue.path.join('.') || '(root)';
|
||||
return `${path}: ${issue.message}`;
|
||||
})
|
||||
.join('; ');
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Website env vars (server-side safe).
|
||||
* Only validates the variables we explicitly support.
|
||||
*/
|
||||
export function getWebsiteServerEnv(): WebsiteServerEnv {
|
||||
const result = serverEnvSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid website server env: ${formatZodIssues(result.error.issues)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses Website public env vars (safe on both server + client).
|
||||
* Note: on the client, only `NEXT_PUBLIC_*` vars are actually present.
|
||||
*/
|
||||
export function getWebsitePublicEnv(): WebsitePublicEnv {
|
||||
const result = publicEnvSchema.safeParse(process.env);
|
||||
if (!result.success) {
|
||||
throw new Error(`Invalid website public env: ${formatZodIssues(result.error.issues)}`);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export function isTruthyEnv(value: string | undefined): boolean {
|
||||
if (!value) return false;
|
||||
return value !== '0' && value.toLowerCase() !== 'false';
|
||||
}
|
||||
|
||||
/**
|
||||
* Matches the semantics used in [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6).
|
||||
*/
|
||||
export function isTestLikeEnvironment(): boolean {
|
||||
const { NODE_ENV, CI, DOCKER } = getWebsiteServerEnv();
|
||||
return NODE_ENV === 'test' || CI === 'true' || DOCKER === 'true';
|
||||
}
|
||||
|
||||
export function isProductionEnvironment(): boolean {
|
||||
return getWebsiteServerEnv().NODE_ENV === 'production';
|
||||
}
|
||||
|
||||
export function isKvConfigured(): boolean {
|
||||
const { KV_REST_API_URL, KV_REST_API_TOKEN } = getWebsiteServerEnv();
|
||||
return Boolean(KV_REST_API_URL && KV_REST_API_TOKEN);
|
||||
}
|
||||
|
||||
export function assertKvConfiguredInProduction(): void {
|
||||
if (isProductionEnvironment() && !isKvConfigured()) {
|
||||
throw new Error('Missing KV_REST_API_URL/KV_REST_API_TOKEN in production environment');
|
||||
}
|
||||
}
|
||||
@@ -1,8 +1,11 @@
|
||||
import { assertKvConfiguredInProduction, isKvConfigured, isProductionEnvironment } from './config/env';
|
||||
|
||||
const RATE_LIMIT_WINDOW = 60 * 60 * 1000; // 1 hour in milliseconds
|
||||
const MAX_REQUESTS_PER_WINDOW = 5;
|
||||
const RATE_LIMIT_PREFIX = 'ratelimit:signup:';
|
||||
|
||||
const isDev = !process.env.KV_REST_API_URL;
|
||||
// Dev fallback: only allowed outside production.
|
||||
const isDev = !isProductionEnvironment() && !isKvConfigured();
|
||||
|
||||
// In-memory fallback for development
|
||||
const devRateLimits = new Map<string, { count: number; resetAt: number }>();
|
||||
@@ -49,6 +52,8 @@ export async function checkRateLimit(identifier: string): Promise<{
|
||||
}
|
||||
|
||||
// Production: Use Vercel KV
|
||||
assertKvConfiguredInProduction();
|
||||
|
||||
const { kv } = await import('@vercel/kv');
|
||||
const key = `${RATE_LIMIT_PREFIX}${identifier}`;
|
||||
|
||||
|
||||
@@ -1,9 +1,20 @@
|
||||
import { apiClient } from '@/lib/apiClient';
|
||||
import { ApiClient } from '@/lib/api';
|
||||
import type { LeaguesApiClient } from '@/lib/api/leagues/LeaguesApiClient';
|
||||
import { getWebsiteApiBaseUrl } from '@/lib/config/apiBaseUrl';
|
||||
import { LeagueMemberViewModel } from '@/lib/view-models/LeagueMemberViewModel';
|
||||
import type { LeagueMemberDTO } from '@/lib/types/generated/LeagueMemberDTO';
|
||||
import type { LeagueMembership } from '@/lib/types/LeagueMembership';
|
||||
|
||||
let cachedLeaguesApiClient: LeaguesApiClient | undefined;
|
||||
|
||||
function getDefaultLeaguesApiClient(): LeaguesApiClient {
|
||||
if (cachedLeaguesApiClient) return cachedLeaguesApiClient;
|
||||
|
||||
const api = new ApiClient(getWebsiteApiBaseUrl());
|
||||
cachedLeaguesApiClient = (api as any).leagues as LeaguesApiClient;
|
||||
return cachedLeaguesApiClient;
|
||||
}
|
||||
|
||||
export class LeagueMembershipService {
|
||||
// In-memory cache for memberships (populated via API calls)
|
||||
private static leagueMemberships = new Map<string, LeagueMembership[]>();
|
||||
@@ -11,7 +22,7 @@ export class LeagueMembershipService {
|
||||
constructor(private readonly leaguesApiClient?: LeaguesApiClient) {}
|
||||
|
||||
private getClient(): LeaguesApiClient {
|
||||
return (this.leaguesApiClient ?? (apiClient as any).leagues) as LeaguesApiClient;
|
||||
return this.leaguesApiClient ?? getDefaultLeaguesApiClient();
|
||||
}
|
||||
|
||||
async getLeagueMemberships(leagueId: string, currentUserId: string): Promise<LeagueMemberViewModel[]> {
|
||||
@@ -45,7 +56,7 @@ export class LeagueMembershipService {
|
||||
*/
|
||||
static async fetchLeagueMemberships(leagueId: string): Promise<LeagueMembership[]> {
|
||||
try {
|
||||
const result = await apiClient.leagues.getMemberships(leagueId);
|
||||
const result = await getDefaultLeaguesApiClient().getMemberships(leagueId);
|
||||
const memberships: LeagueMembership[] = ((result as any)?.members ?? []).map((member: any) => ({
|
||||
id: `${member.driverId}-${leagueId}`, // Generate ID since API doesn't provide it
|
||||
leagueId,
|
||||
|
||||
@@ -73,6 +73,7 @@
|
||||
"hooks/",
|
||||
"lib/",
|
||||
"next-env.d.ts",
|
||||
"env.d.ts",
|
||||
"types/",
|
||||
"utilities/",
|
||||
".next/types/**/*.ts"
|
||||
|
||||
@@ -17,7 +17,7 @@ services:
|
||||
[
|
||||
"sh",
|
||||
"-lc",
|
||||
"set -e; LOCK_HASH=\"$$(sha1sum package-lock.json | awk '{print $$1}')\"; MARKER=\"node_modules/.gridpilot_lock_hash_dev\"; if [ -f \"$$MARKER\" ] && [ \"$$(cat \"$$MARKER\")\" = \"$$LOCK_HASH\" ]; then echo \"[deps] node_modules up-to-date\"; else echo \"[deps] installing api+website deps (slow first time)\"; npm install --no-package-lock --workspace=./apps/api --workspace=./apps/website --include-workspace-root --no-audit --fund=false --prefer-offline; node -e \"require.resolve('ts-node-dev')\"; node -e \"require.resolve('next')\"; echo \"$$LOCK_HASH\" > \"$$MARKER\"; fi",
|
||||
"set -e; LOCK_HASH=\"$$(sha1sum package-lock.json | awk '{print $$1}')\"; MARKER=\"node_modules/.gridpilot_lock_hash_dev\"; if [ -f \"$$MARKER\" ] && [ \"$$(cat \"$$MARKER\")\" = \"$$LOCK_HASH\" ]; then echo \"[deps] node_modules up-to-date\"; else echo \"[deps] installing workspace deps (slow first time)\"; npm install --no-package-lock --include-workspace-root --no-audit --fund=false --prefer-offline; node -e \"require.resolve('ts-node-dev')\"; node -e \"require.resolve('next')\"; echo \"$$LOCK_HASH\" > \"$$MARKER\"; fi",
|
||||
]
|
||||
networks:
|
||||
- gridpilot-network
|
||||
|
||||
@@ -15,7 +15,7 @@ services:
|
||||
[
|
||||
"sh",
|
||||
"-lc",
|
||||
"set -e; LOCK_HASH=\"$$(sha1sum package-lock.json | awk '{print $$1}')\"; MARKER=\"node_modules/.gridpilot_lock_hash_test\"; if [ -f \"$$MARKER\" ] && [ \"$$(cat \"$$MARKER\")\" = \"$$LOCK_HASH\" ]; then echo \"[deps] node_modules up-to-date\"; else echo \"[deps] installing api+website deps\"; npm install --no-package-lock --workspace=./apps/api --workspace=./apps/website --include-workspace-root --no-audit --fund=false --prefer-offline; echo \"$$LOCK_HASH\" > \"$$MARKER\"; fi",
|
||||
"set -e; LOCK_HASH=\"$$(sha1sum package-lock.json | awk '{print $$1}')\"; MARKER=\"node_modules/.gridpilot_lock_hash_test\"; if [ -f \"$$MARKER\" ] && [ \"$$(cat \"$$MARKER\")\" = \"$$LOCK_HASH\" ]; then echo \"[deps] node_modules up-to-date\"; else echo \"[deps] installing workspace deps\"; rm -rf apps/api/node_modules apps/website/node_modules; npm install --no-package-lock --include-workspace-root --no-audit --fund=false --prefer-offline; echo \"$$LOCK_HASH\" > \"$$MARKER\"; fi",
|
||||
]
|
||||
networks:
|
||||
- gridpilot-test-network
|
||||
@@ -26,12 +26,12 @@ services:
|
||||
environment:
|
||||
- NODE_ENV=test
|
||||
ports:
|
||||
- "3001:3000"
|
||||
- "3101:3000"
|
||||
command:
|
||||
[
|
||||
"sh",
|
||||
"-lc",
|
||||
"node -e \"const http=require('http'); const baseCors={ 'Access-Control-Allow-Credentials':'true','Access-Control-Allow-Headers':'Content-Type','Access-Control-Allow-Methods':'GET,POST,PUT,PATCH,DELETE,OPTIONS' }; const server=http.createServer((req,res)=>{ const origin=req.headers.origin||'http://localhost:3000'; res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary','Origin'); for(const [k,v] of Object.entries(baseCors)){ res.setHeader(k,v); } if(req.method==='OPTIONS'){ res.statusCode=204; return res.end(); } const url=new URL(req.url,'http://localhost'); const send=(code,obj)=>{ res.statusCode=code; res.setHeader('content-type','application/json'); res.end(JSON.stringify(obj)); }; if(url.pathname==='/health'){ return send(200,{status:'ok'});} if(url.pathname==='/auth/session'){ res.statusCode=200; res.setHeader('content-type','application/json'); return res.end('null'); } if(url.pathname==='/races/page-data'){ return send(200,{races:[]}); } if(url.pathname==='/leagues/all-with-capacity'){ return send(200,{leagues:[], totalCount:0}); } if(url.pathname==='/teams/all'){ return send(200,{teams:[], totalCount:0}); } return send(404,{message:'Not Found', path:url.pathname}); }); server.listen(3000,()=>console.log('[api-mock] listening on 3000'));\"",
|
||||
"node -e \"const http=require('http'); const baseCors={ 'Access-Control-Allow-Credentials':'true','Access-Control-Allow-Headers':'Content-Type','Access-Control-Allow-Methods':'GET,POST,PUT,PATCH,DELETE,OPTIONS' }; const server=http.createServer((req,res)=>{ const origin=req.headers.origin||'http://localhost:3100'; res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary','Origin'); for(const [k,v] of Object.entries(baseCors)){ res.setHeader(k,v); } if(req.method==='OPTIONS'){ res.statusCode=204; return res.end(); } const url=new URL(req.url,'http://localhost'); const send=(code,obj)=>{ res.statusCode=code; res.setHeader('content-type','application/json'); res.end(JSON.stringify(obj)); }; if(url.pathname==='/health'){ return send(200,{status:'ok'});} if(url.pathname==='/auth/session'){ res.statusCode=200; res.setHeader('content-type','application/json'); return res.end('null'); } if(url.pathname==='/races/page-data'){ return send(200,{races:[]}); } if(url.pathname==='/leagues/all-with-capacity'){ return send(200,{leagues:[], totalCount:0}); } if(url.pathname==='/teams/all'){ return send(200,{teams:[], totalCount:0}); } return send(404,{message:'Not Found', path:url.pathname}); }); server.listen(3000,()=>console.log('[api-mock] listening on 3000'));\"",
|
||||
]
|
||||
networks:
|
||||
- gridpilot-test-network
|
||||
@@ -59,9 +59,9 @@ services:
|
||||
- DOCKER=true
|
||||
- NEXT_PUBLIC_GRIDPILOT_MODE=alpha
|
||||
- API_BASE_URL=http://api:3000
|
||||
- NEXT_PUBLIC_API_BASE_URL=http://localhost:3001
|
||||
- NEXT_PUBLIC_API_BASE_URL=http://localhost:3101
|
||||
ports:
|
||||
- "3000:3000"
|
||||
- "3100:3000"
|
||||
volumes:
|
||||
- ./:/app
|
||||
- test_node_modules:/app/node_modules
|
||||
|
||||
@@ -273,60 +273,49 @@ This pattern applies equally to integration tests (with real database operations
|
||||
|
||||
---
|
||||
|
||||
## Docker E2E Setup
|
||||
## Docker-Based Tests (Website ↔ API Wiring)
|
||||
|
||||
### Architecture
|
||||
This repo uses Docker in two different ways:
|
||||
|
||||
E2E tests run against a full stack orchestrated by `docker-compose.test.yml`:
|
||||
1) **Website ↔ API smoke (wiring validation)**
|
||||
- Orchestrated by [`docker-compose.test.yml`](docker-compose.test.yml:1) at the repo root.
|
||||
- Runs:
|
||||
- Website in Docker (Next.js dev server)
|
||||
- An API mock server in Docker (Node HTTP server)
|
||||
- Goal: catch misconfigured hostnames/ports/env vars and CORS issues that only show up in Dockerized setups.
|
||||
|
||||
```yaml
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
environment:
|
||||
POSTGRES_DB: gridpilot_test
|
||||
POSTGRES_USER: test
|
||||
POSTGRES_PASSWORD: test
|
||||
2) **Hosted-session automation E2E (fixture-driven automation)**
|
||||
- Orchestrated by `docker/docker-compose.e2e.yml` (separate stack; documented later in this file).
|
||||
- Goal: validate Playwright-driven automation against HTML fixtures.
|
||||
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
### Website ↔ API smoke: how to run
|
||||
|
||||
web-api:
|
||||
build: ./src/apps/web-api
|
||||
depends_on:
|
||||
- postgres
|
||||
- redis
|
||||
environment:
|
||||
DATABASE_URL: postgres://test:test@postgres:5432/gridpilot_test
|
||||
REDIS_URL: redis://redis:6379
|
||||
ports:
|
||||
- "3000:3000"
|
||||
```
|
||||
Run:
|
||||
- `npm run test:docker:website` (see [`package.json`](package.json:92))
|
||||
|
||||
### Execution Flow
|
||||
What it does (in order):
|
||||
- Installs deps in a dedicated Docker volume (`npm run docker:test:deps`)
|
||||
- Starts the test stack (`npm run docker:test:up`)
|
||||
- Waits for readiness (`npm run docker:test:wait`)
|
||||
- Runs Playwright smoke tests (`npm run smoke:website:docker`)
|
||||
|
||||
1. **Start Services:** `docker compose -f docker-compose.test.yml up -d`
|
||||
2. **Run Migrations:** `npm run migrate:test` (seeds database)
|
||||
3. **Execute Tests:** Playwright targets `http://localhost:3000`
|
||||
4. **Teardown:** `docker compose -f docker-compose.test.yml down -v`
|
||||
Ports used:
|
||||
- Website: `http://localhost:3100`
|
||||
- API mock: `http://localhost:3101`
|
||||
|
||||
### Environment Setup
|
||||
Key contract:
|
||||
- Website must resolve the API base URL via [`getWebsiteApiBaseUrl()`](apps/website/lib/config/apiBaseUrl.ts:6).
|
||||
- The website’s HTTP client uses `credentials: 'include'`, so the API must support CORS-with-credentials (implemented for the real API in [`bootstrap()`](apps/api/src/main.ts:14)).
|
||||
|
||||
```bash
|
||||
# tests/e2e/setup.ts
|
||||
export async function globalSetup() {
|
||||
// Wait for web-api to be ready
|
||||
await waitForService('http://localhost:3000/health');
|
||||
|
||||
// Run database migrations
|
||||
await runMigrations();
|
||||
}
|
||||
### “Mock vs Real” (Website & API)
|
||||
|
||||
export async function globalTeardown() {
|
||||
// Stop Docker Compose services
|
||||
await exec('docker compose -f docker-compose.test.yml down -v');
|
||||
}
|
||||
```
|
||||
- The Website does **not** have a runtime flag like `AUTOMATION_MODE`.
|
||||
- “Mock vs real” for the Website is **only** which API base URL it uses:
|
||||
- Browser: `NEXT_PUBLIC_API_BASE_URL`
|
||||
- Server: `API_BASE_URL` (preferred in Docker) or `NEXT_PUBLIC_API_BASE_URL` fallback
|
||||
|
||||
In the Docker smoke stack, “mock API” means the Node HTTP server in [`docker-compose.test.yml`](docker-compose.test.yml:24).
|
||||
In Docker dev/prod, “real API” means the NestJS app started from [`bootstrap()`](apps/api/src/main.ts:14), and “real vs in-memory” persistence is controlled by `GRIDPILOT_API_PERSISTENCE` in [`AppModule`](apps/api/src/app.module.ts:25).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -91,8 +91,8 @@
|
||||
"docker:prod:logs": "docker-compose -p gridpilot-prod -f docker-compose.prod.yml logs -f",
|
||||
"docker:test:deps": "COMPOSE_PARALLEL_LIMIT=1 docker-compose -p gridpilot-test -f docker-compose.test.yml run --rm deps",
|
||||
"docker:test:up": "COMPOSE_PARALLEL_LIMIT=1 docker-compose -p gridpilot-test -f docker-compose.test.yml up -d --build api website",
|
||||
"docker:test:down": "docker-compose -p gridpilot-test -f docker-compose.test.yml down -v",
|
||||
"docker:test:wait": "node -e \"const sleep=(ms)=>new Promise(r=>setTimeout(r,ms)); const wait=async(url,label)=>{for(let i=0;i<90;i++){try{const r=await fetch(url); if(r.ok){console.log('[wait] '+label+' ready'); return;} }catch{} await sleep(1000);} console.error('[wait] '+label+' not ready: '+url); process.exit(1);}; (async()=>{await wait('http://localhost:3001/health','api'); await wait('http://localhost:3000','website');})();\"",
|
||||
"docker:test:down": "sh -lc \"docker-compose -p gridpilot-test -f docker-compose.test.yml down -v --remove-orphans || true; docker-compose -p gridpilot-test -f docker-compose.test.yml rm -fsv || true\"",
|
||||
"docker:test:wait": "node -e \"const sleep=(ms)=>new Promise(r=>setTimeout(r,ms)); const wait=async(url,label)=>{for(let i=0;i<90;i++){try{const r=await fetch(url); if(r.ok){console.log('[wait] '+label+' ready'); return;} }catch{} await sleep(1000);} console.error('[wait] '+label+' not ready: '+url); process.exit(1);}; (async()=>{await wait('http://localhost:3101/health','api'); await wait('http://localhost:3100','website');})();\"",
|
||||
"smoke:website:docker": "DOCKER_SMOKE=true npx playwright test -c playwright.website.config.ts",
|
||||
"test:docker:website": "sh -lc \"set -e; trap 'npm run docker:test:down' EXIT; npm run docker:test:deps; npm run docker:test:up; npm run docker:test:wait; npm run smoke:website:docker\"",
|
||||
"dom:process": "npx tsx scripts/dom-export/processWorkflows.ts",
|
||||
|
||||
@@ -29,7 +29,7 @@ export default defineConfig({
|
||||
|
||||
// Base URL for the website
|
||||
use: {
|
||||
baseURL: 'http://localhost:3000',
|
||||
baseURL: process.env.DOCKER_SMOKE ? 'http://localhost:3100' : 'http://localhost:3000',
|
||||
screenshot: 'only-on-failure',
|
||||
video: 'retain-on-failure',
|
||||
trace: 'retain-on-failure',
|
||||
|
||||
34
tests/env.d.ts
vendored
Normal file
34
tests/env.d.ts
vendored
Normal file
@@ -0,0 +1,34 @@
|
||||
declare global {
|
||||
namespace NodeJS {
|
||||
interface ProcessEnv {
|
||||
NODE_ENV?: 'development' | 'production' | 'test';
|
||||
|
||||
// CI providers / generic CI
|
||||
CI?: string;
|
||||
CONTINUOUS_INTEGRATION?: string;
|
||||
GITHUB_ACTIONS?: string;
|
||||
GITLAB_CI?: string;
|
||||
CIRCLECI?: string;
|
||||
TRAVIS?: string;
|
||||
JENKINS_URL?: string;
|
||||
BUILDKITE?: string;
|
||||
TF_BUILD?: string;
|
||||
|
||||
// Playwright / smoke
|
||||
DOCKER_SMOKE?: string;
|
||||
|
||||
// E2E toggles
|
||||
HOSTED_REAL_E2E?: '0' | '1';
|
||||
COMPANION_FIXTURE_HOSTED?: string;
|
||||
|
||||
// Electron (smoke harness)
|
||||
ELECTRON_EXECUTABLE_PATH?: string;
|
||||
|
||||
// Headless heuristics
|
||||
HEADLESS?: string;
|
||||
DISPLAY?: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {};
|
||||
@@ -1,6 +1,7 @@
|
||||
{
|
||||
"extends": "./tsconfig.base.json",
|
||||
"include": [
|
||||
"tests/env.d.ts",
|
||||
"tests/**/*.ts",
|
||||
"tests/**/*.tsx"
|
||||
],
|
||||
|
||||
Reference in New Issue
Block a user