diff --git a/.a0/runtime_report.md b/.a0/runtime_report.md new file mode 100644 index 0000000..d289dee --- /dev/null +++ b/.a0/runtime_report.md @@ -0,0 +1,305 @@ +# Runtime Report — ERP Nutzfahrzeuge + +**Date:** 2026-07-17 +**Phase:** 5 — Runtime Verification +**Runtime:** `/opt/venv/bin/python` (execution environment) + +--- + +## Summary + +| Check | Status | +|-------|--------| +| Backend startup | ✅ PASS | +| Health endpoint | ✅ PASS (200, `{"status":"ok"}`) | +| Root endpoint | ✅ PASS (200, app metadata) | +| Auth login (GET) | ✅ PASS (405 Method Not Allowed — POST-only) | +| OpenAPI/Swagger | ✅ PASS (200, full schema) | +| Frontend build artifacts | ✅ PASS (`.next/` with BUILD_ID, server/, static/) | +| Runtime errors | ✅ NONE (clean logs, no tracebacks) | +| PostgreSQL | ✅ Running (localhost:5432, accepting connections) | +| Redis | ⚠️ NOT installed (JWT-based refresh tokens, no Redis storage needed) | + +**Overall verdict:** Backend starts and runs cleanly. All public endpoints respond as expected. No runtime errors detected. + +--- + +## 1. Startup Verification + +**Start command:** +```bash +cd backend && /opt/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +**Result:** Server started successfully (PID 3383733). + +**Startup log:** +``` +INFO: Started server process [3383733] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +``` + +**Import check:** `from app.main import app` — OK (no ImportError) + +**Core dependencies verified:** +- `uvicorn` ✅ +- `fastapi` ✅ +- `sqlalchemy` ✅ +- `asyncpg` ✅ + +--- + +## 2. Health Check + +**Endpoint:** `GET http://localhost:8000/api/v1/health` + +**Response:** +- HTTP Status: `200 OK` +- Body: `{"status":"ok"}` + +**Verdict:** ✅ Health endpoint reachable and returning expected payload. + +--- + +## 3. Endpoint Verification + +### 3.1 Root Endpoint +- **URL:** `GET http://localhost:8000/` +- **HTTP Status:** `200 OK` +- **Body:** `{"name":"ERP Nutzfahrzeuge","version":"1.0.0","docs":"/docs"}` + +### 3.2 Auth Login (GET — should fail) +- **URL:** `GET http://localhost:8000/api/v1/auth/login` +- **HTTP Status:** `405 Method Not Allowed` +- **Body:** `{"detail":"Method Not Allowed"}` +- **Expected behavior:** Login is POST-only; GET correctly rejected. + +### 3.3 OpenAPI Schema +- **URL:** `GET http://localhost:8000/openapi.json` +- **HTTP Status:** `200 OK` +- **Schema version:** OpenAPI 3.1.0 +- **Title:** ERP Nutzfahrzeuge Backend API +- **Version:** 1.0.0 + +--- + +## 4. Available API Endpoints (from OpenAPI) + +### Auth (`/api/v1/auth`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| POST | `/api/v1/auth/login` | Login with email+password → JWT tokens | No | +| POST | `/api/v1/auth/refresh` | Exchange refresh token for new token pair | No | +| GET | `/api/v1/auth/me` | Get current user profile | Bearer | + +### Users (`/api/v1/users`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| GET | `/api/v1/users/` | List users (paginated, admin only) | Bearer | +| POST | `/api/v1/users/` | Create user (admin only) | Bearer | +| PUT | `/api/v1/users/{user_id}` | Update user (admin only) | Bearer | +| DELETE | `/api/v1/users/{user_id}` | Soft-delete user (admin only) | Bearer | + +### Vehicles (`/api/v1/vehicles`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| GET | `/api/v1/vehicles/` | List vehicles (filter, sort, paginate) | Bearer | +| POST | `/api/v1/vehicles/` | Create vehicle | Bearer | +| GET | `/api/v1/vehicles/{vehicle_id}` | Get single vehicle | Bearer | +| PUT | `/api/v1/vehicles/{vehicle_id}` | Update vehicle | Bearer | +| DELETE | `/api/v1/vehicles/{vehicle_id}` | Soft-delete vehicle | Bearer | +| POST | `/api/v1/vehicles/{vehicle_id}/mobile-de/push` | Push to mobile.de (async, 202) | Bearer | +| GET | `/api/v1/vehicles/{vehicle_id}/mobile-de/status` | Get mobile.de sync status | Bearer | + +### Contacts (`/api/v1/contacts`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| GET | `/api/v1/contacts/` | List contacts (filter, sort, paginate) | Bearer | +| POST | `/api/v1/contacts/` | Create contact (admin/verkaeufer) | Bearer | +| GET | `/api/v1/contacts/{contact_id}` | Get contact with persons | Bearer | +| PUT | `/api/v1/contacts/{contact_id}` | Update contact | Bearer | +| DELETE | `/api/v1/contacts/{contact_id}` | Soft-delete contact | Bearer | +| POST | `/api/v1/contacts/{contact_id}/persons` | Add contact person | Bearer | +| DELETE | `/api/v1/contacts/{contact_id}/persons/{person_id}` | Remove contact person | Bearer | + +### Files (`/api/v1/vehicles/{vehicle_id}/files`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| GET | `/api/v1/vehicles/{vehicle_id}/files` | List files for vehicle | Bearer | +| POST | `/api/v1/vehicles/{vehicle_id}/files` | Upload file (multipart, max 20MB) | Bearer | +| GET | `/api/v1/vehicles/{vehicle_id}/files/{file_id}` | Download file | Bearer | +| DELETE | `/api/v1/vehicles/{vehicle_id}/files/{file_id}` | Delete file | Bearer | + +### OCR (`/api/v1/ocr`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| POST | `/api/v1/ocr/upload` | Upload scan for OCR (async, 202) | Bearer | +| GET | `/api/v1/ocr/results/{result_id}` | Get OCR result | Bearer | +| GET | `/api/v1/ocr/results` | List OCR results | Bearer | +| POST | `/api/v1/ocr/results/{result_id}/apply` | Apply OCR data to vehicle | Bearer | + +### Sales (`/api/v1/sales`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| GET | `/api/v1/sales/` | List sales (filter by status/date) | Bearer | +| POST | `/api/v1/sales/` | Create sale (sets vehicle to 'sold') | Bearer | +| GET | `/api/v1/sales/{sale_id}` | Get sale with nested data | Bearer | +| PUT | `/api/v1/sales/{sale_id}` | Update sale | Bearer | +| DELETE | `/api/v1/sales/{sale_id}` | Cancel sale (soft delete) | Bearer | +| POST | `/api/v1/sales/{sale_id}/contract` | Regenerate contract PDF | Bearer | +| GET | `/api/v1/sales/{sale_id}/contract` | Download contract PDF | Bearer | +| POST | `/api/v1/sales/{sale_id}/verify-ust-id` | Verify USt-IdNr. via BZSt API | Bearer | + +### DATEV (`/api/v1/datev`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| POST | `/api/v1/datev/export` | Create DATEV export (CSV) | Bearer | +| GET | `/api/v1/datev/exports` | List DATEV exports | Bearer | +| GET | `/api/v1/datev/exports/{export_id}/download` | Download DATEV CSV | Bearer | + +### Copilot (`/api/v1/copilot`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| POST | `/api/v1/copilot/chat` | Send message to AI Copilot | Bearer | +| POST | `/api/v1/copilot/action` | Execute confirmed action | Bearer | +| GET | `/api/v1/copilot/history` | Get chat history | Bearer | +| POST | `/api/v1/copilot/voice` | Voice input (base64 audio) | Bearer | + +### Retouch (`/api/v1/retouch`) +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| POST | `/api/v1/retouch/process` | Upload image for retouching (async, 202) | Bearer | +| GET | `/api/v1/retouch/results/{result_id}` | Get retouch result | Bearer | +| POST | `/api/v1/retouch/price-compare` | Compare prices for vehicle | Bearer | + +### Health +| Method | Path | Description | Auth | +|--------|------|-------------|------| +| GET | `/api/v1/health` | Health check | No | +| GET | `/` | Root info | No | + +--- + +## 5. Frontend Build Verification + +**Location:** `frontend/.next/` + +**Build artifacts confirmed:** +- `BUILD_ID` — present (20 bytes) +- `build-manifest.json` — present +- `app-build-manifest.json` — present +- `routes-manifest.json` — present +- `server/` — directory present +- `static/` — directory present +- `prerender-manifest.json` — present +- `required-server-files.json` — present +- `trace/` — build trace present + +**Verdict:** ✅ Frontend build artifacts are complete and present. + +--- + +## 6. Environment Verification + +### Required Environment Variables (from `.env.example`) + +| Variable | Required | Status | Notes | +|----------|----------|--------|-------| +| `DATABASE_URL` | ✅ Yes | Set (local PostgreSQL) | `postgresql+asyncpg://erp_test_user:***@localhost:5432/erp_test` | +| `REDIS_URL` | ✅ Yes | Set in .env.example | Redis NOT installed; JWT refresh tokens are stateless (no Redis storage needed) | +| `JWT_SECRET` | ✅ Yes | Placeholder in .env.example | Must be set to a secure random string for production | +| `JWT_ALGORITHM` | Optional | Default HS256 | — | +| `JWT_ACCESS_TTL_MINUTES` | Optional | Default 15 | — | +| `JWT_REFRESH_TTL_DAYS` | Optional | Default 7 | — | +| `CORS_ORIGINS` | ✅ Yes | Set | `http://localhost:3000,http://localhost:3001` | +| `UPLOAD_DIR` | Optional | Default `/tmp/uploads` | Must be persistent volume in production | +| `APP_NAME` | Optional | Default `ERP Nutzfahrzeuge` | — | +| `APP_ENV` | Optional | Default `development` | Set to `production` for deployment | +| `MOBILE_DE_API_KEY` | Optional | Empty | Required for mobile.de integration | +| `MOBILE_DE_SELLER_ID` | Optional | Empty | Required for mobile.de integration | +| `OPENROUTER_API_KEY` | Optional | Not in .env.example | Required for AI Copilot features (currently empty/non-functional) | + +### Infrastructure Status + +| Service | Status | Notes | +|---------|--------|-------| +| PostgreSQL | ✅ Running | localhost:5432, accepting connections | +| Redis | ⚠️ Not installed | Not required for JWT; may be needed for async task queues (OCR, retouch, mobile.de push) | +| OpenRouter API | ⚠️ Not configured | AI Copilot features will not work without API key | + +--- + +## 7. Errors Found + +**No errors, tracebacks, warnings, or critical messages found in runtime logs.** + +Log output was clean: +``` +INFO: Started server process [3383733] +INFO: Waiting for application startup. +INFO: Application startup complete. +INFO: Uvicorn running on http://0.0.0.0:8000 (Press CTRL+C to quit) +INFO: 127.0.0.1:48688 - "GET /api/v1/health HTTP/1.1" 200 OK +INFO: 127.0.0.1:48700 - "GET / HTTP/1.1" 200 OK +INFO: 127.0.0.1:48706 - "GET /api/v1/auth/login HTTP/1.1" 405 Method Not Allowed +INFO: 127.0.0.1:48710 - "GET /openapi.json HTTP/1.1" 200 OK +``` + +--- + +## 8. Known Risks + +| # | Risk | Severity | Mitigation | +|---|------|----------|------------| +| 1 | **Redis not installed** — async task processing (OCR, retouch, mobile.de push) may fail or fall back to synchronous processing | Medium | Install Redis or verify async tasks have graceful fallback. Check if background tasks use in-memory queues. | +| 2 | **OPENROUTER_API_KEY not set** — AI Copilot (`/api/v1/copilot/*`) will not function | Medium | Set `OPENROUTER_API_KEY` in production environment for Copilot features. | +| 3 | **JWT_SECRET is placeholder** — `change-me-to-a-secure-random-string` in .env.example | High | Generate a cryptographically secure secret (≥256 bits) for production. | +| 4 | **PostgreSQL is local only** — no connection pooling or HA configured | Medium | Configure PgBouncer or use managed PostgreSQL (e.g., Coolify managed DB) for production. | +| 5 | **UPLOAD_DIR = `/tmp/uploads`** — ephemeral storage, files lost on container restart | High | Mount a persistent volume for `UPLOAD_DIR` in production. | +| 6 | **CORS_ORIGINS** — currently allows localhost:3000,3001 only | Low | Add production frontend origin to `CORS_ORIGINS` for deployment. | +| 7 | **MOBILE_DE_API_KEY empty** — mobile.de push/status endpoints will fail | Low | Set API key when mobile.de integration is needed. | +| 8 | **No HTTPS/TLS** — backend runs on plain HTTP | Medium | Use reverse proxy (nginx/Traefik) or Coolify proxy for TLS termination. | +| 9 | **No rate limiting** — no rate limiting middleware detected | Medium | Add rate limiting (e.g., slowapi) before public exposure. | + +--- + +## 9. Recommendations + +### Immediate (before deployment) +1. Generate a secure `JWT_SECRET` and configure in production environment +2. Set `APP_ENV=production` for deployment +3. Configure persistent volume for `UPLOAD_DIR` +4. Add production frontend origin to `CORS_ORIGINS` +5. Install Redis if async task processing (OCR, retouch, mobile.de) is required +6. Set `OPENROUTER_API_KEY` if AI Copilot features are needed + +### Deployment preparation +7. Create Dockerfile / docker-compose.yml for containerized deployment +8. Configure reverse proxy with TLS termination (Coolify proxy or nginx) +9. Set up PostgreSQL as managed service or with backup strategy +10. Add health check to Coolify resource configuration (`GET /api/v1/health`) +11. Configure log rotation and monitoring + +### Post-deployment +12. Run smoke tests against production endpoints +13. Verify JWT auth flow end-to-end +14. Test file upload with persistent volume +15. Monitor for async task failures (OCR, retouch) + +--- + +## 10. Sign-off + +| Item | Status | +|------|--------| +| App starts without errors | ✅ Yes | +| Health endpoint returns 200 | ✅ Yes | +| Bounded error output shows no critical runtime failure | ✅ Yes (zero errors) | +| Frontend build artifacts present | ✅ Yes | +| Deployment docs ready | ✅ Yes (see `deploy/` directory) | +| Ready for deployment | ⚠️ Conditional — requires secure JWT_SECRET, persistent storage, and Redis decision | + +**Runtime verification: PASSED** +**Deployment readiness: CONDITIONAL** (env vars + infrastructure must be configured) diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..daf2cf4 --- /dev/null +++ b/backend/Dockerfile @@ -0,0 +1,45 @@ +# ERP Nutzfahrzeuge — Backend Dockerfile +# Python 3.13-slim + FastAPI + uvicorn +# System packages: WeasyPrint (libpango, libcairo), Pillow (libjpeg, libpng) + +FROM python:3.13-slim AS base + +# --- System packages for WeasyPrint and Pillow --- +RUN apt-get update && apt-get install -y --no-install-recommends \ + libpango-1.0-0 \ + libpangoft2-1.0-0 \ + libcairo2 \ + libgdk-pixbuf2.0-0 \ + libjpeg62-turbo \ + libpng16-16 \ + libffi-dev \ + curl \ + && rm -rf /var/lib/apt/lists/* + +# --- Python venv --- +RUN python -m venv /opt/venv +ENV PATH="/opt/venv/bin:$PATH" +ENV PYTHONDONTWRITEBYTECODE=1 +ENV PYTHONUNBUFFERED=1 + +WORKDIR /app + +# --- Install dependencies first (better layer caching) --- +COPY requirements.txt . +RUN pip install --no-cache-dir -r requirements.txt + +# --- Copy application code --- +COPY . . + +# --- Create upload and contract directories --- +RUN mkdir -p /data/uploads /tmp/contracts + +# --- Expose port --- +EXPOSE 8000 + +# --- Health check --- +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \ + CMD curl -f http://localhost:8000/api/v1/health || exit 1 + +# --- Start command --- +CMD ["python", "-m", "uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8000", "--workers", "4"] diff --git a/deploy/coolify-config.md b/deploy/coolify-config.md new file mode 100644 index 0000000..78df370 --- /dev/null +++ b/deploy/coolify-config.md @@ -0,0 +1,173 @@ +# Coolify Deployment Configuration — ERP Nutzfahrzeuge + +--- + +## Overview + +This document describes how to deploy the ERP Nutzfahrzeuge system on Coolify using Docker Compose. + +The system consists of 4 services: + +| Service | Image | Port | Health Check | +|---------|-------|------|-------------| +| Backend | Custom (Python 3.13-slim) | 8000 | `GET /api/v1/health` | +| Frontend | Custom (Node 18-alpine) | 3000 | `GET /` | +| PostgreSQL | `postgres:16-alpine` | 5432 | `pg_isready` | +| Redis | `redis:7-alpine` | 6379 | `redis-cli ping` | + +--- + +## Coolify Resource Setup + +### Step 1: Create Resource + +1. In Coolify, create a new resource +2. Select **Docker Compose** as the resource type +3. Connect your Forgejo/Git repository +4. Set the base directory to the project root +5. Coolify will detect `docker-compose.yml` automatically + +### Step 2: Configure Environment Variables + +1. Navigate to resource → **Environment Variables** +2. Add all variables from `.env.example` (see `deploy/env.md` for descriptions) +3. **Critical variables:** + - `JWT_SECRET` — generate with `openssl rand -hex 32` + - `POSTGRES_PASSWORD` — strong password for database + - `CORS_ORIGINS` — production frontend URL (e.g., `https://erp.your-domain.com`) + - `NEXT_PUBLIC_API_URL` — production backend URL (e.g., `https://api.your-domain.com/api/v1`) +4. **Optional but feature-dependent:** + - `OPENROUTER_API_KEY` — required for AI features (OCR, retouch, copilot) + - `MOBILE_DE_API_KEY` + `MOBILE_DE_SELLER_ID` — required for mobile.de integration +5. Click **Save** and redeploy + +### Step 3: Configure Health Check + +In the resource settings: + +| Setting | Value | +|---------|-------| +| Health check path | `/api/v1/health` | +| Health check port | `8000` | +| Health check interval | `30s` | +| Health check timeout | `5s` | +| Health check retries | `3` | + +> **Note:** The health check targets the backend service. Docker Compose health checks are also defined per-service in `docker-compose.yml`. + +### Step 4: Configure Persistent Storage + +Docker Compose volumes are defined in `docker-compose.yml`: + +| Volume | Mount Point | Purpose | +|--------|-------------|---------| +| `uploads` | `/data/uploads` (backend) | User file uploads (images, documents) | +| `pgdata` | `/var/lib/postgresql/data` (postgres) | Database data | +| `redisdata` | `/data` (redis) | Redis persistence (AOF) | + +In Coolify: +1. Ensure persistent storage is enabled for the resource +2. Verify volume names match the compose file +3. For production, consider using a managed PostgreSQL instance instead of the containerized one + +### Step 5: Configure Domains / Routing + +1. **Backend API:** + - Set domain: `api.your-domain.com` (or subpath) + - Port: `8000` + - This is the primary entry point for Coolify health checks + +2. **Frontend:** + - Set domain: `erp.your-domain.com` (or root domain) + - Port: `3000` + - Next.js standalone server + +3. **CORS:** + - Set `CORS_ORIGINS` to the frontend domain + - Set `NEXT_PUBLIC_API_URL` to the backend domain + +### Step 6: Deploy + +1. Click **Deploy** in Coolify +2. Wait for all services to build and start +3. Verify health checks pass (green status for all services) +4. Test endpoints: + - `GET https://api.your-domain.com/api/v1/health` → 200 `{"status":"ok"}` + - `GET https://erp.your-domain.com/` → 200 (frontend loads) + - `GET https://api.your-domain.com/openapi.json` → 200 (API schema) + +--- + +## Coolify Service Type + +- **Type:** Docker Compose (multi-service) +- **Compose file:** `docker-compose.yml` (project root) +- **Build:** Coolify builds images from `backend/Dockerfile` and `frontend/Dockerfile` +- **Pre-built images:** `postgres:16-alpine`, `redis:7-alpine` + +--- + +## Port Mapping + +| Service | Internal Port | External Port | Notes | +|---------|---------------|---------------|-------| +| Backend | 8000 | 8000 | FastAPI + uvicorn (4 workers) | +| Frontend | 3000 | 3000 | Next.js standalone server | +| PostgreSQL | 5432 | — (internal only) | Not exposed externally | +| Redis | 6379 | — (internal only) | Not exposed externally | + +> In Coolify, only expose backend (8000) and frontend (3000) via domains. PostgreSQL and Redis should remain internal to the Docker network. + +--- + +## Build Configuration + +### Backend +- **Base image:** `python:3.13-slim` +- **System packages:** libpango, libcairo, libjpeg, libpng (for WeasyPrint + Pillow) +- **Python venv:** `/opt/venv` +- **Entry:** `uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4` + +### Frontend +- **Base image:** `node:18-alpine` (multi-stage) +- **Build:** `npm ci && npm run build` (standalone output) +- **Entry:** `node server.js` +- **Requires:** `output: 'standalone'` in `next.config.js` + +--- + +## Post-Deploy Verification + +1. All 4 containers are running and healthy +2. `GET /api/v1/health` → 200 `{"status":"ok"}` +3. Frontend loads at production URL +4. Login flow works: `POST /api/v1/auth/login` +5. CORS headers present for frontend origin +6. No errors in container logs +7. File uploads work (test upload to `/data/uploads`) +8. Database queries return expected data + +--- + +## Troubleshooting + +| Issue | Cause | Solution | +|-------|-------|----------| +| Backend health check fails | DB not ready | Check `postgres` container health; increase `start_period` | +| WeasyPrint errors | Missing system libs | Verify `libpango`, `libcairo` installed in backend image | +| Frontend build fails | Missing `output: 'standalone'` | Add `output: 'standalone'` to `next.config.js` | +| CORS errors | Wrong `CORS_ORIGINS` | Set to exact frontend domain (no trailing slash) | +| AI features fail | Missing `OPENROUTER_API_KEY` | Set API key in Coolify environment variables | +| mobile.de fails | Missing credentials | Set `MOBILE_DE_API_KEY` and `MOBILE_DE_SELLER_ID` | + +--- + +## Rollback + +See `deploy/rollback.md` for the full rollback procedure. + +Quick rollback in Coolify: +1. Navigate to resource → **Deployments** +2. Find last known-good deployment +3. Click **Redeploy** +4. Verify health check passes diff --git a/deploy/env.md b/deploy/env.md new file mode 100644 index 0000000..85d1bfc --- /dev/null +++ b/deploy/env.md @@ -0,0 +1,56 @@ +# Environment Configuration — ERP Nutzfahrzeuge + +**Source:** `backend/.env.example` (safe reference — no live values) + +--- + +## Required Variables + +These variables MUST be set before the application can start. + +| Variable | Description | Example (redacted) | +|----------|-------------|---------------------| +| `DATABASE_URL` | PostgreSQL async connection string | `postgresql+asyncpg://user:***@host:5432/dbname` | +| `JWT_SECRET` | Secret key for JWT signing (≥256 bits) | `` | + +## Important Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `REDIS_URL` | `redis://localhost:6379/0` | Redis connection for async task queues. Not required for JWT (stateless). Required for OCR/retouch/mobile.de async processing. | +| `JWT_ALGORITHM` | `HS256` | JWT signing algorithm | +| `JWT_ACCESS_TTL_MINUTES` | `15` | Access token TTL in minutes | +| `JWT_REFRESH_TTL_DAYS` | `7` | Refresh token TTL in days | +| `CORS_ORIGINS` | `http://localhost:3000,http://localhost:3001` | Comma-separated allowed origins. Add production frontend URL. | +| `UPLOAD_DIR` | `/tmp/uploads` | File upload directory. **Must be a persistent volume in production.** | +| `APP_NAME` | `ERP Nutzfahrzeuge` | Application display name | +| `APP_ENV` | `development` | Set to `production` for deployment | + +## Optional Integration Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `MOBILE_DE_API_KEY` | (empty) | mobile.de API key for vehicle listing push | +| `MOBILE_DE_SELLER_ID` | (empty) | mobile.de seller ID | +| `OPENROUTER_API_KEY` | (not set) | OpenRouter API key for AI Copilot features. Without this, Copilot endpoints will fail. | +| `BZST_API_ENABLED` | `False` | Enable BZSt USt-IdNr. verification API | + +## Production Checklist + +- [ ] `DATABASE_URL` points to production PostgreSQL (managed or backed up) +- [ ] `JWT_SECRET` is a cryptographically secure random string (not placeholder) +- [ ] `APP_ENV=production` +- [ ] `CORS_ORIGINS` includes production frontend origin +- [ ] `UPLOAD_DIR` points to a persistent volume mount +- [ ] `REDIS_URL` set if async features (OCR, retouch, mobile.de) are needed +- [ ] `OPENROUTER_API_KEY` set if AI Copilot is needed +- [ ] `MOBILE_DE_API_KEY` and `MOBILE_DE_SELLER_ID` set if mobile.de integration is needed + +## Coolify Configuration + +In Coolify, set these as environment variables (or via `.env` file in the resource): +1. Navigate to resource → Environment Variables +2. Add each variable with its production value +3. Redeploy after changes + +**Never commit real secret values to the repository.** Use Coolify's secret management or an external secrets manager. diff --git a/deploy/healthcheck.md b/deploy/healthcheck.md new file mode 100644 index 0000000..f7784ec --- /dev/null +++ b/deploy/healthcheck.md @@ -0,0 +1,63 @@ +# Health Check Configuration — ERP Nutzfahrzeuge + +--- + +## Health Endpoint + +**URL:** `GET /api/v1/health` + +**Expected response:** +- HTTP Status: `200 OK` +- Body: `{"status":"ok"}` + +**No authentication required.** + +--- + +## Coolify Health Check + +Configure in Coolify resource settings: + +| Setting | Value | +|---------|-------| +| Health check path | `/api/v1/health` | +| Health check port | `8000` | +| Health check interval | `30s` (recommended) | +| Health check timeout | `5s` (recommended) | +| Health check retries | `3` (recommended) | + +--- + +## Additional Monitoring Endpoints + +| Endpoint | Method | Auth | Purpose | +|----------|--------|------|---------| +| `/` | GET | No | Root info — returns app name, version, docs URL | +| `/openapi.json` | GET | No | OpenAPI schema — useful for API monitoring | +| `/docs` | GET | No | Swagger UI (disable in production via `APP_ENV=production` if needed) | + +--- + +## Docker Compose Health Check + +If using Docker Compose, add to the backend service: + +```yaml +healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s +``` + +--- + +## Troubleshooting + +| Symptom | Likely Cause | Action | +|---------|-------------|--------| +| Health check returns 502 | Backend not started or crashed | Check container logs, verify `DATABASE_URL` is reachable | +| Health check returns 503 | App startup in progress | Increase `start_period` in health check config | +| Health check times out | Network/firewall issue | Verify port mapping and firewall rules | +| Health check returns 500 | Internal error (DB connection, config) | Check application logs for traceback | diff --git a/deploy/rollback.md b/deploy/rollback.md new file mode 100644 index 0000000..5823f05 --- /dev/null +++ b/deploy/rollback.md @@ -0,0 +1,137 @@ +# Rollback Procedure — ERP Nutzfahrzeuge + +--- + +## Rollback Strategy + +The rollback strategy depends on the deployment method used. + +--- + +## Coolify Rollback + +### Option A: Redeploy Previous Image + +1. In Coolify, navigate to the resource +2. Go to **Deployments** tab +3. Find the last known-good deployment +4. Click **Redeploy** on that deployment +5. Wait for health check to pass +6. Verify endpoints: + - `GET /api/v1/health` → 200 + - `GET /` → 200 + +### Option B: Rollback via Git + +1. In the repository, identify the last known-good commit: + ```bash + git log --oneline -10 + ``` +2. Revert to the previous commit: + ```bash + git revert # or git reset --hard + git push origin main + ``` +3. Trigger a new deployment in Coolify (auto-deploy or manual) +4. Wait for health check to pass + +--- + +## Docker Compose Rollback + +### Step 1: Stop Current Deployment + +```bash +docker-compose down +``` + +### Step 2: Revert Code + +```bash +git checkout +``` + +### Step 3: Rebuild and Restart + +```bash +docker-compose build +docker-compose up -d +``` + +### Step 4: Verify + +```bash +curl -s http://localhost:8000/api/v1/health +docker-compose logs --tail 50 backend +``` + +--- + +## Database Rollback + +### If Alembic migrations are used: + +```bash +cd backend +/opt/venv/bin/python -m alembic downgrade -1 # rollback one revision +# or +/opt/venv/bin/python -m alembic downgrade # rollback to specific revision +``` + +### If no migration tool: + +- Database schema changes are applied on app startup via SQLAlchemy `create_all` +- Rollback requires manual SQL or restoring from a database backup +- **Always take a database backup before deployment:** + ```bash + pg_dump -U erp_user erp_test > backup_$(date +%Y%m%d_%H%M%S).sql + ``` + +### Restore from backup: + +```bash +psql -U erp_user erp_test < backup_YYYYMMDD_HHMMSS.sql +``` + +--- + +## Rollback Decision Tree + +| Situation | Action | +|-----------|--------| +| App won't start after deploy | Redeploy previous image (Coolify) or `git checkout` + rebuild (Docker) | +| Health check fails | Redeploy previous image; check DB connectivity | +| Database migration broke schema | `alembic downgrade` or restore from `pg_dump` backup | +| Frontend broken | Rebuild frontend from previous commit; redeploy | +| Performance regression | Redeploy previous image; investigate in staging | +| Security issue found | Rollback immediately; patch in staging; redeploy | + +--- + +## Post-Rollback Verification + +After any rollback: + +1. ✅ Health check passes: `GET /api/v1/health` → 200 `{"status":"ok"}` +2. ✅ Root endpoint: `GET /` → 200 +3. ✅ Login works: `POST /api/v1/auth/login` → 200 with valid credentials +4. ✅ No errors in logs: `docker logs --tail 50` +5. ✅ Frontend loads at production URL +6. ✅ Database queries return expected data + +--- + +## Emergency Rollback (Quick) + +If the deployment is causing immediate issues: + +```bash +# Coolify: use the Redeploy button on the last good deployment +# Docker Compose: +docker-compose down +git checkout +docker-compose build && docker-compose up -d +curl -s http://localhost:8000/api/v1/health +``` + +**Time to rollback:** < 5 minutes (if image is cached) or < 15 minutes (if rebuild needed). diff --git a/deploy/runbook.md b/deploy/runbook.md new file mode 100644 index 0000000..7ca7cd4 --- /dev/null +++ b/deploy/runbook.md @@ -0,0 +1,189 @@ +# Deployment Runbook — ERP Nutzfahrzeuge + +--- + +## Prerequisites + +1. **PostgreSQL** — running and accessible (managed or self-hosted) +2. **Redis** (optional) — if async features (OCR, retouch, mobile.de push) are needed +3. **Node.js** — for frontend build (if building at deploy time) +4. **Python 3.11+** — for backend runtime +5. **Coolify** (or Docker) — for containerized deployment + +--- + +## Pre-Deployment Steps + +### 1. Verify Environment Variables + +Ensure all required variables are configured in Coolify or `.env`: + +```bash +# Required +DATABASE_URL=postgresql+asyncpg://:@:5432/ +JWT_SECRET= + +# Important +APP_ENV=production +CORS_ORIGINS=https://your-frontend-domain.com +UPLOAD_DIR=/app/uploads # persistent volume + +# Optional (feature-dependent) +REDIS_URL=redis://:6379/0 +OPENROUTER_API_KEY= +MOBILE_DE_API_KEY= +MOBILE_DE_SELLER_ID= +``` + +### 2. Build Frontend + +```bash +cd frontend +npm ci +npm run build +``` + +Build artifacts are output to `frontend/.next/`. + +### 3. Verify Backend Dependencies + +```bash +cd backend +/opt/venv/bin/pip install -r requirements.txt +/opt/venv/bin/python -c "from app.main import app; print('Import OK')" +``` + +### 4. Database Migration (if applicable) + +```bash +cd backend +/opt/venv/bin/python -m alembic upgrade head # if Alembic is configured +# Or ensure tables are created via app startup +``` + +--- + +## Deployment Steps (Coolify) + +### Step 1: Create Resource + +1. In Coolify, create a new resource (Docker Compose or Dockerfile-based) +2. Configure the source repository +3. Set the build/pack type + +### Step 2: Configure Environment + +1. Navigate to resource → Environment Variables +2. Add all variables from `deploy/env.md` +3. Ensure `JWT_SECRET` is a secure random value +4. Set `APP_ENV=production` + +### Step 3: Configure Health Check + +1. Set health check path to `/api/v1/health` +2. Set port to `8000` +3. See `deploy/healthcheck.md` for full configuration + +### Step 4: Configure Persistent Storage + +1. Mount a persistent volume for `UPLOAD_DIR` (e.g., `/app/uploads`) +2. Ensure database volume is persistent + +### Step 5: Deploy + +1. Click **Deploy** in Coolify +2. Wait for build and startup to complete +3. Verify health check passes (green status) +4. Test endpoints: + - `GET /api/v1/health` → 200 `{"status":"ok"}` + - `GET /` → 200 app metadata + - `GET /openapi.json` → 200 OpenAPI schema + +### Step 6: Post-Deployment Verification + +1. Verify frontend is accessible at the configured domain +2. Test login flow: `POST /api/v1/auth/login` with valid credentials +3. Verify CORS headers are present for frontend origin +4. Check logs for any errors: + ```bash + docker logs --tail 50 + ``` + +--- + +## Deployment Steps (Docker Compose) + +### docker-compose.yml (template) + +```yaml +version: '3.8' + +services: + backend: + build: ./backend + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql+asyncpg://user:pass@db:5432/erp + - JWT_SECRET=${JWT_SECRET} + - APP_ENV=production + - CORS_ORIGINS=https://your-domain.com + - UPLOAD_DIR=/app/uploads + - REDIS_URL=redis://redis:6379/0 + volumes: + - uploads:/app/uploads + depends_on: + - db + - redis + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + + db: + image: postgres:16-alpine + environment: + - POSTGRES_USER=erp_user + - POSTGRES_PASSWORD=${DB_PASSWORD} + - POSTGRES_DB=erp + volumes: + - pgdata:/var/lib/postgresql/data + + redis: + image: redis:7-alpine + volumes: + - redisdata:/data + + frontend: + build: ./frontend + ports: + - "3000:3000" + depends_on: + - backend + +volumes: + uploads: + pgdata: + redisdata: +``` + +--- + +## Start Command (standalone) + +```bash +cd backend && /opt/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 +``` + +For production with workers: +```bash +cd backend && /opt/venv/bin/python -m uvicorn app.main:app --host 0.0.0.0 --port 8000 --workers 4 +``` + +--- + +## Rollback + +See `deploy/rollback.md` for rollback procedure. diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..edca976 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,125 @@ +# ERP Nutzfahrzeuge — Production Docker Compose +# Services: backend (FastAPI), frontend (Next.js), postgres, redis +# Compatible with Coolify Docker Compose deployment + +services: + # ============ Backend (FastAPI + uvicorn) ============ + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: erp-backend + restart: unless-stopped + ports: + - "8000:8000" + environment: + - DATABASE_URL=postgresql+asyncpg://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/${POSTGRES_DB} + - REDIS_URL=redis://redis:6379/0 + - JWT_SECRET=${JWT_SECRET} + - JWT_ALGORITHM=${JWT_ALGORITHM:-HS256} + - JWT_ACCESS_TTL_MINUTES=${JWT_ACCESS_TTL_MINUTES:-15} + - JWT_REFRESH_TTL_DAYS=${JWT_REFRESH_TTL_DAYS:-7} + - CORS_ORIGINS=${CORS_ORIGINS} + - UPLOAD_DIR=/data/uploads + - WEASYPRINT_OUTPUT_DIR=/tmp/contracts + - APP_NAME=${APP_NAME:-ERP Nutzfahrzeuge} + - APP_ENV=production + - OPENROUTER_API_KEY=${OPENROUTER_API_KEY} + - OPENROUTER_BASE_URL=${OPENROUTER_BASE_URL:-https://openrouter.ai/api/v1} + - OPENROUTER_OCR_MODEL=${OPENROUTER_OCR_MODEL:-qwen/qwen2.5-vl-72b-instruct} + - MAX_FILE_SIZE_MB=${MAX_FILE_SIZE_MB:-50} + - MOBILE_DE_API_KEY=${MOBILE_DE_API_KEY} + - MOBILE_DE_SELLER_ID=${MOBILE_DE_SELLER_ID} + - BZST_API_ENABLED=${BZST_API_ENABLED:-false} + volumes: + - uploads:/data/uploads + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + healthcheck: + test: ["CMD", "curl", "-f", "http://localhost:8000/api/v1/health"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 15s + networks: + - erp-network + + # ============ Frontend (Next.js standalone) ============ + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000/api/v1} + container_name: erp-frontend + restart: unless-stopped + ports: + - "3000:3000" + environment: + - NEXT_PUBLIC_API_URL=${NEXT_PUBLIC_API_URL:-http://localhost:8000/api/v1} + - NODE_ENV=production + depends_on: + backend: + condition: service_healthy + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:3000/"] + interval: 30s + timeout: 5s + retries: 3 + start_period: 10s + networks: + - erp-network + + # ============ PostgreSQL 16 ============ + postgres: + image: postgres:16-alpine + container_name: erp-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=${POSTGRES_USER} + - POSTGRES_PASSWORD=${POSTGRES_PASSWORD} + - POSTGRES_DB=${POSTGRES_DB} + volumes: + - pgdata:/var/lib/postgresql/data + healthcheck: + test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DB}"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + networks: + - erp-network + + # ============ Redis 7 ============ + redis: + image: redis:7-alpine + container_name: erp-redis + restart: unless-stopped + command: redis-server --appendonly yes --maxmemory 256mb --maxmemory-policy allkeys-lru + volumes: + - redisdata:/data + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + networks: + - erp-network + +# ============ Volumes ============ +volumes: + uploads: + driver: local + pgdata: + driver: local + redisdata: + driver: local + +# ============ Networks ============ +networks: + erp-network: + driver: bridge diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..7f77172 --- /dev/null +++ b/frontend/Dockerfile @@ -0,0 +1,46 @@ +# ERP Nutzfahrzeuge — Frontend Dockerfile +# Node 18 + Next.js 14.2.5 standalone build +# Multi-stage build: deps -> builder -> runner + +# ============ Stage 1: Dependencies ============ +FROM node:18-alpine AS deps +WORKDIR /app +COPY package.json package-lock.json ./ +RUN npm ci + +# ============ Stage 2: Build ============ +FROM node:18-alpine AS builder +WORKDIR /app +COPY --from=deps /app/node_modules ./node_modules +COPY . . + +# Build-time environment variables +ARG NEXT_PUBLIC_API_URL=http://localhost:8000/api/v1 +ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL + +RUN npm run build + +# ============ Stage 3: Runner (production) ============ +FROM node:18-alpine AS runner +WORKDIR /app + +ENV NODE_ENV=production + +# Non-root user for security +RUN addgroup --system --gid 1001 nodejs && \ + adduser --system --uid 1001 nextjs + +# Copy standalone build output +COPY --from=builder /app/public ./public +COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./ +COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static + +USER nextjs + +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=5s --retries=3 --start-period=10s \ + CMD wget --no-verbose --tries=1 --spider http://localhost:3000/ || exit 1 + +CMD ["node", "server.js"] diff --git a/frontend/next.config.js b/frontend/next.config.js index 50b939e..635aab6 100644 --- a/frontend/next.config.js +++ b/frontend/next.config.js @@ -2,6 +2,7 @@ const { NextConfig } = require('next'); /** @type {import('next').NextConfig} */ const nextConfig = { + output: 'standalone', reactStrictMode: true, env: { NEXT_PUBLIC_API_URL: process.env.NEXT_PUBLIC_API_URL || 'http://localhost:8000/api/v1',