wip
This commit is contained in:
62
.dockerignore
Normal file
62
.dockerignore
Normal 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
44
.env.development
Normal 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
53
.env.production
Normal 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
|
||||||
@@ -1,116 +1,120 @@
|
|||||||
# 🏗 Architect
|
# 🏗 Architect
|
||||||
|
|
||||||
## Purpose
|
## 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
|
If the Architect has something to say,
|
||||||
- Domain contains business rules only.
|
it MUST be said as **architecture content**, not commentary.
|
||||||
- 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.
|
|
||||||
|
|
||||||
### 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
|
## Core Principles (Non-Negotiable)
|
||||||
- Single responsibility.
|
All architectural decisions MUST follow:
|
||||||
- Explicit dependencies.
|
- Clean Architecture (strict)
|
||||||
- Clear abstractions.
|
- OOP
|
||||||
- No hidden coupling.
|
- SOLID
|
||||||
|
- KISS
|
||||||
|
- YAGNI
|
||||||
|
|
||||||
### KISS
|
No exceptions unless the user explicitly overrides.
|
||||||
- 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.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Scope of Analysis
|
## 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
|
- scan the repo
|
||||||
- discover context
|
- infer missing context
|
||||||
- guess intent
|
|
||||||
- ask the user questions
|
- ask the user questions
|
||||||
- negotiate scope
|
|
||||||
|
|
||||||
If context is insufficient:
|
If context is insufficient:
|
||||||
Return exactly:
|
Return exactly:
|
||||||
**“Missing context.”**
|
**“Missing context.”**
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Output Rules (Strict)
|
## Output Format (MANDATORY AND FINAL)
|
||||||
The Architect output MUST be:
|
|
||||||
|
The Architect output MUST contain **ONLY these three sections**, in this order:
|
||||||
|
|
||||||
### Diagnosis
|
### Diagnosis
|
||||||
- 3–6 bullet points
|
- 3–6 bullet points
|
||||||
- Each bullet = one concrete violation
|
- each bullet = ONE concrete architectural violation or constraint
|
||||||
- No explanation
|
- no explanations
|
||||||
- No examples
|
- no theory
|
||||||
- No theory
|
- no examples
|
||||||
|
|
||||||
### Plan
|
### Plan
|
||||||
- 3–10 numbered steps
|
- 3–12 numbered steps
|
||||||
- Each step = one imperative architectural action
|
- each step = ONE concrete architectural action
|
||||||
- No options
|
- imperative form
|
||||||
- No alternatives
|
- no alternatives
|
||||||
- No “consider”
|
- no “consider”
|
||||||
- No “if”
|
- no “could”
|
||||||
|
- no “should”
|
||||||
|
- no references to other modes
|
||||||
|
|
||||||
|
This IS the plan.
|
||||||
|
Not a description of a plan.
|
||||||
|
|
||||||
### Summary
|
### Summary
|
||||||
- 1–2 short sentences
|
- 1–2 short sentences
|
||||||
- Direction only
|
- state the architectural direction only
|
||||||
|
- no meta commentary
|
||||||
Nothing else is allowed.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 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:
|
The Architect MUST:
|
||||||
- call out violations immediately
|
- state architecture decisions directly
|
||||||
- stay factual
|
- give clear instructions
|
||||||
- stay concise
|
- remain concise
|
||||||
- never block execution
|
- never hedge
|
||||||
- never soften rules
|
- never explain why principles exist
|
||||||
|
- never soften instructions
|
||||||
|
|
||||||
The Architect MUST NOT:
|
The Architect MUST NOT:
|
||||||
- explain *why* rules exist
|
- output meta summaries
|
||||||
- teach principles
|
- explain process
|
||||||
- describe implementation
|
- describe intent
|
||||||
- discuss tests
|
- teach Clean Architecture
|
||||||
- discuss UX or design
|
- discuss tooling unless it is the architectural subject itself
|
||||||
- produce long text
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## Completion
|
## Completion
|
||||||
A response is valid when:
|
The Architect response is valid ONLY if:
|
||||||
- Clean Architecture is enforced
|
- the Diagnosis lists real issues
|
||||||
- OOP boundaries are clear
|
- the Plan contains concrete architectural steps
|
||||||
- SOLID/KISS/YAGNI are respected
|
- the Summary states direction
|
||||||
- Output contains only Diagnosis, Plan, Summary
|
- NO meta text exists
|
||||||
112
.roo/rules-ask/rules.md
Normal file
112
.roo/rules-ask/rules.md
Normal 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 user’s 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 **1–3 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 user’s 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
|
||||||
@@ -13,9 +13,9 @@ Each mode has **one responsibility** and performs **only that responsibility**:
|
|||||||
|
|
||||||
- Orchestrator
|
- Orchestrator
|
||||||
- Architect
|
- Architect
|
||||||
- Clarification
|
- Ask
|
||||||
- Debugger
|
- Debugger
|
||||||
- Backend Coder
|
- Coder
|
||||||
- Frontend Coder
|
- Frontend Coder
|
||||||
- Designer
|
- Designer
|
||||||
- Quality
|
- 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)
|
## Forbidden (Applies to All Modes)
|
||||||
Modes may NOT:
|
Modes may NOT:
|
||||||
- override user intent
|
- override user intent
|
||||||
|
|||||||
184
README.docker.md
Normal file
184
README.docker.md
Normal 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
|
||||||
20
apps/website/.dockerignore
Normal file
20
apps/website/.dockerignore
Normal 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
|
||||||
23
apps/website/Dockerfile.dev
Normal file
23
apps/website/Dockerfile.dev
Normal 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"]
|
||||||
48
apps/website/Dockerfile.prod
Normal file
48
apps/website/Dockerfile.prod
Normal 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"]
|
||||||
@@ -2,6 +2,8 @@
|
|||||||
|
|
||||||
const nextConfig = {
|
const nextConfig = {
|
||||||
reactStrictMode: true,
|
reactStrictMode: true,
|
||||||
|
// Fix for Next.js 13+ Turbopack in monorepos to correctly identify the workspace root
|
||||||
|
outputFileTracingRoot: '../../',
|
||||||
images: {
|
images: {
|
||||||
remotePatterns: [
|
remotePatterns: [
|
||||||
{
|
{
|
||||||
@@ -17,9 +19,6 @@ const nextConfig = {
|
|||||||
typescript: {
|
typescript: {
|
||||||
ignoreBuildErrors: false,
|
ignoreBuildErrors: false,
|
||||||
},
|
},
|
||||||
eslint: {
|
|
||||||
ignoreDuringBuilds: false,
|
|
||||||
},
|
|
||||||
transpilePackages: [
|
transpilePackages: [
|
||||||
'@gridpilot/racing',
|
'@gridpilot/racing',
|
||||||
'@gridpilot/identity',
|
'@gridpilot/identity',
|
||||||
|
|||||||
60
docker-compose.dev.yml
Normal file
60
docker-compose.dev.yml
Normal 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
205
docker-compose.prod.yml
Normal 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
215
nginx/nginx.conf
Normal 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
6598
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@@ -13,7 +13,19 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "echo 'Development server placeholder - to be configured'",
|
"dev": "echo 'Development server placeholder - to be configured'",
|
||||||
"build": "echo 'Build all packages 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": "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:unit": "vitest run tests/unit",
|
||||||
"test:integration": "vitest run tests/integration",
|
"test:integration": "vitest run tests/integration",
|
||||||
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
"test:e2e": "vitest run --config vitest.e2e.config.ts",
|
||||||
@@ -56,6 +68,7 @@
|
|||||||
"@testing-library/react": "^16.3.0",
|
"@testing-library/react": "^16.3.0",
|
||||||
"@types/jsdom": "^27.0.0",
|
"@types/jsdom": "^27.0.0",
|
||||||
"@types/node": "^24.10.1",
|
"@types/node": "^24.10.1",
|
||||||
|
"@types/express": "^4.17.21",
|
||||||
"@vitest/ui": "^2.1.8",
|
"@vitest/ui": "^2.1.8",
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"commander": "^11.0.0",
|
"commander": "^11.0.0",
|
||||||
|
|||||||
@@ -5,9 +5,19 @@
|
|||||||
* Kept in domain/types so domain/entities contains only entity classes.
|
* 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 {
|
export interface PageViewProps {
|
||||||
id: string;
|
id: string;
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 67 KiB |
@@ -19,6 +19,8 @@
|
|||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
|
"emitDecoratorMetadata": true,
|
||||||
|
"experimentalDecorators": true,
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"],
|
"@/*": ["./*"],
|
||||||
"@/lib/*": ["apps/website/lib/*"],
|
"@/lib/*": ["apps/website/lib/*"],
|
||||||
|
|||||||
Reference in New Issue
Block a user