This commit is contained in:
2025-12-15 13:34:27 +01:00
parent 129c63c362
commit 021feaf51e
19 changed files with 6548 additions and 1318 deletions

62
.dockerignore Normal file
View File

@@ -0,0 +1,62 @@
# Node modules
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
# Build outputs
dist
.next
build
out
.turbo
# Env files (will be added separately)
.env
.env.*
!.env.development
!.env.production
# Git
.git
.gitignore
# Docker
Dockerfile*
docker-compose*
.dockerignore
# IDE
.vscode
.idea
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Documentation
*.md
!README.md
# Tests
coverage
.nyc_output
test-results
# Logs
*.log
logs
# Large directories to exclude
html-dumps
html-dumps-optimized
backups
.husky
# Development files
.prettierrc
.eslintrc*
tsconfig.tsbuildinfo

44
.env.development Normal file
View File

@@ -0,0 +1,44 @@
# ==========================================
# GridPilot Development Environment
# ==========================================
# Node Environment
NODE_ENV=development
# ==========================================
# Database (PostgreSQL)
# ==========================================
DATABASE_URL=postgres://gridpilot_user:gridpilot_dev_pass@db:5432/gridpilot_dev
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
# ==========================================
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
NEXT_PUBLIC_SITE_URL=http://localhost:3001
NEXT_PUBLIC_API_URL=http://localhost:3000
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
NEXT_TELEMETRY_DISABLED=1
# ==========================================
# Vercel KV (Optional in Development)
# ==========================================
# KV_REST_API_URL=
# KV_REST_API_TOKEN=
# ==========================================
# Automation Mode
# ==========================================
AUTOMATION_MODE=dev
CHROME_DEBUG_PORT=9222
AUTOMATION_TIMEOUT=30000
RETRY_ATTEMPTS=3
SCREENSHOT_ON_ERROR=true

53
.env.production Normal file
View File

@@ -0,0 +1,53 @@
# ==========================================
# GridPilot Production Environment
# ==========================================
# Node Environment
NODE_ENV=production
# ==========================================
# Database (PostgreSQL)
# ==========================================
# IMPORTANT: Change these credentials in production!
DATABASE_URL=postgres://gridpilot_user:CHANGE_ME_IN_PRODUCTION@db:5432/gridpilot_prod
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_PASSWORD=CHANGE_ME_IN_PRODUCTION
REDIS_HOST=redis
REDIS_PORT=6379
# ==========================================
# API Configuration
# ==========================================
API_PORT=3000
API_HOST=0.0.0.0
# ==========================================
# Website Configuration
# ==========================================
NEXT_PUBLIC_GRIDPILOT_MODE=alpha
NEXT_PUBLIC_SITE_URL=https://gridpilot.com
NEXT_PUBLIC_API_URL=https://api.gridpilot.com
NEXT_PUBLIC_DISCORD_URL=https://discord.gg/your-invite-code
NEXT_TELEMETRY_DISABLED=1
# ==========================================
# Vercel KV (REQUIRED in Production)
# ==========================================
KV_REST_API_URL=your_kv_rest_api_url_here
KV_REST_API_TOKEN=your_kv_rest_api_token_here
# ==========================================
# Automation Mode
# ==========================================
AUTOMATION_MODE=production
AUTOMATION_TIMEOUT=30000
RETRY_ATTEMPTS=3
SCREENSHOT_ON_ERROR=false

View File

