Initial commit: breakpilot-core - Shared Infrastructure

Docker Compose with 24+ services:
- PostgreSQL (PostGIS), Valkey, MinIO, Qdrant
- Vault (PKI/TLS), Nginx (Reverse Proxy)
- Backend Core API, Consent Service, Billing Service
- RAG Service, Embedding Service
- Gitea, Woodpecker CI/CD
- Night Scheduler, Health Aggregator
- Jitsi (Web/XMPP/JVB/Jicofo), Mailpit

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:13 +01:00
commit ad111d5e69
244 changed files with 84288 additions and 0 deletions

13
scripts/Dockerfile.health Normal file
View File

@@ -0,0 +1,13 @@
FROM python:3.11-slim
WORKDIR /app
RUN pip install --no-cache-dir fastapi uvicorn httpx
COPY health_aggregator.py .
ENV PORT=8099
EXPOSE ${PORT}
CMD ["sh", "-c", "uvicorn health_aggregator:app --host 0.0.0.0 --port ${PORT}"]

91
scripts/health-check.sh Normal file
View File

@@ -0,0 +1,91 @@
#!/bin/bash
# =========================================================
# BreakPilot — Health Check for All Projects
# =========================================================
GREEN='\033[0;32m'
RED='\033[0;31m'
YELLOW='\033[1;33m'
NC='\033[0m'
check_service() {
local name=$1
local url=$2
local timeout=${3:-5}
if curl -sf --max-time $timeout "$url" > /dev/null 2>&1; then
echo -e " ${GREEN}${NC} $name"
return 0
else
echo -e " ${RED}${NC} $name ($url)"
return 1
fi
}
echo "========================================="
echo " BreakPilot Health Check"
echo "========================================="
TOTAL=0
OK=0
echo ""
echo "CORE Infrastructure:"
for svc in \
"Health Aggregator|http://127.0.0.1:8099/health" \
"PostgreSQL|http://127.0.0.1:8099/health" \
"Backend Core|http://127.0.0.1:8000/health|10" \
"Embedding Service|http://127.0.0.1:8087/health|10" \
"RAG Service|http://127.0.0.1:8097/health|10" \
"Consent Service|http://127.0.0.1:8081/health|5" \
"Gitea|http://127.0.0.1:3003/api/healthz" \
"Mailpit|http://127.0.0.1:8025/" \
; do
IFS='|' read -r name url timeout <<< "$svc"
TOTAL=$((TOTAL + 1))
if check_service "$name" "$url" "$timeout"; then
OK=$((OK + 1))
fi
done
echo ""
echo "LEHRER Platform:"
for svc in \
"Studio v2|https://127.0.0.1/|5" \
"Admin Lehrer|https://127.0.0.1:3002/|5" \
"Backend Lehrer|https://127.0.0.1:8001/health|10" \
"Klausur Service|https://127.0.0.1:8086/health|10" \
"Voice Service|https://127.0.0.1:8091/health|5" \
"Website|https://127.0.0.1:3000/|5" \
; do
IFS='|' read -r name url timeout <<< "$svc"
TOTAL=$((TOTAL + 1))
if check_service "$name" "$url" "$timeout"; then
OK=$((OK + 1))
fi
done
echo ""
echo "COMPLIANCE Platform:"
for svc in \
"Admin Compliance|https://127.0.0.1:3007/|5" \
"Backend Compliance|https://127.0.0.1:8002/health|10" \
"AI Compliance SDK|https://127.0.0.1:8093/health|10" \
"Developer Portal|https://127.0.0.1:3006/|5" \
; do
IFS='|' read -r name url timeout <<< "$svc"
TOTAL=$((TOTAL + 1))
if check_service "$name" "$url" "$timeout"; then
OK=$((OK + 1))
fi
done
echo ""
echo "========================================="
echo -e " Result: ${OK}/${TOTAL} services healthy"
if [ $OK -eq $TOTAL ]; then
echo -e " ${GREEN}All services are up!${NC}"
else
echo -e " ${RED}$((TOTAL - OK)) services are down${NC}"
fi
echo "========================================="

View File

