Infrastruktur-Grundgeruest fuer Kairo Jinkendo: Docker, Gitea Actions, Deployment-Doku (ohne App-Code).
Some checks failed
Test Suite / lint-backend (push) Waiting to run
Test Suite / build-frontend (push) Waiting to run
Test Suite / k6 /health Baseline (push) Waiting to run
Test Suite / playwright-tests (push) Waiting to run
Deploy Development / deploy (push) Failing after 1s
Test Suite / pytest-backend (push) Has been cancelled

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Lars 2026-07-04 18:15:24 +02:00
commit 848452f837
16 changed files with 1024 additions and 0 deletions

70
.env.example Normal file
View File

@ -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

View File

@ -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 ==="

View File

@ -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 ==="

312
.gitea/workflows/test.yml Normal file
View File

@ -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

76
.gitignore vendored Normal file
View File

@ -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

59
CLAUDE.md Normal file
View File

@ -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)
```

63
README.md Normal file
View File

@ -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

14
backend/README.md Normal file
View File

@ -0,0 +1,14 @@
# Backend
FastAPI-Anwendung — noch anzulegen.
Erwartete Struktur (analog shinkan-jinkendo):
```
backend/
├── Dockerfile
├── main.py
├── requirements.txt
├── migrations/
└── routers/
```

View File

@ -0,0 +1,79 @@
# Keine festen container_name — Compose-Namen haben Projektprefix (<projekt>-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

82
docker-compose.yml Normal file
View File

@ -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

127
docs/DEPLOYMENT.md Normal file
View File

@ -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

13
frontend/README.md Normal file
View File

@ -0,0 +1,13 @@
# Frontend
React + Vite SPA — noch anzulegen.
Erwartete Struktur (analog shinkan-jinkendo):
```
frontend/
├── Dockerfile
├── nginx.conf
├── package.json
└── src/
```

11
package.json Normal file
View File

@ -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"
}
}

19
playwright.config.js Normal file
View File

@ -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',
};

16
scripts/load/README.md Normal file
View File

@ -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.

View File

@ -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,
})
}