@@ -1,62 +1,50 @@
# 🏗 Architect
## Purpose
Enforce **strict Clean Architecture, strict OOP, SOLID, KISS, and YAGNI**.
Provide **direct architectural diagnosis and a concrete architectural plan**.
Nothing more.
The Architect must ALWAYS output the **actual plan itself**,
never a description of having created a plan.
---
## Core Rules (Non-Negotiable)
## Absolute Rule: NO META OUTPUT
The Architect MUST NOT:
- describe work done
- summarize that a plan exists
- state readiness for implementation
- talk about what Code mode should do
- mention other modes
- write “I have provided…”
- write “This plan is ready…”
- write “The following plan covers…”
### Clean Architecture
- Domain contains business rules only.
- Domain depends on nothing.
- Application orchestrates, does not contain business rules.
- Infrastructure contains details only.
- UI contains no business rules.
- Dependency direction is always inward.
- No cross-layer imports.
- Ports define boundaries, adapters implement them.
- Presenters map data only, no logic.
If the Architect has something to say,
it MUST be said as **architecture content**, not commentary.
### OOP
- One responsibility per class.
- Behavior lives with data.
- No anemic models.
- No procedural code disguised as objects.
- No god objects.
- Prefer composition over inheritance.
- Value Objects for invariants and meaning.
---
### SOLID
- Single responsibility.
- Explicit dependencies.
- Clear abstractions.
- No hidden coupling.
## Core Principles (Non-Negotiable)
All architectural decisions MUST follow:
- Clean Architecture (strict)
- OOP
- SOLID
- KISS
- YAGNI
### KISS
- Simplest structure that works.
- No cleverness.
- No unnecessary abstraction.
- No indirection without need.
### YAGNI
- No features “for later”.
- No abstractions “just in case”.
- No structure without current demand.
No exceptions unless the user explicitly overrides.
---
## Scope of Analysis
The Architect analyzes **only the context provided by the Orchestrator**.
The Architect analyzes ONLY:
- context explicitly provided by the Orchestrator
- files, modules, and goals explicitly named
The Architect does NOT:
The Architect MUST NOT:
- scan the repo
- discover context
- guess intent
- infer missing context
- ask the user questions
- negotiate scope
If context is insufficient:
Return exactly:
@@ -64,53 +52,69 @@ Return exactly:
---
## Output Rules (Strict)
The Architect output MUST be:
## Output Format (MANDATORY AND FINAL)
The Architect output MUST contain **ONLY these three sections**, in this order:
### Diagnosis
- 36 bullet points
- Each bullet = one concrete violation
- No explanation
- No examples
- No theory
- each bullet = ONE concrete architectural violation or constraint
- no explanations
- no theory
- no examples
### Plan
- 310 numbered steps
- Each step = one imperative architectural action
- No options
- No alternatives
- No “consider”
- No “if
- 312 numbered steps
- each step = ONE concrete architectural action
- imperative form
- no alternatives
- no “consider”
- no “could
- no “should”
- no references to other modes
This IS the plan.
Not a description of a plan.
### Summary
- 12 short sentences
- Direction only
Nothing else is allowed.
- state the architectural direction only
- no meta commentary
---
## Behavior
## Examples of FORBIDDEN Output
❌ “I have provided a detailed plan…”
❌ “This plan is ready for implementation…”
❌ “The following plan outlines…”
❌ “Code mode can now implement…”
❌ “Next steps would be…”
These are **never allowed**.
---
## Behavior Rules
The Architect MUST:
- call out violations immediately
- stay factual
- stay concise
- never block execution
- never soften rules
- state architecture decisions directly
- give clear instructions
- remain concise
- never hedge
- never explain why principles exist
- never soften instructions
The Architect MUST NOT:
- explain *why* rules exist
- teach principles
- describe implementation
- discuss tests
- discuss UX or design
- produce long text
- output meta summaries
- explain process
- describe intent
- teach Clean Architecture
- discuss tooling unless it is the architectural subject itself
---
## Completion
A response is valid when:
- Clean Architecture is enforced
- OOP boundaries are clear
- SOLID/KISS/YAGNI are respected
- Output contains only Diagnosis, Plan, Summary
The Architect response is valid ONLY if:
- the Diagnosis lists real issues
- the Plan contains concrete architectural steps
- the Summary states direction
- NO meta text exists

112
.roo/rules-ask/rules.md Normal file
View File

