From 848452f8373d6cc17732e0341f6056a2f06692d6 Mon Sep 17 00:00:00 2001 From: Lars Date: Sat, 4 Jul 2026 18:15:24 +0200 Subject: [PATCH] Infrastruktur-Grundgeruest fuer Kairo Jinkendo: Docker, Gitea Actions, Deployment-Doku (ohne App-Code). Co-authored-by: Cursor --- .env.example | 70 +++++++ .gitea/workflows/deploy-dev.yml | 28 +++ .gitea/workflows/deploy-prod.yml | 23 +++ .gitea/workflows/test.yml | 312 +++++++++++++++++++++++++++++ .gitignore | 76 +++++++ CLAUDE.md | 59 ++++++ README.md | 63 ++++++ backend/README.md | 14 ++ docker-compose.dev-env.yml | 79 ++++++++ docker-compose.yml | 82 ++++++++ docs/DEPLOYMENT.md | 127 ++++++++++++ frontend/README.md | 13 ++ package.json | 11 + playwright.config.js | 19 ++ scripts/load/README.md | 16 ++ scripts/load/k6-health-baseline.js | 32 +++ 16 files changed, 1024 insertions(+) create mode 100644 .env.example create mode 100644 .gitea/workflows/deploy-dev.yml create mode 100644 .gitea/workflows/deploy-prod.yml create mode 100644 .gitea/workflows/test.yml create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.md create mode 100644 backend/README.md create mode 100644 docker-compose.dev-env.yml create mode 100644 docker-compose.yml create mode 100644 docs/DEPLOYMENT.md create mode 100644 frontend/README.md create mode 100644 package.json create mode 100644 playwright.config.js create mode 100644 scripts/load/README.md create mode 100644 scripts/load/k6-health-baseline.js diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..dda5441 --- /dev/null +++ b/.env.example @@ -0,0 +1,70 @@ +# === .env neben der jeweiligen docker-compose.*.yml kopieren ==================== +# Docker Compose ersetzt ${VARIABLE} beim Start. +# +# Pro Umgebung eigene Datei (z. B. ~/docker/kairo/.env für Prod, +# ~/docker/kairo-dev/.env für Dev) — dieselben SCHLÜSSEL, unterschiedliche Werte. +# Kein separates DEV_APP_URL vs APP_URL: immer APP_URL, ALLOWED_ORIGINS, DB_*, … + +# ─── Typische Werte PROD (docker-compose.yml) ───────────────────────────────── +# DB_NAME=kairo +# DB_USER=kairo_user +# DB_PASSWORD=… +# APP_URL=https://kairo.jinkendo.de +# ALLOWED_ORIGINS=https://kairo.jinkendo.de +# ENVIRONMENT=production +# KAIRO_MEDIA_HOST=/kairo-media +# MEDIA_ROOT=/app/media + +# ─── Typische Werte DEV (docker-compose.dev-env.yml) ───────────────────────── +# DB_NAME=kairo_dev +# DB_USER=kairo_dev +# DB_PASSWORD=dev_password +# APP_URL=https://dev.kairo.jinkendo.de +# ALLOWED_ORIGINS=https://dev.kairo.jinkendo.de,http://192.168.2.49:3097 +# ENVIRONMENT=development +# KAIRO_MEDIA_HOST=/kairo-media/dev +# MEDIA_ROOT=/app/media + +# ─── Ab hier: eine ausfüllbare Vorlage (bei uns meist Prod-Defaults) ─────────── +DB_HOST=postgres +DB_PORT=5432 +DB_NAME=kairo +DB_USER=kairo_user +DB_PASSWORD=CHANGE_ME_SECURE_PASSWORD + +OPENROUTER_API_KEY=your_api_key_here +OPENROUTER_MODEL=anthropic/claude-sonnet-4 + +# Vereins-Kontingente hart blockieren (KI-Kosten!). Nur 1, true oder yes aktivieren. +CLUB_FEATURE_ENFORCE=1 + +# KI-Debug (Docker): KAIRO_AI_DEBUG in docker-compose*.yml angebunden — 1 = ausführliche WARN-Logs. +# KAIRO_AI_DEBUG=1 + +SMTP_HOST=smtp.example.com +SMTP_PORT=587 +SMTP_USER=noreply@jinkendo.de +SMTP_PASS=your_smtp_password +SMTP_FROM=noreply@jinkendo.de +SMTP_SSL= +SMTP_STARTTLS= + +AUTO_ADMIN_FIRST_USER=true +ADMIN_BOOTSTRAP_EMAILS= + +APP_URL=https://kairo.jinkendo.de +ALLOWED_ORIGINS=https://kairo.jinkendo.de +ENVIRONMENT=production + +# Medien (Docker Compose): KAIRO_MEDIA_HOST = Verzeichnis auf dem Host (Bind-Mount), +# MEDIA_ROOT = gleicher Pfad im Container (muss mit dem Mount-Ziel übereinstimmen). +KAIRO_MEDIA_HOST=/kairo-media +MEDIA_ROOT=/app/media + +MEDIAWIKI_API_URL=https://karatetrainer.net/api.php +MEDIAWIKI_USER=Jinkendo +MEDIAWIKI_PASSWORD=CHANGE_ME +MEDIAWIKI_CATEGORY_EXERCISES=Übungen +MEDIAWIKI_CATEGORY_SKILLS=Fähigkeitsbeschreibung +MEDIAWIKI_CATEGORY_METHODS=Methodenbeschreibung +MEDIAWIKI_CATEGORY_MODELS=Reifegradmodelle diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml new file mode 100644 index 0000000..a9af2a9 --- /dev/null +++ b/.gitea/workflows/deploy-dev.yml @@ -0,0 +1,28 @@ +name: Deploy Development + +on: + push: + branches: [develop] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy to Development + run: | + set -e + echo "=== Deploying Kairo to DEVELOPMENT ===" + cd /home/lars/docker/kairo-dev + git fetch origin develop || git clone http://192.168.2.144:3000/Lars/Kairo-Jinkendo.git . + git reset --hard origin/develop + docker compose -f docker-compose.dev-env.yml build --no-cache + docker compose -f docker-compose.dev-env.yml up -d + sleep 5 + if ! curl -sf http://localhost:8097/api/version; then + echo "✗ DEV API nicht erreichbar — Backend-Logs (Migration/Startup):" + docker compose -f docker-compose.dev-env.yml logs backend --tail 120 || true + exit 1 + fi + echo "✓ DEV API healthy" + curl -sf http://localhost:3097/api/version && echo "✓ DEV über Frontend-Nginx (wie Browser) healthy" + echo "=== Kairo DEV Deploy complete ===" diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..51f6801 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -0,0 +1,23 @@ +name: Deploy Production + +on: + push: + branches: [main] + +jobs: + deploy: + runs-on: ubuntu-latest + steps: + - name: Deploy to Production + run: | + set -e + echo "=== Deploying Kairo to PRODUCTION ===" + cd /home/lars/docker/kairo + git fetch origin main || git clone http://192.168.2.144:3000/Lars/Kairo-Jinkendo.git . + git reset --hard origin/main + docker compose build --no-cache + docker compose up -d + sleep 5 + curl -sf http://localhost:8004/api/version && echo "✓ PROD API (direkt) healthy" + curl -sf http://localhost:3004/api/version && echo "✓ PROD API über Frontend-Nginx (wie Browser) healthy" + echo "=== Kairo PROD Deploy complete ===" diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml new file mode 100644 index 0000000..67c864b --- /dev/null +++ b/.gitea/workflows/test.yml @@ -0,0 +1,312 @@ +name: Test Suite + +# develop: push/PR → Tests gegen Dev (parallel oder vor Deploy Development). +# main: kein push/PR-Trigger — vermeidet doppelten Dev-Lauf beim Merge develop→main; +# Prod-Tests nur via workflow_run nach erfolgreichem Deploy Production. +on: + push: + branches: [develop] + pull_request: + branches: [develop] + workflow_run: + workflows: ["Deploy Development", "Deploy Production"] + types: [completed] + +jobs: + pytest-backend: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Backend pytest im deployten Container + run: | + set -e + EVENT_NAME="${{ github.event_name }}" + REF_NAME="${{ github.ref_name }}" + BASE_REF="${{ github.base_ref }}" + RUN_WORKFLOW="${{ github.event.workflow_run.name }}" + APP_DIR="/home/lars/docker/kairo" + COMPOSE_FILE="docker-compose.yml" + + if [ "$EVENT_NAME" = "workflow_run" ]; then + if [ "$RUN_WORKFLOW" = "Deploy Development" ]; then + APP_DIR="/home/lars/docker/kairo-dev" + COMPOSE_FILE="docker-compose.dev-env.yml" + fi + elif [ "$REF_NAME" = "develop" ] || [ "$BASE_REF" = "develop" ]; then + APP_DIR="/home/lars/docker/kairo-dev" + COMPOSE_FILE="docker-compose.dev-env.yml" + fi + + cd "$APP_DIR" + echo "Warte auf stabilen backend-Container …" + for i in $(seq 1 60); do + if docker compose -f "$COMPOSE_FILE" exec -T backend true 2>/dev/null; then + echo "Backend bereit (Versuch $i)" + break + fi + if [ "$i" -eq 60 ]; then + echo "Timeout: backend-Container nicht bereit" + docker compose -f "$COMPOSE_FILE" ps || true + docker compose -f "$COMPOSE_FILE" logs backend --tail 80 || true + exit 1 + fi + sleep 5 + done + + docker compose -f "$COMPOSE_FILE" exec -T backend sh -lc " + pip install -r /app/requirements-dev.txt && + cd /app && + ACCESS_LAYER_STRICT=1 python scripts/check_access_layer_hints.py && + python scripts/security_release_checks.py && + ACCESS_LAYER_INTEGRATION=1 SKIP_DB_MIGRATE=1 python -m pytest tests -m 'not slow' -ra -vv --tb=short + " + + lint-backend: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Check backend syntax + run: | + EVENT_NAME="${{ github.event_name }}" + REF_NAME="${{ github.ref_name }}" + RUN_WORKFLOW="${{ github.event.workflow_run.name }}" + APP_DIR="/home/lars/docker/kairo" + + if [ "$EVENT_NAME" = "workflow_run" ]; then + if [ "$RUN_WORKFLOW" = "Deploy Development" ]; then + APP_DIR="/home/lars/docker/kairo-dev" + fi + elif [ "$REF_NAME" = "develop" ]; then + APP_DIR="/home/lars/docker/kairo-dev" + fi + + python3 -m py_compile "$APP_DIR/backend/main.py" + echo "✓ Backend syntax OK" + + build-frontend: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + steps: + - name: Build frontend + run: | + EVENT_NAME="${{ github.event_name }}" + REF_NAME="${{ github.ref_name }}" + RUN_WORKFLOW="${{ github.event.workflow_run.name }}" + APP_DIR="/home/lars/docker/kairo" + + if [ "$EVENT_NAME" = "workflow_run" ]; then + if [ "$RUN_WORKFLOW" = "Deploy Development" ]; then + APP_DIR="/home/lars/docker/kairo-dev" + fi + elif [ "$REF_NAME" = "develop" ]; then + APP_DIR="/home/lars/docker/kairo-dev" + fi + + cd "$APP_DIR/frontend" + npm install + npm run build + echo "✓ Frontend build OK" + + k6-health-baseline: + name: k6 /health Baseline + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + env: + E2E_TARGET_URL: https://dev.kairo.jinkendo.de + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: E2E-Ziel wählen (Dev über Proxy vs. Production) + id: e2e + run: | + EVENT="${{ github.event_name }}" + WF_NAME="${{ github.event.workflow_run.name }}" + DEV_BASE="${{ env.E2E_TARGET_URL }}" + if [ "$EVENT" = "workflow_run" ] && [ "$WF_NAME" = "Deploy Production" ]; then + echo "mode=prod" >> $GITHUB_OUTPUT + echo "base_url=https://kairo.jinkendo.de" >> $GITHUB_OUTPUT + echo "→ k6 gegen Prod-Basis." + else + echo "mode=dev" >> $GITHUB_OUTPUT + echo "base_url=${DEV_BASE}" >> $GITHUB_OUTPUT + echo "→ k6 gegen Dev (${DEV_BASE})." + fi + + - name: Dev /health abwarten + if: ${{ steps.e2e.outputs.mode == 'dev' }} + run: | + BASE="${{ steps.e2e.outputs.base_url }}" + echo "Warte auf $BASE/health …" + for i in $(seq 1 90); do + if curl -sf "$BASE/health" >/dev/null 2>&1; then + echo "Health OK (Versuch $i)" + exit 0 + fi + sleep 2 + done + echo "Timeout: Dev /health nicht erreichbar — Deploy / DNS / Firewall prüfen." + curl -v "$BASE/health" || true + exit 1 + + - name: Prod /health abwarten + if: ${{ steps.e2e.outputs.mode == 'prod' }} + run: | + BASE="${{ steps.e2e.outputs.base_url }}" + echo "Warte auf $BASE/health …" + for i in $(seq 1 60); do + if curl -sf "$BASE/health" >/dev/null 2>&1; then + echo "Health OK (Versuch $i)" + exit 0 + fi + sleep 5 + done + echo "Timeout: Prod /health nicht erreichbar" + curl -v "$BASE/health" || true + exit 1 + + - name: Install k6 + run: | + set -e + K6_VER="v0.55.0" + ARCH=$(uname -m) + case "$ARCH" in + x86_64) K6_ARCH=amd64 ;; + aarch64|arm64) K6_ARCH=arm64 ;; + *) echo "k6: unbekannte Architektur: $ARCH"; exit 1 ;; + esac + echo "Installing k6 ${K6_VER} linux-${K6_ARCH}" + curl -sSL "https://github.com/grafana/k6/releases/download/${K6_VER}/k6-${K6_VER}-linux-${K6_ARCH}.tar.gz" -o /tmp/k6.tgz + tar -xzf /tmp/k6.tgz -C /tmp + sudo mv "/tmp/k6-${K6_VER}-linux-${K6_ARCH}/k6" /usr/local/bin/k6 + k6 version + + - name: k6 Health-Baseline (parallele /health) + env: + BASE_URL: ${{ steps.e2e.outputs.base_url }} + run: | + set -e + echo "k6 gegen BASE_URL=$BASE_URL" + k6 run scripts/load/k6-health-baseline.js + echo "✓ k6 Health-Baseline passed" + + playwright-tests: + if: ${{ github.event_name != 'workflow_run' || github.event.workflow_run.conclusion == 'success' }} + runs-on: ubuntu-latest + env: + E2E_TARGET_URL: https://dev.kairo.jinkendo.de + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + + - name: E2E-Ziel wählen (Dev über Proxy vs. Production) + id: e2e + run: | + EVENT="${{ github.event_name }}" + WF_NAME="${{ github.event.workflow_run.name }}" + DEV_BASE="${{ env.E2E_TARGET_URL }}" + if [ "$EVENT" = "workflow_run" ] && [ "$WF_NAME" = "Deploy Production" ]; then + echo "mode=prod" >> $GITHUB_OUTPUT + echo "base_url=https://kairo.jinkendo.de" >> $GITHUB_OUTPUT + echo "→ Prod. Secrets: E2E_PROD_TEST_EMAIL / E2E_PROD_TEST_PASSWORD." + else + echo "mode=dev" >> $GITHUB_OUTPUT + echo "base_url=${DEV_BASE}" >> $GITHUB_OUTPUT + echo "→ Deployte Dev-Umgebung (${DEV_BASE}). Secrets: E2E_DEV_TEST_EMAIL / E2E_DEV_TEST_PASSWORD." + fi + + - name: Dev /health abwarten + if: ${{ steps.e2e.outputs.mode == 'dev' }} + run: | + BASE="${{ steps.e2e.outputs.base_url }}" + echo "Warte auf $BASE/health …" + for i in $(seq 1 90); do + if curl -sf "$BASE/health" >/dev/null 2>&1; then + echo "Health OK (Versuch $i)" + exit 0 + fi + sleep 2 + done + echo "Timeout: Dev /health nicht erreichbar — Deploy / DNS / Firewall prüfen." + curl -v "$BASE/health" || true + exit 1 + + - name: Prod /health abwarten + if: ${{ steps.e2e.outputs.mode == 'prod' }} + run: | + BASE="${{ steps.e2e.outputs.base_url }}" + echo "Warte auf $BASE/health …" + for i in $(seq 1 60); do + if curl -sf "$BASE/health" >/dev/null 2>&1; then + echo "Health OK (Versuch $i)" + exit 0 + fi + sleep 5 + done + echo "Timeout: Prod /health nicht erreichbar" + curl -v "$BASE/health" || true + exit 1 + + - name: Testnutzer registrieren (Dev, nur wenn möglich) + if: ${{ steps.e2e.outputs.mode == 'dev' }} + env: + E2E_DEV_TEST_EMAIL: ${{ secrets.E2E_DEV_TEST_EMAIL }} + E2E_DEV_TEST_PASSWORD: ${{ secrets.E2E_DEV_TEST_PASSWORD }} + run: | + BASE="${{ steps.e2e.outputs.base_url }}" + if [ -z "$E2E_DEV_TEST_EMAIL" ] || [ -z "$E2E_DEV_TEST_PASSWORD" ]; then + echo "(Registrierung übersprungen — Secrets E2E_DEV_* nicht gesetzt.)" + exit 0 + fi + curl -sf -X POST "$BASE/api/auth/register" \ + -H "Content-Type: application/json" \ + -d "{\"email\":\"${E2E_DEV_TEST_EMAIL}\",\"password\":\"${E2E_DEV_TEST_PASSWORD}\",\"name\":\"Playwright CI\"}" \ + || echo "(Register evtl. schon erfolgt oder Limits — Login-Test gilt trotzdem.)" + + - name: Install Playwright + run: | + npm ci || npm install + npx playwright install --with-deps chromium + + - name: Run Playwright tests + env: + E2E_DEV_TEST_EMAIL: ${{ secrets.E2E_DEV_TEST_EMAIL }} + E2E_DEV_TEST_PASSWORD: ${{ secrets.E2E_DEV_TEST_PASSWORD }} + run: | + set -e + MODE="${{ steps.e2e.outputs.mode }}" + BASE_URL="${{ steps.e2e.outputs.base_url }}" + export PLAYWRIGHT_BASE_URL="$BASE_URL" + + if [ "$MODE" = "prod" ]; then + export TEST_EMAIL="${{ secrets.E2E_PROD_TEST_EMAIL }}" + export TEST_PASSWORD="${{ secrets.E2E_PROD_TEST_PASSWORD }}" + if [ -z "$TEST_EMAIL" ] || [ -z "$TEST_PASSWORD" ]; then + echo "Fehler: E2E_PROD_TEST_EMAIL und E2E_PROD_TEST_PASSWORD setzen." + exit 1 + fi + else + export TEST_EMAIL="$E2E_DEV_TEST_EMAIL" + export TEST_PASSWORD="$E2E_DEV_TEST_PASSWORD" + if [ -z "$TEST_EMAIL" ] || [ -z "$TEST_PASSWORD" ]; then + echo "Fehler: E2E_DEV_TEST_EMAIL und E2E_DEV_TEST_PASSWORD setzen (Playwright soll gegen Dev einloggen)." + exit 1 + fi + fi + + mkdir -p screenshots + npx playwright test + echo "✓ Playwright tests passed" + + - name: Upload test screenshots + if: failure() + uses: actions/upload-artifact@v3 + with: + name: playwright-screenshots + path: screenshots/ + retention-days: 7 diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..06b8805 --- /dev/null +++ b/.gitignore @@ -0,0 +1,76 @@ +# Environment +.env +.env.local +.env.production + +# Dependencies +node_modules/ +__pycache__/ +*.pyc +*.pyo +.Python +*.egg-info/ +dist/ +build/ +.venv/ +venv/ + +# Build output +frontend/dist/ + +# Data (NEVER commit database or user data) +*.db +*.sqlite +*.sqlite3 +data/ +photos/ +uploads/ +media/ + +# Logs +*.log +logs/ + +# IDE +.vscode/settings.json +.idea/ +*.swp + +# OS +.DS_Store +Thumbs.db + +# Docker overrides +docker-compose.override.yml + +# SSL certificates (never commit) +nginx/ssl/ +nginx/certbot/ +*.pem +*.key +*.crt +*.csr + +# Pytest +.pytest_cache/ +.coverage +coverage/ + +# Temp +tmp/ +*.tmp + +# Claude: nur ausgewählte Bereiche versionieren +.claude/** +!.claude/README.md +!.claude/docs/ +!.claude/docs/**/* +!.claude/rules/ +!.claude/rules/**/* +!.claude/commands/ +!.claude/commands/**/* +.claude/settings.local.json + +# Cursor MCP +.cursor/mcp.json +frontend/package-lock.json diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..da3c2d2 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,59 @@ +# Kairo Jinkendo – Entwickler-Kontext + +## Projekt-Übersicht + +**Kairo Jinkendo** (回廊 Jinkendo) — Schwesterprodukt in der **Jinkendo**-App-Familie (人拳道). +Domains: kairo.jinkendo.de · dev.kairo.jinkendo.de + +**Status:** Infrastruktur (Docker, Gitea Actions, Deployment-Doku) ist eingerichtet. Anwendungscode folgt. + +## Tech-Stack (geplant) + +| Komponente | Technologie | +|-----------|-------------| +| Frontend | React 18 + Vite + PWA (Node 20) | +| Backend | FastAPI Python 3.12 | +| Datenbank | PostgreSQL 16 Alpine | +| Container | Docker + Docker Compose | +| Auth | Token-basiert + bcrypt (Familien-Standard) | + +**Ports:** Prod 3004/8004 · Dev 3097/8097 — nicht ändern ohne explizite Freigabe (Reverse Proxy/Fritz!Box). + +## Deployment + +``` +Internet → Fritz!Box (privat.stommer.com) → Synology NAS → Raspberry Pi 5 (192.168.2.49) + +Git Workflow: + develop → Auto-Deploy → dev.kairo.jinkendo.de (kairo-dev/, Port 3097/8097) + main → Auto-Deploy → kairo.jinkendo.de (kairo/, Port 3004/8004) + +Gitea: http://192.168.2.144:3000/Lars/Kairo-Jinkendo +Runner: Raspberry Pi (/home/lars/gitea-runner/) — gemeinsam mit Shinkan/Mitai + +Manuell: + cd /home/lars/docker/kairo[-dev] + docker compose -f docker-compose[.dev-env].yml build --no-cache && up -d +``` + +## Verzeichnisstruktur (Zielbild) + +``` +backend/ # FastAPI — noch anzulegen +frontend/ # React + Vite — noch anzulegen +.gitea/workflows/ # CI/CD (deploy + test) +docs/ # Deployment-Doku +scripts/load/ # k6 Health-Baseline +``` + +## Referenz + +Deployment-Muster und Familien-Standards: Schwesterprojekt **shinkan-jinkendo** (`c:\Dev\shinkan-jinkendo`). + +## Jinkendo-Familie + +``` +mitai.jinkendo.de → Körper-Tracker (身体) +shinkan.jinkendo.de → Trainingsplanung (真観) +kairo.jinkendo.de → (回廊 — Produktdefinition folgt) +``` diff --git a/README.md b/README.md new file mode 100644 index 0000000..2220726 --- /dev/null +++ b/README.md @@ -0,0 +1,63 @@ +# Kairo Jinkendo (回廊) + +**Produktfamilie Jinkendo (人拳道) — Schwesterprodukt zu Shinkan und Mitai** + +> Infrastruktur-Setup: Deployment, Docker und Gitea Actions sind vorbereitet. +> Anwendungscode (Backend/Frontend) folgt in einem separaten Schritt. + +## Deployment-Übersicht + +| Umgebung | Branch | Server-Pfad | Host-Ports (UI/API) | Domain | +|----------|--------|-------------|---------------------|--------| +| **Development** | `develop` | `/home/lars/docker/kairo-dev` | 3097 / 8097 | https://dev.kairo.jinkendo.de | +| **Production** | `main` | `/home/lars/docker/kairo` | 3004 / 8004 | https://kairo.jinkendo.de | + +Auto-Deploy via Gitea Actions auf dem Raspberry Pi (gleicher Runner wie Shinkan/Mitai). + +**Gitea:** http://192.168.2.144:3000/Lars/Kairo-Jinkendo + +## Tech-Stack (geplant) + +- **Frontend:** React 18 + Vite + PWA +- **Backend:** FastAPI (Python 3.12) +- **Datenbank:** PostgreSQL 16 +- **Container:** Docker + Docker Compose + +## Lokales Setup (wenn App-Code vorhanden) + +```bash +git clone http://192.168.2.144:3000/Lars/Kairo-Jinkendo.git +cd Kairo-Jinkendo +cp .env.example .env +# .env anpassen + +# Development +docker compose -f docker-compose.dev-env.yml up --build + +# Production (lokal) +docker compose up --build +``` + +Frontend (Dev): http://localhost:3097 +Backend (Dev): http://localhost:8097 + +## Server-Einrichtung + +Siehe [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) für Verzeichnisse, `.env`-Dateien und Reverse-Proxy-Hinweise auf dem Pi. + +## Git-Workflow + +``` +develop → Auto-Deploy → kairo-dev (Dev) +main → Auto-Deploy → kairo (Prod) +``` + +## Dokumentation + +- **Deployment:** [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) +- **Entwickler-Kontext:** [CLAUDE.md](CLAUDE.md) +- **Schwesterprojekt (Referenz):** [shinkan-jinkendo](http://192.168.2.144:3000/Lars/shinkan-jinkendo) + +## Lizenz + +Proprietary – Lars Stommer diff --git a/backend/README.md b/backend/README.md new file mode 100644 index 0000000..b14904a --- /dev/null +++ b/backend/README.md @@ -0,0 +1,14 @@ +# Backend + +FastAPI-Anwendung — noch anzulegen. + +Erwartete Struktur (analog shinkan-jinkendo): + +``` +backend/ +├── Dockerfile +├── main.py +├── requirements.txt +├── migrations/ +└── routers/ +``` diff --git a/docker-compose.dev-env.yml b/docker-compose.dev-env.yml new file mode 100644 index 0000000..1f8cf22 --- /dev/null +++ b/docker-compose.dev-env.yml @@ -0,0 +1,79 @@ +# Keine festen container_name — Compose-Namen haben Projektprefix (-postgres-1). +# Medien: In .env KAIRO_MEDIA_HOST (Host-Pfad) und optional MEDIA_ROOT (Container-Pfad) setzen. +# Default Host /kairo-media/dev — Verzeichnis ggf. anlegen oder Compose legt es an. + +services: + postgres: + image: postgres:16-alpine + environment: + POSTGRES_DB: "${DB_NAME:-kairo_dev}" + POSTGRES_USER: "${DB_USER:-kairo_dev}" + POSTGRES_PASSWORD: "${DB_PASSWORD:-dev_password}" + volumes: + - dev-kairo-db-data:/var/lib/postgresql/data + ports: + - "5437:5432" + restart: unless-stopped + networks: + - dev-kairo-network + + backend: + build: + context: ./backend + dockerfile: Dockerfile + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: "${DB_NAME:-kairo_dev}" + DB_USER: "${DB_USER:-kairo_dev}" + DB_PASSWORD: "${DB_PASSWORD:-dev_password}" + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + OPENROUTER_MODEL: ${OPENROUTER_MODEL} + KAIRO_AI_DEBUG: "${KAIRO_AI_DEBUG:-}" + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_USER: ${SMTP_USER} + SMTP_PASS: ${SMTP_PASS} + SMTP_FROM: ${SMTP_FROM} + APP_URL: "${APP_URL:-https://dev.kairo.jinkendo.de}" + ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://dev.kairo.jinkendo.de,http://192.168.2.49:3097}" + ENVIRONMENT: "${ENVIRONMENT:-development}" + CLUB_FEATURE_ENFORCE: "${CLUB_FEATURE_ENFORCE:-1}" + MEDIAWIKI_API_URL: "${MEDIAWIKI_API_URL:-https://karatetrainer.net/api.php}" + MEDIAWIKI_USER: "${MEDIAWIKI_USER:-Jinkendo}" + MEDIAWIKI_PASSWORD: "${MEDIAWIKI_PASSWORD:-CHANGE_ME}" + MEDIAWIKI_CATEGORY_EXERCISES: "${MEDIAWIKI_CATEGORY_EXERCISES:-Übungen}" + MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" + MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" + MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" + MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}" + volumes: + - ${KAIRO_MEDIA_HOST:-/kairo-media/dev}:${MEDIA_ROOT:-/app/media} + ports: + - "8097:8000" + depends_on: + - postgres + restart: unless-stopped + networks: + - dev-kairo-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: "" + ports: + - "3097:80" + depends_on: + - backend + restart: unless-stopped + networks: + - dev-kairo-network + +volumes: + dev-kairo-db-data: + +networks: + dev-kairo-network: + driver: bridge diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..1227c11 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,82 @@ +services: + postgres: + image: postgres:16-alpine + container_name: kairo-db-prod + environment: + POSTGRES_DB: "${DB_NAME:-kairo}" + POSTGRES_USER: "${DB_USER:-kairo_user}" + POSTGRES_PASSWORD: ${DB_PASSWORD} + volumes: + - kairo-db-data:/var/lib/postgresql/data + ports: + - "127.0.0.1:5436:5432" + restart: unless-stopped + networks: + - kairo-network + + backend: + build: + context: ./backend + dockerfile: Dockerfile + container_name: kairo-api + environment: + DB_HOST: postgres + DB_PORT: 5432 + DB_NAME: kairo + DB_USER: kairo_user + DB_PASSWORD: ${DB_PASSWORD} + OPENROUTER_API_KEY: ${OPENROUTER_API_KEY} + OPENROUTER_MODEL: ${OPENROUTER_MODEL} + KAIRO_AI_DEBUG: "${KAIRO_AI_DEBUG:-}" + SMTP_HOST: ${SMTP_HOST} + SMTP_PORT: ${SMTP_PORT} + SMTP_USER: ${SMTP_USER} + SMTP_PASS: ${SMTP_PASS} + SMTP_FROM: ${SMTP_FROM} + SMTP_SSL: ${SMTP_SSL:-} + SMTP_STARTTLS: ${SMTP_STARTTLS:-} + AUTO_ADMIN_FIRST_USER: "${AUTO_ADMIN_FIRST_USER:-true}" + ADMIN_BOOTSTRAP_EMAILS: "${ADMIN_BOOTSTRAP_EMAILS:-}" + APP_URL: "${APP_URL:-https://kairo.jinkendo.de}" + ALLOWED_ORIGINS: "${ALLOWED_ORIGINS:-https://kairo.jinkendo.de}" + ENVIRONMENT: "${ENVIRONMENT:-production}" + CLUB_FEATURE_ENFORCE: "${CLUB_FEATURE_ENFORCE:-1}" + MEDIAWIKI_API_URL: "${MEDIAWIKI_API_URL:-https://karatetrainer.net/api.php}" + MEDIAWIKI_USER: "${MEDIAWIKI_USER:-}" + MEDIAWIKI_PASSWORD: "${MEDIAWIKI_PASSWORD:-}" + MEDIAWIKI_CATEGORY_EXERCISES: "${MEDIAWIKI_CATEGORY_EXERCISES:-Übungen}" + MEDIAWIKI_CATEGORY_SKILLS: "${MEDIAWIKI_CATEGORY_SKILLS:-Fähigkeitsbeschreibung}" + MEDIAWIKI_CATEGORY_METHODS: "${MEDIAWIKI_CATEGORY_METHODS:-Methodenbeschreibung}" + MEDIAWIKI_CATEGORY_MODELS: "${MEDIAWIKI_CATEGORY_MODELS:-Reifegradmodelle}" + MEDIA_ROOT: "${MEDIA_ROOT:-/app/media}" + volumes: + - ${KAIRO_MEDIA_HOST:-/kairo-media}:${MEDIA_ROOT:-/app/media} + ports: + - "8004:8000" + depends_on: + - postgres + restart: unless-stopped + networks: + - kairo-network + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: "" + container_name: kairo-ui + ports: + - "3004:80" + depends_on: + - backend + restart: unless-stopped + networks: + - kairo-network + +volumes: + kairo-db-data: + +networks: + kairo-network: + driver: bridge diff --git a/docs/DEPLOYMENT.md b/docs/DEPLOYMENT.md new file mode 100644 index 0000000..fb26212 --- /dev/null +++ b/docs/DEPLOYMENT.md @@ -0,0 +1,127 @@ +# Deployment – Kairo Jinkendo + +**Stand:** 2026-07-04 +**Server:** Raspberry Pi 5 (`192.168.2.49`) — gleicher Host wie Shinkan/Mitai +**Runner:** Gitea Actions (`/home/lars/gitea-runner/`) + +--- + +## Port- und Pfad-Übersicht + +| | Production | Development | +|---|------------|-------------| +| **Git-Branch** | `main` | `develop` | +| **Server-Verzeichnis** | `/home/lars/docker/kairo` | `/home/lars/docker/kairo-dev` | +| **Frontend-Port** | 3004 | 3097 | +| **Backend-Port** | 8004 | 8097 | +| **PostgreSQL (localhost)** | 5436 | 5437 | +| **Domain** | kairo.jinkendo.de | dev.kairo.jinkendo.de | +| **Medien-Host-Pfad** | `/kairo-media` | `/kairo-media/dev` | + +**Familien-Referenz (bereits belegt):** + +| App | Prod UI/API | Dev UI/API | +|-----|-------------|------------| +| Mitai | 3002 / 8002 | 3099 / 8099 | +| Shinkan | 3003 / 8003 | 3098 / 8098 | +| **Kairo** | **3004 / 8004** | **3097 / 8097** | + +--- + +## Einmalige Server-Einrichtung + +Auf dem Pi als User `lars` ausführen: + +```bash +# Verzeichnisse anlegen +mkdir -p /home/lars/docker/kairo +mkdir -p /home/lars/docker/kairo-dev +mkdir -p /kairo-media /kairo-media/dev + +# Production +cd /home/lars/docker/kairo +git clone http://192.168.2.144:3000/Lars/Kairo-Jinkendo.git . +git checkout main +cp .env.example .env +# .env bearbeiten (DB_PASSWORD, SMTP, OPENROUTER, …) + +# Development +cd /home/lars/docker/kairo-dev +git clone http://192.168.2.144:3000/Lars/Kairo-Jinkendo.git . +git checkout develop +cp .env.example .env +# .env mit Dev-Werten bearbeiten (siehe Kommentare in .env.example) +``` + +> **Hinweis:** Erstes `docker compose up` funktioniert erst, wenn `backend/` und `frontend/` mit Dockerfiles vorhanden sind. + +--- + +## Reverse Proxy (Synology / Fritz!Box) + +Analog zu Shinkan — neue Hostnamen im Proxy eintragen: + +| Hostname | Ziel (Pi) | +|----------|-----------| +| `kairo.jinkendo.de` | `http://192.168.2.49:3004` | +| `dev.kairo.jinkendo.de` | `http://192.168.2.49:3097` | + +SSL/TLS wie bei den Schwester-Apps (Let's Encrypt über Synology). + +--- + +## Gitea Actions + +Workflows unter `.gitea/workflows/`: + +| Workflow | Trigger | Aktion | +|----------|---------|--------| +| `deploy-dev.yml` | Push auf `develop` | Build + Deploy nach `kairo-dev/` | +| `deploy-prod.yml` | Push auf `main` | Build + Deploy nach `kairo/` | +| `test.yml` | PR/Push `develop`, nach Deploy | pytest, Lint, Frontend-Build, k6, Playwright | + +Health-Checks nach Deploy: + +- Dev: `curl http://localhost:8097/api/version` und `http://localhost:3097/api/version` +- Prod: `curl http://localhost:8004/api/version` und `http://localhost:3004/api/version` + +--- + +## Gitea Secrets (für E2E-Tests) + +In Gitea unter Repository → Settings → Actions → Secrets (analog Shinkan): + +| Secret | Verwendung | +|--------|------------| +| `E2E_DEV_TEST_EMAIL` | Playwright Dev-Login | +| `E2E_DEV_TEST_PASSWORD` | Playwright Dev-Login | +| `E2E_PROD_TEST_EMAIL` | Playwright Prod-Login | +| `E2E_PROD_TEST_PASSWORD` | Playwright Prod-Login | + +--- + +## Manuelles Deploy + +```bash +# Development +cd /home/lars/docker/kairo-dev +git fetch origin develop && git reset --hard origin/develop +docker compose -f docker-compose.dev-env.yml build --no-cache +docker compose -f docker-compose.dev-env.yml up -d + +# Production +cd /home/lars/docker/kairo +git fetch origin main && git reset --hard origin/main +docker compose build --no-cache +docker compose up -d +``` + +--- + +## Nächster Schritt (App-Code) + +1. `backend/` mit FastAPI-Skeleton (main.py, Dockerfile, migrations/) +2. `frontend/` mit React/Vite-Skeleton (Dockerfile, nginx.conf) +3. Erster Push auf `develop` → Auto-Deploy Dev +4. Reverse-Proxy-Einträge für Domains aktivieren +5. Gitea E2E-Secrets setzen diff --git a/frontend/README.md b/frontend/README.md new file mode 100644 index 0000000..c6eaf61 --- /dev/null +++ b/frontend/README.md @@ -0,0 +1,13 @@ +# Frontend + +React + Vite SPA — noch anzulegen. + +Erwartete Struktur (analog shinkan-jinkendo): + +``` +frontend/ +├── Dockerfile +├── nginx.conf +├── package.json +└── src/ +``` diff --git a/package.json b/package.json new file mode 100644 index 0000000..b8b5d7d --- /dev/null +++ b/package.json @@ -0,0 +1,11 @@ +{ + "name": "kairo-jinkendo", + "private": true, + "description": "Workspace-Metadaten für E2E (Playwright). Frontend: frontend/", + "scripts": { + "test:e2e": "playwright test" + }, + "devDependencies": { + "@playwright/test": "^1.49.0" + } +} diff --git a/playwright.config.js b/playwright.config.js new file mode 100644 index 0000000..1cdbc40 --- /dev/null +++ b/playwright.config.js @@ -0,0 +1,19 @@ +// CI: PLAYWRIGHT_BASE_URL = öffentliche Dev-/Prod-URL (HTTPS über Reverse Proxy), nicht localhost +// Lokal: export PLAYWRIGHT_BASE_URL=http://192.168.x.x:3097 + +const rawBase = + process.env.PLAYWRIGHT_BASE_URL || + process.env.BASE_URL || + 'http://127.0.0.1:3097'; + +module.exports = { + testDir: './tests', + timeout: 30000, + use: { + headless: true, + viewport: { width: 390, height: 844 }, + screenshot: 'only-on-failure', + baseURL: rawBase.replace(/\/$/, ''), + }, + reporter: 'list', +}; diff --git a/scripts/load/README.md b/scripts/load/README.md new file mode 100644 index 0000000..1394019 --- /dev/null +++ b/scripts/load/README.md @@ -0,0 +1,16 @@ +# k6 Load-Tests (Phase 0) + +Parallele `/health`-Requests gegen Dev oder Prod. + +## Lokal / CI + +```powershell +$env:BASE_URL="https://dev.kairo.jinkendo.de" +k6 run scripts/load/k6-health-baseline.js +``` + +```bash +BASE_URL=https://dev.kairo.jinkendo.de k6 run scripts/load/k6-health-baseline.js +``` + +Im Gitea-Workflow `test.yml` (Job `k6-health-baseline`) wird k6 automatisch installiert und ausgeführt. diff --git a/scripts/load/k6-health-baseline.js b/scripts/load/k6-health-baseline.js new file mode 100644 index 0000000..9324153 --- /dev/null +++ b/scripts/load/k6-health-baseline.js @@ -0,0 +1,32 @@ +/** + * Phase-0-Baseline: parallele GET /health (kein Auth). + * BASE_URL optional, z. B. https://dev.kairo.jinkendo.de + */ +import http from 'k6/http' +import { check } from 'k6' + +export const options = { + scenarios: { + health: { + executor: 'constant-vus', + vus: 10, + duration: '30s', + gracefulStop: '5s', + tags: { scenario: 'health' }, + exec: 'health', + }, + }, + thresholds: { + http_req_failed: ['rate<0.05'], + 'http_req_duration{scenario:health}': ['p(95)<3000'], + }, +} + +const BASE = (__ENV.BASE_URL || 'https://dev.kairo.jinkendo.de').replace(/\/$/, '') + +export function health() { + const res = http.get(`${BASE}/health`, { tags: { scenario: 'health' } }) + check(res, { + 'health 2xx': (r) => r.status >= 200 && r.status < 300, + }) +}