@@ -0,0 +1,169 @@
"""
BreakPilot Health Aggregator Service
Checks TCP connectivity to all configured services and exposes
aggregate health status via a FastAPI HTTP interface.
Configuration via environment variables:
CHECK_SERVICES - comma-separated "host:port" pairs
e.g. "postgres:5432,redis:6379,api:3000"
CHECK_TIMEOUT - per-service TCP timeout in seconds (default: 3)
CACHE_TTL - seconds to cache results (default: 10)
"""
from __future__ import annotations
import asyncio
import os
import time
from typing import Any
from fastapi import FastAPI
from fastapi.responses import JSONResponse
app = FastAPI(title="BreakPilot Health Aggregator")
# ---------------------------------------------------------------------------
# Configuration
# ---------------------------------------------------------------------------
CHECK_TIMEOUT: float = float(os.environ.get("CHECK_TIMEOUT", "3"))
CACHE_TTL: float = float(os.environ.get("CACHE_TTL", "10"))
def _parse_services() -> list[dict[str, Any]]:
"""Parse CHECK_SERVICES env var into a list of {host, port} dicts."""
raw = os.environ.get("CHECK_SERVICES", "")
services: list[dict[str, Any]] = []
for entry in raw.split(","):
entry = entry.strip()
if not entry:
continue
if ":" not in entry:
continue
host, port_str = entry.rsplit(":", 1)
try:
port = int(port_str)
except ValueError:
continue
services.append({"host": host, "port": port})
return services
# ---------------------------------------------------------------------------
# In-memory cache
# ---------------------------------------------------------------------------
_cache: dict[str, Any] = {
"timestamp": 0.0,
"results": [],
}
# ---------------------------------------------------------------------------
# TCP health check
# ---------------------------------------------------------------------------
async def _check_service(host: str, port: int) -> dict[str, Any]:
"""Attempt a TCP connection to host:port and return status + timing."""
start = time.monotonic()
try:
_, writer = await asyncio.wait_for(
asyncio.open_connection(host, port),
timeout=CHECK_TIMEOUT,
)
elapsed_ms = round((time.monotonic() - start) * 1000, 2)
writer.close()
await writer.wait_closed()
return {
"service": f"{host}:{port}",
"status": "up",
"response_time_ms": elapsed_ms,
}
except (OSError, asyncio.TimeoutError) as exc:
elapsed_ms = round((time.monotonic() - start) * 1000, 2)
return {
"service": f"{host}:{port}",
"status": "down",
"response_time_ms": elapsed_ms,
"error": str(exc) or type(exc).__name__,
}
async def _check_all() -> list[dict[str, Any]]:
"""Check every configured service concurrently, with caching."""
now = time.monotonic()
if _cache["results"] and (now - _cache["timestamp"]) < CACHE_TTL:
return _cache["results"]
services = _parse_services()
if not services:
return []
tasks = [_check_service(s["host"], s["port"]) for s in services]
results = await asyncio.gather(*tasks)
_cache["timestamp"] = time.monotonic()
_cache["results"] = list(results)
return _cache["results"]
# ---------------------------------------------------------------------------
# Routes
# ---------------------------------------------------------------------------
@app.get("/health")
async def health():
"""Aggregate health endpoint.
Returns 200 when all services are reachable, 503 otherwise.
"""
results = await _check_all()
all_up = all(r["status"] == "up" for r in results)
total = len(results)
healthy = sum(1 for r in results if r["status"] == "up")
body = {
"status": "healthy" if all_up else "degraded",
"services_total": total,
"services_healthy": healthy,
}
if not results:
body["status"] = "no_services_configured"
return JSONResponse(content=body, status_code=200)
status_code = 200 if all_up else 503
return JSONResponse(content=body, status_code=status_code)
@app.get("/health/details")
async def health_details():
"""Detailed per-service health information.
Returns 200 when all services are reachable, 503 otherwise.
"""
results = await _check_all()
all_up = all(r["status"] == "up" for r in results)
total = len(results)
healthy = sum(1 for r in results if r["status"] == "up")
body = {
"status": "healthy" if all_up else "degraded",
"services_total": total,
"services_healthy": healthy,
"services": results,
}
if not results:
body["status"] = "no_services_configured"
return JSONResponse(content=body, status_code=200)
status_code = 200 if all_up else 503
return JSONResponse(content=body, status_code=status_code)