@@ -0,0 +1,112 @@
# ❓ Ask
## Purpose
Resolve **meaning, intent, and decision clarity** by consulting existing knowledge,
especially **product decisions stored in Memory (MCP)**.
Ask mode exists to prevent re-deciding things that are already decided.
---
## Core Responsibility
Ask mode MUST:
- interpret the users intent at a semantic level
- check whether relevant decisions or constraints already exist in Memory
- surface those decisions clearly
- collapse ambiguity into a single, stable interpretation
Ask mode does NOT create new decisions.
---
## Memory Usage (Primary Function)
Ask mode MUST:
- consult Memory for existing **product decisions, invariants, or constraints**
- identify whether the current question is already answered by Memory
- report the relevant decision verbatim (or summarized factually)
Ask mode MUST NOT:
- write to Memory
- reinterpret Memory entries
- extend Memory
- challenge stored decisions
Memory is treated as **truth**.
---
## What Ask Produces
Ask mode produces:
- clarification of intent
- identification of applicable existing decisions
- resolution of ambiguity using known constraints
Ask mode does NOT:
- plan work
- assign tasks
- suggest changes
- evaluate architecture
- propose solutions
- gather technical context
- scan the repository
---
## Output Rules (STRICT)
Ask mode output MUST:
- be **13 short lines**
- contain **statements only**
- contain **no questions**
- contain **no options or alternatives**
- contain **no explanation or justification**
- contain **no technical detail**
Typical output forms:
- “This is already decided: X.”
- “Memory defines Y as invariant.”
- “Intent resolves to Z under existing constraints.”
- “No prior decision exists for this.”
---
## If Memory Has No Relevant Entry
If Memory contains no relevant decision:
- Ask mode states this explicitly
- Ask mode does NOT invent a resolution
Exact output:
“No existing decision found.”
---
## Context Handling
Ask mode operates ONLY on:
- the users stated intent
- context provided by the Orchestrator
- Memory contents
Ask mode MUST NOT:
- ask the user questions
- infer missing information
- guess intent
- expand scope
---
## Forbidden
Ask mode MUST NOT:
- act as Clarification-by-interrogation
- perform analysis beyond semantics
- explain concepts
- teach principles
- propose actions
- contradict Memory
- block execution
---
## Completion
Ask mode is complete when:
- applicable Memory decisions are identified (or absence is confirmed)
- intent is unambiguous
- no further semantic clarification is needed

View File

@@ -13,9 +13,9 @@ Each mode has **one responsibility** and performs **only that responsibility**:
- Orchestrator
- Architect
- Clarification
- Ask
- Debugger
- Backend Coder
- Coder
- Frontend Coder
- Designer
- Quality
@@ -115,6 +115,58 @@ execution must proceed without further comment.
---
## Memory (MCP) — Product Brain Rules
Memory is **not** a place for instructions, prompts, or process rules.
Memory represents **product knowledge and decision constraints** that are:
- not encoded directly in code
- not part of working instructions
- but must influence future decisions
### What MAY be stored in Memory
Only **truths about the product**, such as:
- domain rules that are not obvious from code alone
- irreversible product decisions
- historical or business constraints
- intentional trade-offs
- invariants that must hold across all future work
Examples (illustrative only):
- “The website is marketing-only and contains no business rules.”
- “Public DTOs are external contracts and must not be renamed.”
- “Some automation flows intentionally allow partial failure.”
Memory entries must be:
- declarative
- short
- atomic
- free of explanation
- free of history
- free of instruction language
### What MUST NEVER be stored in Memory
- instructions or rules of how to work
- role definitions or mode behavior
- prompts or prompt fragments
- TODOs or task lists
- explanations or examples
- chat summaries
- code
- logs
- decisions about process
If something belongs in a prompt, README, or code comment → it does NOT belong in memory.
### Memory Access Rules
- Only the **Orchestrator** may read from or write to memory.
- Experts must NEVER read or write memory directly.
- Memory is consulted only when making decisions, never during execution.
Memory exists to **prevent re-deciding facts**, not to guide implementation.
---
## Forbidden (Applies to All Modes)
Modes may NOT:
- override user intent

184
README.docker.md Normal file
View File

@@ -0,0 +1,184 @@
# Docker Setup for GridPilot
This document describes the Docker setup for local development and production deployment of GridPilot.
## Quick Start
### Development
Start all services with hot-reloading:
```bash
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
- Enable hot-reloading for both apps
Access:
- Website: http://localhost:3001
- API: http://localhost:3000
- Database: localhost:5432
### Production
Start all services in production mode:
```bash
npm run docker:prod:build
```
This will:
- Build optimized Docker images
- Start PostgreSQL, Redis, API, Website, and Nginx
- Enable health checks, auto-restart, and resource limits
- Configure caching and performance optimizations
Access:
- Nginx (Website + API): http://localhost:80
## Available Commands
### Development
- `npm run docker:dev` - Start dev environment
- `npm run docker:dev:build` - Rebuild and start
- `npm run docker:dev:down` - Stop services
- `npm run docker:dev:logs` - View logs
- `npm run docker:dev:clean` - Stop and remove volumes
### Production
- `npm run docker:prod` - Start prod environment
- `npm run docker:prod:build` - Rebuild and start
- `npm run docker:prod:down` - Stop services
- `npm run docker:prod:logs` - View logs
- `npm run docker:prod:clean` - Stop and remove volumes
## Environment Variables
### 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)
## Architecture
### Development Setup
- Hot-reloading enabled via volume mounts
- Source code changes reflect immediately
- Database persisted in named volume
- Debug port exposed for API (9229)
### Production Setup
- Multi-stage builds for optimized images
- Only production dependencies included
- Nginx reverse proxy for both services
- Health checks for all services
- Auto-restart on failure
## Docker Services
### API (NestJS)
- Dev: `apps/api/Dockerfile.dev`
- Prod: `apps/api/Dockerfile.prod`
- Port: 3000
- Debug: 9229 (dev only)
### Website (Next.js)
- Dev: `apps/website/Dockerfile.dev`
- Prod: `apps/website/Dockerfile.prod`
- Port: 3001 (dev), 3000 (prod)
### Database (PostgreSQL)
- Image: postgres:15-alpine
- Port: 5432 (internal)
- Data: Persisted in Docker volume
- Optimized with performance tuning parameters
### Redis (Production only)
- Image: redis:7-alpine
- Port: 6379 (internal)
- Configured with:
- LRU eviction policy
- 512MB max memory
- AOF persistence
- Password protection
### Nginx (Production only)
- Reverse proxy for website + API
- Features:
- Rate limiting (API: 10r/s, General: 30r/s)
- Security headers (XSS, CSP, Frame-Options)
- Gzip compression
- Static asset caching
- Connection pooling
- Request buffering
- Port: 80, 443
## Troubleshooting
### Services won't start
```bash
# Clean everything and rebuild
npm run docker:dev:clean
npm run docker:dev:build
```
### Hot-reloading not working
Check that volume mounts are correct in docker-compose.dev.yml
### Database connection issues
Ensure DATABASE_URL in .env matches the database service configuration
### Check logs
```bash
# All services
npm run docker:dev:logs
# Specific service
docker-compose -f docker-compose.dev.yml logs -f api
docker-compose -f docker-compose.dev.yml logs -f website
docker-compose -f docker-compose.dev.yml logs -f db
```
## Tips
1. **First time setup**: Use `docker:dev:build` to ensure images are built
2. **Clean slate**: Use `docker:dev:clean` to remove all data and start fresh
3. **Production testing**: Test prod setup locally before deploying
4. **Database access**: Use any PostgreSQL client with credentials from .env file
5. **Debugging**: Attach debugger to port 9229 for API debugging
## Production Deployment
Before deploying to production:
1. Update `.env.production` with real credentials
2. Configure SSL certificates in `nginx/ssl/`
3. Update Nginx configuration for HTTPS
4. Set proper domain names in environment variables
5. Consider using Docker secrets for sensitive data
## File Structure
```
.
├── docker-compose.dev.yml # Development orchestration
├── docker-compose.prod.yml # Production orchestration
├── .env.development # Dev environment variables
├── .env.production # Prod environment variables
├── apps/
│ ├── api/
│ │ ├── Dockerfile.dev # API dev image
│ │ ├── Dockerfile.prod # API prod image
│ │ └── .dockerignore
│ └── website/
│ ├── Dockerfile.dev # Website dev image
│ ├── Dockerfile.prod # Website prod image
│ └── .dockerignore
└── nginx/
└── nginx.conf # Nginx configuration