135
scripts/init-schemas.sql Normal file
View File

@@ -0,0 +1,135 @@
-- BreakPilot Schema-Separation
-- Erstellt 3 getrennte Schemas für Core, Lehrer und Compliance
-- Wird beim ersten Start von postgres ausgeführt
-- Schemas erstellen
CREATE SCHEMA IF NOT EXISTS core;
CREATE SCHEMA IF NOT EXISTS lehrer;
CREATE SCHEMA IF NOT EXISTS compliance;
-- Berechtigungen für breakpilot User
GRANT ALL ON SCHEMA core TO breakpilot;
GRANT ALL ON SCHEMA lehrer TO breakpilot;
GRANT ALL ON SCHEMA compliance TO breakpilot;
-- Default search_path für den breakpilot User
ALTER ROLE breakpilot SET search_path TO public, core, lehrer, compliance;
-- =============================================
-- TABELLEN-MIGRATION: public -> core Schema
-- =============================================
-- Hinweis: Wird nur ausgeführt wenn Tabellen in public existieren
-- Bei Neuinstallation werden Tabellen direkt im richtigen Schema erstellt
DO $$
BEGIN
-- Core-Tabellen verschieben (falls vorhanden)
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'users') THEN
ALTER TABLE public.users SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sessions') THEN
ALTER TABLE public.sessions SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'consent_records') THEN
ALTER TABLE public.consent_records SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'notifications') THEN
ALTER TABLE public.notifications SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'email_templates') THEN
ALTER TABLE public.email_templates SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'billing_customers') THEN
ALTER TABLE public.billing_customers SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'billing_subscriptions') THEN
ALTER TABLE public.billing_subscriptions SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'billing_invoices') THEN
ALTER TABLE public.billing_invoices SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'rbac_roles') THEN
ALTER TABLE public.rbac_roles SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'rbac_permissions') THEN
ALTER TABLE public.rbac_permissions SET SCHEMA core;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'rbac_role_permissions') THEN
ALTER TABLE public.rbac_role_permissions SET SCHEMA core;
END IF;
-- Lehrer-Tabellen verschieben (falls vorhanden)
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'classrooms') THEN
ALTER TABLE public.classrooms SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'learning_units') THEN
ALTER TABLE public.learning_units SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'klausuren') THEN
ALTER TABLE public.klausuren SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'corrections') THEN
ALTER TABLE public.corrections SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'worksheets') THEN
ALTER TABLE public.worksheets SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'certificates') THEN
ALTER TABLE public.certificates SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'letters') THEN
ALTER TABLE public.letters SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'meetings') THEN
ALTER TABLE public.meetings SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'messenger_contacts') THEN
ALTER TABLE public.messenger_contacts SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'messenger_conversations') THEN
ALTER TABLE public.messenger_conversations SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'messenger_messages') THEN
ALTER TABLE public.messenger_messages SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'vocab_sessions') THEN
ALTER TABLE public.vocab_sessions SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'game_sessions') THEN
ALTER TABLE public.game_sessions SET SCHEMA lehrer;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'game_scores') THEN
ALTER TABLE public.game_scores SET SCHEMA lehrer;
END IF;
-- Compliance-Tabellen verschieben (falls vorhanden)
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'compliance_risks') THEN
ALTER TABLE public.compliance_risks SET SCHEMA compliance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'compliance_controls') THEN
ALTER TABLE public.compliance_controls SET SCHEMA compliance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'compliance_requirements') THEN
ALTER TABLE public.compliance_requirements SET SCHEMA compliance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'compliance_evidence') THEN
ALTER TABLE public.compliance_evidence SET SCHEMA compliance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'dsr_requests') THEN
ALTER TABLE public.dsr_requests SET SCHEMA compliance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sdk_tenants') THEN
ALTER TABLE public.sdk_tenants SET SCHEMA compliance;
END IF;
IF EXISTS (SELECT 1 FROM information_schema.tables WHERE table_schema = 'public' AND table_name = 'sdk_audit_logs') THEN
ALTER TABLE public.sdk_audit_logs SET SCHEMA compliance;
END IF;
RAISE NOTICE 'Schema migration complete.';
END $$;
-- Cross-Schema Views für häufige Lookups
CREATE OR REPLACE VIEW compliance.v_users AS SELECT * FROM core.users;
CREATE OR REPLACE VIEW lehrer.v_users AS SELECT * FROM core.users;
CREATE OR REPLACE VIEW lehrer.v_consent_records AS SELECT * FROM core.consent_records;
CREATE OR REPLACE VIEW compliance.v_consent_records AS SELECT * FROM core.consent_records;