View File

@@ -0,0 +1,20 @@
node_modules
.next
dist
.env
.env.local
.env.*.local
Dockerfile*
docker-compose.*
.git
.gitignore
README.md
npm-debug.log
yarn-debug.log
yarn-error.log
.DS_Store
*.md
.vscode
.idea
coverage
.turbo

View File

@@ -0,0 +1,23 @@
FROM node:20-alpine
WORKDIR /app
# Install bash for better shell capabilities
RUN apk add --no-cache bash
# Copy root package.json and install dependencies
COPY package.json package-lock.json ./
RUN npm ci
RUN find ./node_modules -name "next" -print || true # Debugging line
# Copy apps/website and packages for development
COPY apps/website apps/website/
COPY packages packages/
COPY apps/website/tsconfig.json apps/website/
COPY scripts scripts/
COPY tsconfig.base.json ./
EXPOSE 3000
# Run from the correct workspace context
CMD ["npm", "run", "dev", "--workspace=@gridpilot/website"]

View File

@@ -0,0 +1,48 @@
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files and install dependencies
COPY package.json package-lock.json ./
RUN npm ci
# Copy apps/website, packages, and config for building
COPY apps/website apps/website/
COPY packages packages/
COPY apps/website/tsconfig.json apps/website/
COPY scripts scripts/
COPY tsconfig.base.json ./
# Build the Next.js application
# Run from the root workspace context
RUN node ./node_modules/next/dist/bin/next build
# Production stage: slim image with only production dependencies and built files
FROM node:20-alpine AS production_final
WORKDIR /app
# Install wget for healthchecks
RUN apk add --no-cache wget
# Copy package files and install production dependencies only
COPY --from=builder /app/package.json ./
COPY --from=builder /app/package-lock.json ./
RUN npm ci --omit=dev
# Copy built Next.js application
COPY --from=builder /app/apps/website/.next ./apps/website/.next
COPY --from=builder /app/apps/website/public ./apps/website/public
COPY --from=builder /app/apps/website/package.json ./apps/website/package.json
COPY --from=builder /app/apps/website/next.config.mjs ./apps/website/next.config.mjs
# Copy packages (needed for runtime dependencies)
COPY --from=builder /app/packages ./packages
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
# Run Next.js in production mode from the root workspace context
CMD ["node", "./node_modules/next/dist/bin/next", "start"]

View File

@@ -2,6 +2,8 @@
const nextConfig = {
reactStrictMode: true,
// Fix for Next.js 13+ Turbopack in monorepos to correctly identify the workspace root
outputFileTracingRoot: '../../',
images: {
remotePatterns: [
{
@@ -17,9 +19,6 @@ const nextConfig = {
typescript: {
ignoreBuildErrors: false,
},
eslint: {
ignoreDuringBuilds: false,
},
transpilePackages: [
'@gridpilot/racing',
'@gridpilot/identity',

60
docker-compose.dev.yml Normal file
View File

@@ -0,0 +1,60 @@
version: '3.8'
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile.dev
env_file:
- .env.development
environment:
- NODE_ENV=development
ports:
- "3000:3000"
- "9229:9229"
depends_on:
db:
condition: service_healthy
networks:
- gridpilot-network
restart: unless-stopped
website:
build:
context: .
dockerfile: apps/website/Dockerfile.dev
env_file:
- .env.development
environment:
- NEXT_TELEMETRY_DISABLED=1
ports:
- "3001:3000"
networks:
- gridpilot-network
restart: unless-stopped
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=${POSTGRES_DB:-gridpilot_db}
- POSTGRES_USER=${POSTGRES_USER:-user}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
ports:
- "5432:5432"
volumes:
- dev_db_data:/var/lib/postgresql/data
networks:
- gridpilot-network
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-gridpilot_db}"]
interval: 5s
timeout: 5s
retries: 5
networks:
gridpilot-network:
driver: bridge
volumes:
dev_db_data:

205
docker-compose.prod.yml Normal file
View File

@@ -0,0 +1,205 @@
version: '3.8'
services:
api:
build:
context: .
dockerfile: apps/api/Dockerfile.prod
env_file:
- .env.production
environment:
- NODE_ENV=production
expose:
- "3000"
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- gridpilot-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000/health"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
website:
build:
context: .
dockerfile: apps/website/Dockerfile.prod
env_file:
- .env.production
environment:
- NODE_ENV=production
- NEXT_TELEMETRY_DISABLED=1
expose:
- "3000"
depends_on:
redis:
condition: service_healthy
networks:
- gridpilot-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: '1'
memory: 1G
reservations:
cpus: '0.5'
memory: 512M
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost:3000"]
interval: 30s
timeout: 10s
retries: 3
start_period: 40s
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
db:
image: postgres:15-alpine
restart: unless-stopped
environment:
- POSTGRES_DB=${POSTGRES_DB:-gridpilot_db}
- POSTGRES_USER=${POSTGRES_USER:-user}
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD:-password}
- POSTGRES_INITDB_ARGS=--encoding=UTF-8 --lc-collate=C --lc-ctype=C
command: >
postgres
-c shared_buffers=256MB
-c max_connections=200
-c effective_cache_size=1GB
-c maintenance_work_mem=64MB
-c checkpoint_completion_target=0.9
-c wal_buffers=16MB
-c default_statistics_target=100
-c random_page_cost=1.1
-c effective_io_concurrency=200
-c work_mem=2621kB
-c min_wal_size=1GB
-c max_wal_size=4GB
expose:
- "5432"
volumes:
- prod_db_data:/var/lib/postgresql/data
- ./backups:/backups
networks:
- gridpilot-network
deploy:
resources:
limits:
cpus: '1'
memory: 2G
reservations:
cpus: '0.5'
memory: 512M
healthcheck:
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER:-user} -d ${POSTGRES_DB:-gridpilot_db}"]
interval: 10s
timeout: 5s
retries: 5
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
redis:
image: redis:7-alpine
restart: unless-stopped
command: >
redis-server
--maxmemory 512mb
--maxmemory-policy allkeys-lru
--save 60 1000
--appendonly yes
--appendfsync everysec
--requirepass ${REDIS_PASSWORD:-CHANGE_ME_IN_PRODUCTION}
expose:
- "6379"
volumes:
- prod_redis_data:/data
networks:
- gridpilot-network
deploy:
resources:
limits:
cpus: '0.5'
memory: 768M
reservations:
cpus: '0.25'
memory: 256M
healthcheck:
test: ["CMD", "redis-cli", "--raw", "incr", "ping"]
interval: 10s
timeout: 3s
retries: 5
logging:
driver: "json-file"
options:
max-size: "5m"
max-file: "2"
nginx:
image: nginx:stable-alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx/nginx.conf:/etc/nginx/nginx.conf:ro
- ./nginx/ssl:/etc/nginx/ssl:ro
- nginx_cache:/var/cache/nginx
depends_on:
- api
- website
networks:
- gridpilot-network
restart: unless-stopped
deploy:
resources:
limits:
cpus: '0.5'
memory: 256M
reservations:
cpus: '0.25'
memory: 128M
healthcheck:
test: ["CMD", "wget", "--spider", "-q", "http://localhost/health"]
interval: 30s
timeout: 5s
retries: 3
logging:
driver: "json-file"
options:
max-size: "10m"
max-file: "3"
networks:
gridpilot-network:
driver: bridge
volumes:
prod_db_data:
prod_redis_data:
nginx_cache:

215
nginx/nginx.conf Normal file
View File

@@ -0,0 +1,215 @@
user nginx;
worker_processes auto;
error_log /var/log/nginx/error.log warn;
pid /var/run/nginx.pid;
events {
worker_connections 2048;
use epoll;
multi_accept on;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
# Logging
log_format main '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" '
'"$http_user_agent" "$http_x_forwarded_for" '
'rt=$request_time uct="$upstream_connect_time" '
'uht="$upstream_header_time" urt="$upstream_response_time"';
access_log /var/log/nginx/access.log main;
# Performance optimizations
sendfile on;
tcp_nopush on;
tcp_nodelay on;
keepalive_timeout 65;
keepalive_requests 100;
types_hash_max_size 2048;
server_tokens off;
# Buffer optimizations
client_body_buffer_size 128k;
client_max_body_size 20M;
client_header_buffer_size 1k;
large_client_header_buffers 4 8k;
output_buffers 1 32k;
postpone_output 1460;
# Gzip compression
gzip on;
gzip_vary on;
gzip_proxied any;
gzip_comp_level 6;
gzip_types text/plain text/css text/xml text/javascript
application/json application/javascript application/xml+rss
application/rss+xml font/truetype font/opentype
application/vnd.ms-fontobject image/svg+xml;
gzip_disable "msie6";
# Brotli compression (if available)
# brotli on;
# brotli_comp_level 6;
# brotli_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;
# Cache settings
proxy_cache_path /var/cache/nginx levels=1:2 keys_zone=cache:10m max_size=1g inactive=60m use_temp_path=off;
proxy_cache_key "$scheme$request_method$host$request_uri";
proxy_cache_valid 200 60m;
proxy_cache_valid 404 1m;
proxy_cache_use_stale error timeout updating http_500 http_502 http_503 http_504;
proxy_cache_background_update on;
proxy_cache_lock on;
# Upstreams
upstream api {
server api:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
upstream website {
server website:3000 max_fails=3 fail_timeout=30s;
keepalive 32;
}
# Rate limiting zones
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
limit_req_zone $binary_remote_addr zone=general_limit:10m rate=30r/s;
limit_req_zone $binary_remote_addr zone=login_limit:10m rate=5r/m;
limit_conn_zone $binary_remote_addr zone=conn_limit:10m;
# HTTP server - redirect to HTTPS in production
server {
listen 80;
server_name _;
# Health check endpoint (no redirect)
location /health {
access_log off;
return 200 "healthy\n";
add_header Content-Type text/plain;
}
# For development/testing, serve directly
# In production, uncomment the redirect below
# return 301 https://$host$request_uri;
# Security headers
add_header X-Frame-Options "SAMEORIGIN" always;
add_header X-Content-Type-Options "nosniff" always;
add_header X-XSS-Protection "1; mode=block" always;
add_header Referrer-Policy "no-referrer-when-downgrade" always;
add_header Content-Security-Policy "default-src 'self' http: https: data: blob: 'unsafe-inline'" always;
# Connection limits
limit_conn conn_limit 10;
# API routes
location /api/ {
limit_req zone=api_limit burst=20 nodelay;
limit_req_status 429;
proxy_pass http://api/;
proxy_http_version 1.1;
# Headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
proxy_cache_bypass $http_upgrade;
}
# API health check (no rate limit)
location /api/health {
access_log off;
proxy_pass http://api/health;
proxy_http_version 1.1;
proxy_set_header Connection "";
}
# Static assets with caching
location ~* \.(jpg|jpeg|png|gif|ico|css|js|svg|woff|woff2|ttf|eot)$ {
proxy_pass http://website;
proxy_cache cache;
proxy_cache_valid 200 7d;
proxy_cache_valid 404 1m;
add_header Cache-Control "public, max-age=604800, immutable";
add_header X-Cache-Status $upstream_cache_status;
}
# Website routes
location / {
limit_req zone=general_limit burst=50 nodelay;
limit_req_status 429;
proxy_pass http://website/;
proxy_http_version 1.1;
# Headers
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Port $server_port;
# Timeouts
proxy_connect_timeout 60s;
proxy_send_timeout 60s;
proxy_read_timeout 60s;
# Buffering
proxy_buffering on;
proxy_buffer_size 4k;
proxy_buffers 8 4k;
proxy_busy_buffers_size 8k;
proxy_cache_bypass $http_upgrade;
}
}
# HTTPS server (configure when SSL is ready)
# server {
# listen 443 ssl http2;
# server_name your-domain.com;
#
# # SSL configuration
# ssl_certificate /etc/nginx/ssl/cert.pem;
# ssl_certificate_key /etc/nginx/ssl/key.pem;
# ssl_protocols TLSv1.2 TLSv1.3;
# ssl_ciphers ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-RSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:ECDHE-RSA-AES256-GCM-SHA384:ECDHE-ECDSA-CHACHA20-POLY1305:ECDHE-RSA-CHACHA20-POLY1305:DHE-RSA-AES128-GCM-SHA256:DHE-RSA-AES256-GCM-SHA384;
# ssl_prefer_server_ciphers off;
# ssl_session_cache shared:SSL:10m;
# ssl_session_timeout 10m;
# ssl_session_tickets off;
# ssl_stapling on;
# ssl_stapling_verify on;
#
# # HSTS (uncomment after testing)
# # add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload" always;
#
# # ... rest of location blocks from HTTP server
# }
}

6598
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -13,7 +13,19 @@
"scripts": {
"dev": "echo 'Development server placeholder - to be configured'",
"build": "echo 'Build all packages placeholder - to be configured'",
"docker:dev": "docker-compose -f docker-compose.dev.yml up",
"docker:dev:build": "docker-compose -f docker-compose.dev.yml up --build",
"docker:dev:down": "docker-compose -f docker-compose.dev.yml down",
"docker:dev:logs": "docker-compose -f docker-compose.dev.yml logs -f",
"docker:dev:clean": "docker-compose -f docker-compose.dev.yml down -v",
"docker:prod": "docker-compose -f docker-compose.prod.yml up -d",
"docker:prod:build": "docker-compose -f docker-compose.prod.yml up -d --build",
"docker:prod:down": "docker-compose -f docker-compose.prod.yml down",
"docker:prod:logs": "docker-compose -f docker-compose.prod.yml logs -f",
"docker:prod:clean": "docker-compose -f docker-compose.prod.yml down -v",
"api:build": "npm run build --workspace=@gridpilot/api",
"test": "vitest run && vitest run --config vitest.e2e.config.ts && npm run smoke:website",
"test:api": "npx jest --config=apps/api/jest.config.js",
"test:unit": "vitest run tests/unit",
"test:integration": "vitest run tests/integration",
"test:e2e": "vitest run --config vitest.e2e.config.ts",
@@ -56,6 +68,7 @@
"@testing-library/react": "^16.3.0",
"@types/jsdom": "^27.0.0",
"@types/node": "^24.10.1",
"@types/express": "^4.17.21",
"@vitest/ui": "^2.1.8",
"cheerio": "^1.0.0",
"commander": "^11.0.0",

View File

@@ -5,9 +5,19 @@
* Kept in domain/types so domain/entities contains only entity classes.
*/
export type EntityType = 'league' | 'driver' | 'team' | 'race' | 'sponsor';
export enum EntityType {
LEAGUE = 'league',
DRIVER = 'driver',
TEAM = 'team',
RACE = 'race',
SPONSOR = 'sponsor',
}
export type VisitorType = 'anonymous' | 'driver' | 'sponsor';
export enum VisitorType {
ANONYMOUS = 'anonymous',
DRIVER = 'driver',
SPONSOR = 'sponsor',
}
export interface PageViewProps {
id: string;

Binary file not shown.

Before

Width:  |  Height:  |  Size: 67 KiB

View File

@@ -19,6 +19,8 @@
"isolatedModules": true,
"noEmit": true,
"baseUrl": ".",
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
"paths": {
"@/*": ["./*"],
"@/lib/*": ["apps/website/lib/*"],