84
scripts/start-all.sh Normal file
View File

@@ -0,0 +1,84 @@
#!/bin/bash
# =========================================================
# BreakPilot — Start All Projects
# =========================================================
# Usage: ./start-all.sh [--core-only] [--no-compliance]
# =========================================================
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_ROOT="$(dirname "$SCRIPT_DIR")"
LEHRER_ROOT="$(dirname "$PROJECT_ROOT")/breakpilot-lehrer"
COMPLIANCE_ROOT="$(dirname "$PROJECT_ROOT")/breakpilot-compliance"
DOCKER="/usr/local/bin/docker"
# Colors
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
RED='\033[0;31m'
NC='\033[0m'
echo -e "${GREEN}=========================================${NC}"
echo -e "${GREEN} BreakPilot — Starting All Projects${NC}"
echo -e "${GREEN}=========================================${NC}"
# Phase 1: Core
echo -e "\n${YELLOW}[1/3] Starting Core Infrastructure...${NC}"
$DOCKER compose -f "$PROJECT_ROOT/docker-compose.yml" up -d
echo -e "${GREEN}Core started. Waiting for health check...${NC}"
# Wait for health aggregator
MAX_WAIT=120
WAITED=0
until curl -sf http://127.0.0.1:8099/health > /dev/null 2>&1; do
sleep 5
WAITED=$((WAITED + 5))
if [ $WAITED -ge $MAX_WAIT ]; then
echo -e "${RED}Core health check timeout after ${MAX_WAIT}s${NC}"
echo "Check: $DOCKER compose -f $PROJECT_ROOT/docker-compose.yml logs"
exit 1
fi
echo " Waiting for core... (${WAITED}s/${MAX_WAIT}s)"
done
echo -e "${GREEN}Core is healthy!${NC}"
if [ "$1" = "--core-only" ]; then
echo -e "\n${GREEN}Done (core-only mode).${NC}"
exit 0
fi
# Phase 2: Lehrer
if [ -f "$LEHRER_ROOT/docker-compose.yml" ]; then
echo -e "\n${YELLOW}[2/3] Starting Lehrer Platform...${NC}"
$DOCKER compose -f "$LEHRER_ROOT/docker-compose.yml" up -d
echo -e "${GREEN}Lehrer platform started.${NC}"
else
echo -e "\n${YELLOW}[2/3] Skipping Lehrer (not found: $LEHRER_ROOT)${NC}"
fi
# Phase 3: Compliance
if [ "$1" = "--no-compliance" ]; then
echo -e "\n${YELLOW}[3/3] Skipping Compliance (--no-compliance flag)${NC}"
elif [ -f "$COMPLIANCE_ROOT/docker-compose.yml" ]; then
echo -e "\n${YELLOW}[3/3] Starting Compliance Platform...${NC}"
$DOCKER compose -f "$COMPLIANCE_ROOT/docker-compose.yml" up -d
echo -e "${GREEN}Compliance platform started.${NC}"
else
echo -e "\n${YELLOW}[3/3] Skipping Compliance (not found: $COMPLIANCE_ROOT)${NC}"
fi
echo -e "\n${GREEN}=========================================${NC}"
echo -e "${GREEN} All Projects Started!${NC}"
echo -e "${GREEN}=========================================${NC}"
echo ""
echo "URLs:"
echo " Core Health: http://macmini:8099/health"
echo " Studio v2: https://macmini/"
echo " Admin Lehrer: https://macmini:3002/"
echo " Admin Compliance: https://macmini:3007/"
echo " Backend Core: https://macmini:8000/"
echo " Backend Lehrer: https://macmini:8001/"
echo " Backend Compliance:https://macmini:8002/"
echo " RAG Service: https://macmini:8097/"