feat: BreakPilot PWA - Full codebase (clean push without large binaries)
Some checks failed
Tests / Go Tests (push) Has been cancelled
Tests / Python Tests (push) Has been cancelled
Tests / Integration Tests (push) Has been cancelled
Tests / Go Lint (push) Has been cancelled
Tests / Python Lint (push) Has been cancelled
Tests / Security Scan (push) Has been cancelled
Tests / All Checks Passed (push) Has been cancelled
Security Scanning / Secret Scanning (push) Has been cancelled
Security Scanning / Dependency Vulnerability Scan (push) Has been cancelled
Security Scanning / Go Security Scan (push) Has been cancelled
Security Scanning / Python Security Scan (push) Has been cancelled
Security Scanning / Node.js Security Scan (push) Has been cancelled
Security Scanning / Docker Image Security (push) Has been cancelled
Security Scanning / Security Summary (push) Has been cancelled
CI/CD Pipeline / Go Tests (push) Has been cancelled
CI/CD Pipeline / Python Tests (push) Has been cancelled
CI/CD Pipeline / Website Tests (push) Has been cancelled
CI/CD Pipeline / Linting (push) Has been cancelled
CI/CD Pipeline / Security Scan (push) Has been cancelled
CI/CD Pipeline / Docker Build & Push (push) Has been cancelled
CI/CD Pipeline / Integration Tests (push) Has been cancelled
CI/CD Pipeline / Deploy to Staging (push) Has been cancelled
CI/CD Pipeline / Deploy to Production (push) Has been cancelled
CI/CD Pipeline / CI Summary (push) Has been cancelled
ci/woodpecker/manual/build-ci-image Pipeline was successful
ci/woodpecker/manual/main Pipeline failed

All services: admin-v2, studio-v2, website, ai-compliance-sdk,
consent-service, klausur-service, voice-service, and infrastructure.
Large PDFs and compiled binaries excluded via .gitignore.
This commit is contained in:
BreakPilot Dev
2026-02-11 13:25:58 +01:00
commit 19855efacc
2512 changed files with 933814 additions and 0 deletions

425
.claude/CLAUDE.md Normal file
View File

@@ -0,0 +1,425 @@
# BreakPilot PWA - Projekt-Kontext für Claude
## Entwicklungsumgebung (WICHTIG - IMMER ZUERST LESEN)
### Zwei-Rechner-Setup
| Gerät | Rolle | Aufgaben |
|-------|-------|----------|
| **MacBook** | Client | Claude Terminal, Browser (Frontend-Tests) |
| **Mac Mini** | Server | Docker, alle Services, Code-Ausführung, Tests, Git |
**WICHTIG:** Die Entwicklung findet vollständig auf dem **Mac Mini** statt!
- Alle Befehle (docker, git, tests, builds) per SSH auf dem Mac Mini ausführen
- Das MacBook dient nur als Terminal und Browser für Frontend-Tests
- Dateien werden auf dem Mac Mini bearbeitet, nicht lokal auf dem MacBook
### SSH-Verbindung
```bash
# Verbindung zum Mac Mini im lokalen Netzwerk
ssh macmini
# Projektverzeichnis auf Mac Mini
cd /Users/benjaminadmin/Projekte/breakpilot-pwa
# Oder direkt (BEVORZUGT für einzelne Befehle):
ssh macmini "<befehl>"
```
**Hostname:** `macmini` (im lokalen Netzwerk via Bonjour)
**User:** `benjaminadmin`
**Projekt:** `/Users/benjaminadmin/Projekte/breakpilot-pwa`
### Beispiele für korrekte Befehlsausführung
```bash
# ✅ RICHTIG: Befehle auf Mac Mini ausführen
ssh macmini "docker compose ps"
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa && git status"
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa/backend && source venv/bin/activate && pytest -v"
# ❌ FALSCH: Lokale Befehle auf MacBook (Docker/Services laufen dort nicht!)
docker compose ps
pytest -v
```
### Browser-Tests (auf MacBook)
Frontend im Browser testen via:
- https://macmini/ (Studio)
- https://macmini:3002/ (Admin)
- https://macmini:3000/ (Website)
---
## Kernprinzipien (IMMER BEACHTEN)
### 1. Open Source Policy
- **NUR Open Source mit kommerziell nutzbarer Lizenz verwenden**
- Erlaubte Lizenzen: MIT, Apache-2.0, BSD-2-Clause, BSD-3-Clause, ISC, MPL-2.0, LGPL
- **VERBOTEN:** GPL (außer LGPL), AGPL, proprietäre Software, "free for non-commercial use"
- **Bei jeder neuen Dependency:** SBOM aktualisieren unter https://macmini:3002/infrastructure/sbom
### 2. Testing & Dokumentation
- **Tests sind Pflicht:** Unit Tests, Integration Tests für jede Änderung
- **Dokumentation aktualisieren:** https://macmini:3002/development/docs (MKDocs)
- Siehe `@.claude/rules/testing.md` und `@.claude/rules/documentation.md`
### 3. Architektur & Visualisierung aktualisieren
Nach größeren Änderungen diese Dashboards aktualisieren:
- **Architektur:** https://macmini:3002/architecture
- **Screenflows:** https://macmini:3002/development/screen-flow
- **Dashboard:** https://macmini:3002/dashboard
- **Security Tools:** https://macmini:3002/infrastructure/security
### 4. CI/CD Pipeline
Alle Security-Tools müssen nach der Pipeline durchlaufen:
- Trivy (Container-Scanning)
- Semgrep (SAST)
- Gitleaks (Secret-Detection)
- SBOM-Generierung
---
## Projektübersicht
**Projektname:** BreakPilot PWA
**Typ:** DSGVO-konforme EdTech-Plattform für den DACH-Raum
**Architektur:** Microservices mit Docker Compose
**Plattform:** Mac Mini M2 (Apple Silicon / ARM64)
---
## Haupt-URLs (HTTPS via Nginx)
| URL | Service | Beschreibung |
|-----|---------|--------------|
| https://macmini/ | Studio v2 | Lehrer-/Schüler-Interface |
| https://macmini:3000/ | Website | Öffentliche Website |
| https://macmini:3002/ | Admin v2 | **Admin-Dashboard (Hauptzugang)** |
| https://macmini:8000/ | Backend API | FastAPI Backend |
| https://macmini:8086/ | Klausur Service | Prüfungs-/Klausurservice |
| https://macmini:8443/ | Jitsi Meet | Videokonferenzen |
| wss://macmini:8091/ | Voice Service | Spracheingabe WebSocket |
| https://macmini:3002/infrastructure/night-mode | Night Mode | Nachtabschaltung UI |
### AI Compliance SDK (DSGVO-Tools)
| URL | Modul | Beschreibung |
|-----|-------|--------------|
| https://macmini:3002/sdk | SDK Admin | Haupt-SDK mit allen Modulen |
| https://macmini:3002/sdk/tom | TOM | Technisch-Organisatorische Maßnahmen |
| https://macmini:3002/sdk/dsfa | DSFA | Datenschutz-Folgenabschätzung |
| https://macmini:3002/sdk/vvt | VVT | Verzeichnis von Verarbeitungstätigkeiten |
| https://macmini:3002/sdk/loeschfristen | Löschfristen | Löschfristen-Verwaltung |
| https://macmini:3002/developers | Developer Portal | API-Dokumentation für Kunden |
| https://macmini:8093/ | SDK API | Backend-API für SDK |
### Interne Dienste
| URL | Service |
|-----|---------|
| http://macmini:8200/ | Vault UI (Secrets) |
| http://macmini:8025/ | Mailpit (E-Mail Dev) |
| http://macmini:9001/ | MinIO Console (S3) |
| http://macmini:3003/ | Gitea (Git-Server) |
| http://macmini:8090/ | Woodpecker CI |
| http://macmini:8089/ | Camunda (BPMN) |
| http://macmini:8096/ | Night Scheduler API |
| http://macmini:8009/ | MkDocs (Projekt-Doku) |
### AI Tools (Admin v2)
| URL | Tool | Beschreibung |
|-----|------|--------------|
| https://macmini:3002/ai/llm-compare | LLM Vergleich | KI-Provider vergleichen |
| https://macmini:3002/ai/ocr-compare | OCR Vergleich | OCR-Methoden & Vokabel-Extraktion |
| https://macmini:3002/ai/ocr-labeling | OCR Labeling | Trainingsdaten erstellen |
| https://macmini:3002/ai/test-quality | Test Quality (BQAS) | Golden Suite & Tests |
| https://macmini:3002/ai/gpu | GPU Infrastruktur | vast.ai Management |
| https://macmini:3002/ai/rag-pipeline | RAG Pipeline | Retrieval Augmented Generation |
| https://macmini:3002/ai/magic-help | Magic Help | KI-Assistent |
### Lehrer-Tools (Studio v2)
| URL | Tool | Beschreibung |
|-----|------|--------------|
| https://macmini/vocab-worksheet | Vokabel-Arbeitsblatt | OCR-Scan & Arbeitsblatt-Generator |
| https://macmini/korrektur | Korrekturplattform | Abiturklausur-Korrektur |
| https://macmini:8000/app | Dashboard (alt) | Altes Dashboard |
---
## Services (49 Container)
### Kern-Applikationen
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `studio-v2` | Next.js | 443 | Lehrer-/Schüler-Studio |
| `admin-v2` | Next.js | 3002 | Admin-Dashboard |
| `website` | Next.js | 3000 | Öffentliche Website |
| `backend` | Python/FastAPI | 8000 | API Backend |
| `consent-service` | Go/Gin | 8081 | Consent-Management |
### Bildungs-Services
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `klausur-service` | Python/FastAPI | 8086 | Prüfungen, OCR, RAG |
| `school-service` | Python | 8082 | Schulverwaltung |
| `edu-search-service` | Python | 8088 | Bildungssuche |
| `breakpilot-drive` | Node.js | 8087 | Dateiablage (IPFS) |
| `geo-service` | Python | 8084 | Geo-Daten (PostGIS) |
| `voice-service` | Python | 8091 | Spracheingabe |
### KI & Compliance
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `ai-compliance-sdk` | Python | 8093 | DSGVO-konforme KI-Nutzung |
| `embedding-service` | Python | 8083 | Text-Embeddings |
| `paddleocr-service` | Python | - | OCR für Dokumente |
| `transcription-worker` | Python | - | Audio-Transkription |
### Kommunikation
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `jitsi-web` | Jitsi | 8443 | Videokonferenzen |
| `jitsi-xmpp` | Prosody | - | XMPP Server |
| `jitsi-jicofo` | Jicofo | - | Konferenz-Fokus |
| `jitsi-jvb` | JVB | 8080 | Video Bridge |
| `jibri` | Jibri | - | Aufnahme/Streaming |
| `synapse` | Matrix | 8008 | Chat-Server |
### Datenbanken & Storage
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `postgres` | PostGIS 16 | 5432 | Hauptdatenbank |
| `valkey` | Valkey 8 | 6379 | Session-Cache (Redis-Fork) |
| `qdrant` | Qdrant | 6333/6334 | Vektordatenbank |
| `minio` | MinIO | 9000/9001 | S3-kompatibler Storage |
### Infrastructure & DevOps
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `nginx` | Nginx | 80/443 | Reverse Proxy + TLS |
| `vault` | HashiCorp Vault | 8200 | Secrets Management |
| `vault-agent` | Vault | - | Zertifikatserneuerung |
| `gitea` | Gitea | 3003 | Git-Server |
| `woodpecker-server` | Woodpecker | 8090 | CI/CD Server |
| `woodpecker-agent` | Woodpecker | - | CI/CD Agent |
| `night-scheduler` | Python/FastAPI | 8096 | Auto-Shutdown/Startup |
| `mailpit` | Mailpit | 8025/1025 | E-Mail (Dev) |
### ERP & Billing
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `erpnext-frontend` | ERPNext | 8009 | ERP Frontend |
| `erpnext-backend` | ERPNext | - | ERP Backend |
| `erpnext-db` | MariaDB | - | ERP Datenbank |
| `billing-service` | Python | - | Abrechnungsservice |
### DSMS (Data Sharing)
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `dsms-node` | Node.js | 4001/5001 | IPFS Node |
| `dsms-gateway` | Node.js | 8085 | IPFS Gateway |
### Prozesse
| Service | Tech | Port | Beschreibung |
|---------|------|------|--------------|
| `camunda` | Camunda | 8089 | BPMN Engine |
---
## Tech-Stack nach Sprache
### Go
- `consent-service`: Gin, GORM, JWT
### Python
- `backend`: FastAPI, SQLAlchemy, Pydantic
- `klausur-service`: FastAPI, PaddleOCR, RAG
- `ai-compliance-sdk`: FastAPI, Langfuse
- `embedding-service`: FastAPI, Sentence-Transformers
- `voice-service`: FastAPI, Whisper
- `geo-service`: FastAPI, PostGIS
- `school-service`: FastAPI
- `night-scheduler`: FastAPI
### TypeScript/Next.js
- `studio-v2`: Next.js 15, React, TailwindCSS
- `admin-v2`: Next.js 15, React, TailwindCSS
- `website`: Next.js 14
### Node.js
- `breakpilot-drive`: Express, IPFS
- `dsms-node`: IPFS
- `dsms-gateway`: Express
---
## Verzeichnisstruktur
```
breakpilot-pwa/
├── .claude/ # Claude-Konfiguration
│ ├── CLAUDE.md # Diese Datei
│ ├── rules/ # Automatische Regeln
│ │ ├── testing.md
│ │ ├── documentation.md
│ │ ├── night-scheduler.md
│ │ ├── open-source-policy.md
│ │ ├── compliance-checklist.md
│ │ ├── abiturkorrektur.md
│ │ ├── vocab-worksheet.md
│ │ ├── multi-agent-architecture.md
│ │ └── experimental-dashboard.md
│ └── settings.json
├── admin-v2/ # Admin Dashboard (Next.js)
├── studio-v2/ # Lehrer-/Schüler-Studio (Next.js)
├── website/ # Öffentliche Website (Next.js)
├── backend/ # Python Backend (FastAPI)
├── consent-service/ # Go Consent Service
├── klausur-service/ # Klausur/OCR Service
├── ai-compliance-sdk/ # KI-Compliance SDK
├── voice-service/ # Spracheingabe
├── geo-service/ # Geo-Daten
├── school-service/ # Schulverwaltung
├── edu-search-service/ # Bildungssuche
├── breakpilot-drive/ # Dateiablage
├── night-scheduler/ # Auto-Shutdown
├── nginx/ # Reverse Proxy Config
├── vault/ # Vault Config
├── docs-src/ # MKDocs Quellen
├── docs-site/ # MKDocs Build
├── docker-compose.yml # Haupt-Docker-Config
└── mkdocs.yml # MKDocs Config
```
---
## Dokumentation (MKDocs)
**Live-URL:** https://macmini:3002/development/docs
**Quellen:** `/docs-src/`
**Build:** `/docs-site/`
**Config:** `mkdocs.yml`
### Dokumentation bearbeiten
```bash
# MKDocs lokal starten (Live-Reload)
cd /Users/benjaminadmin/Projekte/breakpilot-pwa
mkdocs serve -a 0.0.0.0:8008
# Build
mkdocs build
```
### Struktur
- `docs-src/index.md` - Startseite
- `docs-src/architecture/` - Architektur-Docs
- `docs-src/services/` - Service-Dokumentation
- `docs-src/api/` - API-Dokumentation
- `docs-src/development/` - Entwickler-Guides
---
## Häufige Befehle
### Docker (via SSH auf Mac Mini)
```bash
# Alle Services starten
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml up -d"
# Einzelnen Service neu bauen & starten
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml build --no-cache <service-name>"
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml up -d <service-name>"
# Logs anzeigen
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml logs -f <service-name>"
# Status aller Container
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml ps"
```
**WICHTIG:** Docker-Pfad auf Mac Mini ist `/usr/local/bin/docker` (nicht im Standard-PATH bei SSH).
### Tests (via SSH)
```bash
# Go Tests (Consent Service)
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa/consent-service && go test -v ./..."
# Python Tests
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa/backend && source venv/bin/activate && pytest -v"
```
### Git
```bash
# Remote ist localhost:3003 (Gitea laeuft als Container auf Mac Mini)
# Vom MacBook aus: http://macmini:3003/pilotadmin/breakpilot-pwa.git
# Vom Mac Mini aus: http://localhost:3003/pilotadmin/breakpilot-pwa.git
# Git-Befehle auf Mac Mini ausfuehren (ohne cd):
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-pwa status"
ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-pwa pull --no-rebase origin main"
```
---
## Rollen & Berechtigungen
| Rolle | Beschreibung |
|-------|--------------|
| `user` | Normaler Benutzer (Schüler/Lehrer) |
| `admin` | Administrator |
| `data_protection_officer` | Datenschutzbeauftragter |
| `school_admin` | Schuladministrator |
---
## Compliance & Sicherheit
### DSGVO
- Consent-Management via `consent-service`
- Datenexport-Funktionen
- Löschkonzept implementiert
### AI Act
- `ai-compliance-sdk` für konforme KI-Nutzung
- Risikobewertung für KI-Funktionen
- Audit-Logging
### BSI
- BSI-TR-03161 Dokumentation vorhanden
- Security-Scanning in CI/CD
---
## Sensitive Dateien
**NIEMALS ändern oder committen:**
- `.env`, `.env.local`, `.env.backup`
- `secrets/`
- Vault-Tokens
- SSL-Zertifikate
---
## Ansprechpartner
- **Git-Server:** http://macmini:3003 (Gitea)
- **CI/CD:** http://macmini:8090 (Woodpecker)
- **Issue-Tracker:** Gitea Issues

View File

@@ -0,0 +1,263 @@
# Plan: Embedding-Service Separation
## Ziel
Trennung der ML/Embedding-Komponenten vom klausur-service in einen eigenständigen `embedding-service`, um die Build-Zeit von ~20 Minuten auf ~30 Sekunden zu reduzieren.
## Aktuelle Situation
| Service | Build-Zeit | Image-Größe | Problem |
|---------|------------|-------------|---------|
| klausur-service | ~20 min | ~2.5 GB | PyTorch + sentence-transformers werden bei jedem Build installiert |
## Ziel-Architektur
```
┌─────────────────┐ HTTP ┌──────────────────┐
│ klausur-service │ ───────────→ │ embedding-service │
│ (FastAPI) │ │ (FastAPI) │
│ Port 8086 │ │ Port 8087 │
│ ~200 MB │ │ ~2.5 GB │
│ Build: 30s │ │ Build: 15 min │
└─────────────────┘ └──────────────────┘
│ │
└────────────┬───────────────────┘
┌───────────────┐
│ Qdrant │
│ Port 6333 │
└───────────────┘
```
## Phase 1: Neuen Embedding-Service erstellen
### 1.1 Verzeichnisstruktur anlegen
```
klausur-service/
├── embedding-service/ # NEU
│ ├── Dockerfile
│ ├── requirements.txt
│ ├── main.py # FastAPI App
│ ├── eh_pipeline.py # Kopie
│ ├── reranker.py # Kopie
│ ├── hyde.py # Kopie
│ ├── hybrid_search.py # Kopie
│ ├── pdf_extraction.py # Kopie
│ └── config.py # Embedding-Konfiguration
├── backend/ # Bestehend (wird angepasst)
└── frontend/ # Bestehend
```
### 1.2 Dateien in embedding-service erstellen
**requirements.txt** (ML-spezifisch):
```
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
torch>=2.0.0
sentence-transformers>=2.2.0
qdrant-client>=1.7.0
unstructured>=0.12.0
pypdf>=4.0.0
httpx>=0.26.0
pydantic>=2.0.0
python-dotenv>=1.0.0
```
**main.py** - API-Endpoints:
- `POST /embed` - Generiert Embeddings für Text/Liste von Texten
- `POST /embed-single` - Einzelnes Embedding
- `POST /rerank` - Re-Ranking von Suchergebnissen
- `POST /extract-pdf` - PDF-Text-Extraktion
- `GET /health` - Health-Check
- `GET /models` - Verfügbare Modelle
### 1.3 Dockerfile (embedding-service)
```dockerfile
FROM python:3.11-slim
WORKDIR /app
# PyTorch CPU-only für kleinere Images
RUN pip install --no-cache-dir torch --index-url https://download.pytorch.org/whl/cpu
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Modelle vorab laden (Layer-Cache)
RUN python -c "from sentence_transformers import SentenceTransformer; SentenceTransformer('BAAI/bge-m3')"
RUN python -c "from sentence_transformers import CrossEncoder; CrossEncoder('BAAI/bge-reranker-v2-m3')"
COPY . .
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8087"]
```
## Phase 2: Klausur-Service anpassen
### 2.1 ML-Dependencies aus requirements.txt entfernen
Entfernen:
- `torch`
- `sentence-transformers`
- `unstructured`
- `pypdf`
Behalten:
- `fastapi`, `uvicorn`, `httpx`
- `qdrant-client` (für Suche)
- `cryptography` (BYOEH)
- Alle Business-Logic-Dependencies
### 2.2 Embedding-Client erstellen
Neue Datei `backend/embedding_client.py`:
```python
class EmbeddingClient:
def __init__(self, base_url: str = "http://embedding-service:8087"):
self.base_url = base_url
async def generate_embeddings(self, texts: list[str]) -> list[list[float]]:
async with httpx.AsyncClient() as client:
response = await client.post(f"{self.base_url}/embed", json={"texts": texts})
return response.json()["embeddings"]
async def rerank(self, query: str, documents: list[str]) -> list[dict]:
async with httpx.AsyncClient() as client:
response = await client.post(f"{self.base_url}/rerank",
json={"query": query, "documents": documents})
return response.json()["results"]
```
### 2.3 Bestehende Aufrufe umleiten
Dateien anpassen:
- `backend/main.py`: `generate_single_embedding()``embedding_client.generate_embeddings()`
- `backend/admin_api.py`: Embedding-Aufrufe über Client
- `backend/qdrant_service.py`: Bleibt für Suche, Indexierung nutzt Client
## Phase 3: Docker-Compose Integration
### 3.1 docker-compose.dev.yml erweitern
```yaml
services:
klausur-service:
build:
context: ./klausur-service
dockerfile: Dockerfile
ports:
- "8086:8086"
environment:
- EMBEDDING_SERVICE_URL=http://embedding-service:8087
depends_on:
- embedding-service
- qdrant
embedding-service:
build:
context: ./klausur-service/embedding-service
dockerfile: Dockerfile
ports:
- "8087:8087"
environment:
- EMBEDDING_BACKEND=local
- LOCAL_EMBEDDING_MODEL=BAAI/bge-m3
- LOCAL_RERANKER_MODEL=BAAI/bge-reranker-v2-m3
volumes:
- embedding-models:/root/.cache/huggingface # Model-Cache
deploy:
resources:
limits:
memory: 4G
qdrant:
image: qdrant/qdrant:latest
ports:
- "6333:6333"
volumes:
- qdrant-data:/qdrant/storage
volumes:
embedding-models:
qdrant-data:
```
## Phase 4: Tests und Validierung
### 4.1 Unit Tests für Embedding-Service
- Test Embedding-Generierung
- Test Re-Ranking
- Test PDF-Extraktion
- Test Health-Endpoint
### 4.2 Integration Tests
- Test klausur-service → embedding-service Kommunikation
- Test RAG-Query End-to-End
- Test EH-Upload mit Embedding
### 4.3 Performance-Validierung
- Build-Zeit klausur-service messen (Ziel: <1 min)
- Embedding-Latenz messen (Ziel: <500ms für einzelnes Embedding)
- Re-Ranking-Latenz messen (Ziel: <1s für 10 Dokumente)
## Implementierungsreihenfolge
1. **embedding-service/main.py** - FastAPI App mit Endpoints
2. **embedding-service/config.py** - Konfiguration
3. **embedding-service/requirements.txt** - Dependencies
4. **embedding-service/Dockerfile** - Container-Build
5. **backend/embedding_client.py** - HTTP-Client
6. **backend/requirements.txt** - ML-Deps entfernen
7. **backend/main.py** - Aufrufe umleiten
8. **backend/admin_api.py** - Aufrufe umleiten
9. **docker-compose.dev.yml** - Service hinzufügen
10. **Tests** - Validierung
## Zu bewegende Dateien (Referenz)
| Datei | Zeilen | Aktion |
|-------|--------|--------|
| eh_pipeline.py | 777 | Kopieren → embedding-service |
| reranker.py | 253 | Kopieren → embedding-service |
| hyde.py | 209 | Kopieren → embedding-service |
| hybrid_search.py | 285 | Kopieren → embedding-service |
| pdf_extraction.py | 479 | Kopieren → embedding-service |
## Umgebungsvariablen
### embedding-service
```
EMBEDDING_BACKEND=local
LOCAL_EMBEDDING_MODEL=BAAI/bge-m3
LOCAL_RERANKER_MODEL=BAAI/bge-reranker-v2-m3
OPENAI_API_KEY= # Optional für OpenAI-Backend
PDF_EXTRACTION_BACKEND=auto
LOG_LEVEL=INFO
```
### klausur-service (neu)
```
EMBEDDING_SERVICE_URL=http://embedding-service:8087
```
## Risiken und Mitigationen
| Risiko | Mitigation |
|--------|------------|
| Netzwerk-Latenz zwischen Services | Model-Caching, Connection-Pooling |
| embedding-service nicht erreichbar | Health-Checks, Retry-Logik, Graceful Degradation |
| Inkonsistente Embedding-Modelle | Versionierung, Model-Hash-Prüfung |
| Erhöhter RAM-Bedarf (2 Container) | Memory-Limits in Docker, Model-Offloading |
## Erwartete Ergebnisse
| Metrik | Vorher | Nachher |
|--------|--------|---------|
| Build-Zeit klausur-service | ~20 min | ~30 sec |
| Build-Zeit embedding-service | - | ~15 min |
| Image-Größe klausur-service | ~2.5 GB | ~200 MB |
| Image-Größe embedding-service | - | ~2.5 GB |
| Entwickler-Iteration | Langsam | Schnell |
## Nicht vergessen (Task A)
Nach Abschluss der Service-Trennung:
- [ ] EH-Upload-Wizard mit Test-Klausur testen
- [ ] Security-Infobox im Wizard verifizieren
- [ ] End-to-End RAG-Query testen

View File

@@ -0,0 +1,614 @@
# Abiturkorrektur-System - Entwicklerdokumentation
**WICHTIG: Diese Datei wird bei jedem Compacting gelesen. Alle Implementierungsdetails hier dokumentieren!**
---
## 1. Projektziel
Entwicklung eines KI-gestützten Korrektur-Systems für Deutsch-Abiturklausuren:
- **Zielgruppe**: Lehrer in Niedersachsen (Pilot), später alle Bundesländer
- **Kernproblem**: Erstkorrektur dauert 6 Stunden pro Arbeit
- **Lösung**: KI schlägt Bewertungen vor, Lehrer bestätigt/korrigiert
---
## 2. Architektur-Übersicht
```
┌─────────────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ /website/app/admin/klausur-korrektur/ │
│ - page.tsx (Klausur-Liste) │
│ - [klausurId]/page.tsx (Studenten-Liste) │
│ - [klausurId]/[studentId]/page.tsx (Korrektur-Workspace) │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ klausur-service (FastAPI) │
│ Port 8086 - /klausur-service/backend/main.py │
│ - Klausur CRUD (/api/v1/klausuren) │
│ - Student Work (/api/v1/students) │
│ - Annotations (/api/v1/annotations) [NEU] │
│ - Gutachten Generation │
│ - Fairness Analysis │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ Infrastruktur │
│ - Qdrant (Vektor-DB für RAG) │
│ - MinIO (Datei-Storage) │
│ - PostgreSQL (Metadaten) │
│ - Embedding-Service (Port 8087) │
└─────────────────────────────────────────────────────────────┘
```
---
## 3. Bestehende Backend-Komponenten (NUTZEN!)
### 3.1 Klausur-Service API (main.py)
```python
# Bereits implementiert:
GET/POST /api/v1/klausuren # Klausur CRUD
GET /api/v1/klausuren/{id} # Klausur Details
POST /api/v1/klausuren/{id}/students # Student Work hochladen
GET /api/v1/klausuren/{id}/students # Studenten-Liste
PUT /api/v1/students/{id}/criteria # Kriterien bewerten
PUT /api/v1/students/{id}/gutachten # Gutachten speichern
POST /api/v1/students/{id}/gutachten/generate # Gutachten generieren (KI)
GET /api/v1/klausuren/{id}/fairness # Fairness-Analyse
GET /api/v1/grade-info # Notensystem-Info
```
### 3.2 Datenmodelle (main.py)
```python
@dataclass
class Klausur:
id: str
title: str
subject: str = "Deutsch"
year: int = 2025
semester: str = "Abitur"
modus: str = "abitur" # oder "vorabitur"
eh_id: Optional[str] = None # Erwartungshorizont-Referenz
@dataclass
class StudentKlausur:
id: str
klausur_id: str
anonym_id: str
file_path: str
ocr_text: str = ""
criteria_scores: Dict[str, int] = field(default_factory=dict)
gutachten: str = ""
status: str = "UPLOADED"
raw_points: int = 0
grade_points: int = 0
# Status-Workflow:
# UPLOADED → OCR_PROCESSING → OCR_COMPLETE → ANALYZING →
# FIRST_EXAMINER → SECOND_EXAMINER → COMPLETED
```
### 3.3 Notensystem (15-Punkte)
```python
GRADE_THRESHOLDS = {
15: 95, 14: 90, 13: 85, 12: 80, 11: 75,
10: 70, 9: 65, 8: 60, 7: 55, 6: 50,
5: 45, 4: 40, 3: 33, 2: 27, 1: 20, 0: 0
}
DEFAULT_CRITERIA = {
"rechtschreibung": {"name": "Rechtschreibung", "weight": 15},
"grammatik": {"name": "Grammatik", "weight": 15},
"inhalt": {"name": "Inhalt", "weight": 40},
"struktur": {"name": "Struktur", "weight": 15},
"stil": {"name": "Stil", "weight": 15}
}
```
---
## 4. NEU ZU IMPLEMENTIEREN
### Phase 1: Korrektur-Workspace MVP
#### 4.1 Frontend-Struktur
```
/website/app/admin/klausur-korrektur/
├── page.tsx # Klausur-Übersicht (Liste aller Klausuren)
├── types.ts # TypeScript Interfaces
├── [klausurId]/
│ ├── page.tsx # Studenten-Liste einer Klausur
│ └── [studentId]/
│ └── page.tsx # Korrektur-Workspace (2/3-1/3)
└── components/
├── KlausurCard.tsx # Klausur in Liste
├── StudentList.tsx # Studenten-Übersicht
├── DocumentViewer.tsx # PDF/Bild-Anzeige (links, 2/3)
├── AnnotationLayer.tsx # SVG-Overlay für Markierungen
├── AnnotationToolbar.tsx # Werkzeuge
├── CorrectionPanel.tsx # Bewertungs-Panel (rechts, 1/3)
├── CriteriaScoreCard.tsx # Einzelnes Kriterium
├── EHSuggestionPanel.tsx # EH-Vorschläge via RAG
├── GutachtenEditor.tsx # Gutachten bearbeiten
└── StudentNavigation.tsx # Prev/Next Navigation
```
#### 4.2 Annotations-Backend (NEU in main.py)
```python
# Neues Datenmodell:
@dataclass
class Annotation:
id: str
student_work_id: str
page: int
position: dict # {x, y, width, height} in % (0-100)
type: str # 'rechtschreibung' | 'grammatik' | 'inhalt' | 'struktur' | 'stil' | 'comment'
text: str # Kommentar-Text
severity: str # 'minor' | 'major' | 'critical'
suggestion: str # Korrekturvorschlag (bei RS/Gram)
created_by: str # User-ID (EK oder ZK)
created_at: datetime
role: str # 'first_examiner' | 'second_examiner'
linked_criterion: Optional[str] # Verknüpfung zu Kriterium
# Neue Endpoints:
POST /api/v1/students/{id}/annotations # Erstellen
GET /api/v1/students/{id}/annotations # Abrufen
PUT /api/v1/annotations/{id} # Ändern
DELETE /api/v1/annotations/{id} # Löschen
```
#### 4.3 UI-Layout Spezifikation
```
┌──────────────────────────────────────────────────────────────────────┐
│ Header: Klausur-Titel | Student: Anonym-123 | [← Prev] [5/24] [Next →]│
├─────────────────────────────────────────┬────────────────────────────┤
│ │ Tabs: [Kriterien] [Gutachten]│
│ ┌─────────────────────────────────┐ │ │
│ │ │ │ ▼ Rechtschreibung (15%) │
│ │ Dokument-Anzeige │ │ [====|====] 70/100 │
│ │ (PDF/Bild mit Zoom) │ │ 12 Fehler markiert │
│ │ │ │ │
│ │ + Annotation-Overlay │ │ ▼ Grammatik (15%) │
│ │ (SVG Layer) │ │ [====|====] 80/100 │
│ │ │ │ │
│ │ │ │ ▼ Inhalt (40%) │
│ │ │ │ [====|====] 65/100 │
│ │ │ │ EH-Vorschläge: [Laden] │
│ └─────────────────────────────────┘ │ │
│ │ ▼ Struktur (15%) │
│ Toolbar: [RS] [Gram] [Kommentar] │ [====|====] 75/100 │
│ [Zoom+] [Zoom-] [Fit] │ │
│ │ ▼ Stil (15%) │
│ Seiten: [1] [2] [3] [4] [5] │ [====|====] 70/100 │
│ │ │
│ │ ━━━━━━━━━━━━━━━━━━━━━━━━━━ │
│ │ Gesamtnote: 10 Punkte (2-) │
│ │ [Gutachten generieren] │
│ │ [Speichern] [Abschließen] │
├─────────────────────────────────────────┴────────────────────────────┤
│ 2/3 Breite │ 1/3 Breite │
└──────────────────────────────────────────────────────────────────────┘
```
---
## 5. Implementierungs-Reihenfolge
### Phase 1.1: Grundgerüst (AKTUELL)
1. ✅ Dokumentation erstellen
2. [ ] `/website/app/admin/klausur-korrektur/page.tsx` - Klausur-Liste
3. [ ] `/website/app/admin/klausur-korrektur/types.ts` - TypeScript Types
4. [ ] Navigation in AdminLayout.tsx hinzufügen
5. [ ] Deploy + Test
### Phase 1.2: Korrektur-Workspace
1. [ ] `[klausurId]/page.tsx` - Studenten-Liste
2. [ ] `[klausurId]/[studentId]/page.tsx` - Workspace
3. [ ] `components/DocumentViewer.tsx` - Bild/PDF Anzeige
4. [ ] `components/CorrectionPanel.tsx` - Bewertungs-Panel
5. [ ] Deploy + Test mit Lehrer
### Phase 1.3: Annotations-System
1. [ ] Backend: Annotations-Endpoints in main.py
2. [ ] `components/AnnotationLayer.tsx` - SVG Overlay
3. [ ] `components/AnnotationToolbar.tsx` - Werkzeuge
4. [ ] Farbkodierung: RS=rot, Gram=blau, Inhalt=grün
5. [ ] Deploy + Test
### Phase 1.4: EH-Integration
1. [ ] `components/EHSuggestionPanel.tsx`
2. [ ] Backend: `/api/v1/students/{id}/eh-suggestions`
3. [ ] RAG-Query mit Student-Text
4. [ ] Deploy + Test
### Phase 1.5: Gutachten-Editor
1. [ ] `components/GutachtenEditor.tsx`
2. [ ] Beleg-Verlinkung zu Annotations
3. [ ] Gutachten-Generierung Button
4. [ ] Deploy + Test
---
## 6. API-Konfiguration
```typescript
// Frontend API Base URLs
const KLAUSUR_SERVICE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
// Endpoints:
// Klausuren
GET ${KLAUSUR_SERVICE}/api/v1/klausuren
POST ${KLAUSUR_SERVICE}/api/v1/klausuren
GET ${KLAUSUR_SERVICE}/api/v1/klausuren/{id}
GET ${KLAUSUR_SERVICE}/api/v1/klausuren/{id}/students
// Studenten
GET ${KLAUSUR_SERVICE}/api/v1/students/{id}
GET ${KLAUSUR_SERVICE}/api/v1/students/{id}/file // Dokument-Download
PUT ${KLAUSUR_SERVICE}/api/v1/students/{id}/criteria
PUT ${KLAUSUR_SERVICE}/api/v1/students/{id}/gutachten
POST ${KLAUSUR_SERVICE}/api/v1/students/{id}/gutachten/generate
// Annotations (NEU)
GET ${KLAUSUR_SERVICE}/api/v1/students/{id}/annotations
POST ${KLAUSUR_SERVICE}/api/v1/students/{id}/annotations
PUT ${KLAUSUR_SERVICE}/api/v1/annotations/{id}
DELETE ${KLAUSUR_SERVICE}/api/v1/annotations/{id}
// System
GET ${KLAUSUR_SERVICE}/api/v1/grade-info
```
---
## 7. Deployment-Prozess
```bash
# 1. Dateien auf Mac Mini synchronisieren
rsync -avz --delete \
--exclude 'node_modules' --exclude '.next' --exclude '.git' \
/Users/benjaminadmin/Projekte/breakpilot-pwa/website/ \
macmini:/Users/benjaminadmin/Projekte/breakpilot-pwa/website/
# 2. Website-Container neu bauen
ssh macmini "/usr/local/bin/docker compose \
-f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml \
build --no-cache website"
# 3. Container neu starten
ssh macmini "/usr/local/bin/docker compose \
-f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml \
up -d website"
# 4. Testen unter:
# http://macmini:3000/admin/klausur-korrektur
```
---
## 8. Bundesland-Spezifika (Niedersachsen Pilot)
```json
// /klausur-service/backend/policies/bundeslaender.json
{
"NI": {
"name": "Niedersachsen",
"grading_mode": "points_15",
"requires_gutachten": true,
"zk_visibility": "full", // ZK sieht EK-Korrektur
"third_correction_threshold": 4, // Ab 4 Punkte Diff
"colors": {
"first_examiner": "#dc2626", // Rot
"second_examiner": "#16a34a" // Grün
},
"criteria_weights": {
"rechtschreibung": 15,
"grammatik": 15,
"inhalt": 40,
"struktur": 15,
"stil": 15
}
}
}
```
---
## 9. Wichtige Dateien (Referenz)
| Datei | Beschreibung |
|-------|--------------|
| `/klausur-service/backend/main.py` | Haupt-API, alle Endpoints |
| `/klausur-service/backend/eh_pipeline.py` | BYOEH Verarbeitung |
| `/klausur-service/backend/qdrant_service.py` | RAG Vector-Suche |
| `/klausur-service/backend/hybrid_search.py` | Hybrid Search |
| `/website/components/admin/AdminLayout.tsx` | Admin Navigation |
| `/website/app/admin/ocr-labeling/page.tsx` | Referenz für 2/3-1/3 Layout |
---
## 10. Testing-Checkliste
### Nach jeder Phase:
- [ ] Seite lädt ohne Fehler
- [ ] API-Calls funktionieren (DevTools Network)
- [ ] Responsives Layout korrekt
- [ ] Lehrer kann Workflow durchführen
### Lehrer-Test-Szenarien:
1. Klausur erstellen
2. 3+ Studentenarbeiten hochladen
3. Erste Arbeit korrigieren (alle Kriterien)
4. Annotations setzen
5. Gutachten generieren
6. Zur nächsten Arbeit navigieren
7. Fairness-Check nach allen Arbeiten
---
## 11. Phase 2: Zweitkorrektur-System (NEU)
### 11.1 Neue Backend-Endpoints (main.py)
```python
# Zweitkorrektur Workflow
POST /api/v1/students/{id}/start-zweitkorrektur # ZK starten (nach EK)
POST /api/v1/students/{id}/submit-zweitkorrektur # ZK-Ergebnis abgeben
# Einigung (bei Diff 3 Punkte)
POST /api/v1/students/{id}/einigung # Einigung einreichen
# Drittkorrektur (bei Diff >= 4 Punkte)
POST /api/v1/students/{id}/assign-drittkorrektor # DK zuweisen
POST /api/v1/students/{id}/submit-drittkorrektur # DK-Ergebnis (final)
# Workflow-Status & Visibility-Filtering
GET /api/v1/students/{id}/examiner-workflow # Workflow-Status abrufen
GET /api/v1/students/{id}/annotations-filtered # Policy-gefilterte Annotations
```
### 11.2 Workflow-Status
```python
class ExaminerWorkflowStatus(str, Enum):
NOT_STARTED = "not_started"
EK_IN_PROGRESS = "ek_in_progress"
EK_COMPLETED = "ek_completed"
ZK_ASSIGNED = "zk_assigned"
ZK_IN_PROGRESS = "zk_in_progress"
ZK_COMPLETED = "zk_completed"
EINIGUNG_REQUIRED = "einigung_required"
EINIGUNG_COMPLETED = "einigung_completed"
DRITTKORREKTUR_REQUIRED = "drittkorrektur_required"
DRITTKORREKTUR_ASSIGNED = "drittkorrektur_assigned"
DRITTKORREKTUR_IN_PROGRESS = "drittkorrektur_in_progress"
COMPLETED = "completed"
```
### 11.3 Visibility-Regeln (aus bundeslaender.json)
| Modus | ZK sieht EK-Annotations | ZK sieht EK-Note | ZK sieht EK-Gutachten |
|-------|-------------------------|------------------|----------------------|
| `blind` | Nein | Nein | Nein |
| `semi` (Bayern) | Ja | Nein | Nein |
| `full` (NI, Default) | Ja | Ja | Ja |
### 11.4 Konsens-Regeln
| Differenz EK-ZK | Aktion |
|-----------------|--------|
| 0-2 Punkte | Auto-Konsens (Durchschnitt) |
| 3 Punkte | Einigung erforderlich |
| >= 4 Punkte | Drittkorrektur erforderlich |
---
## 12. Aktueller Stand
**Datum**: 2026-01-21
**Phase**: Alle Phasen abgeschlossen
**Status**: MVP komplett - bereit fuer Produktionstest
### Abgeschlossen:
- [x] Phase 1: Korrektur-Workspace MVP
- [x] Phase 1.1: Grundgerüst (Klausur-Liste, Studenten-Liste)
- [x] Phase 1.2: Annotations-System
- [x] Phase 1.3: RS/Grammatik Overlays
- [x] Phase 1.4: EH-Vorschläge via RAG
- [x] Phase 2.1 Backend: Zweitkorrektur-Endpoints
- [x] Phase 2.2 Backend: Einigung-Endpoint
- [x] Phase 2.3 Backend: Drittkorrektur-Trigger
- [x] Phase 2.1 Frontend: ZK-Modus UI
- [x] Phase 2.2 Frontend: Einigung-Screen
- [x] Phase 3.1: Fairness-Dashboard Frontend
- [x] Phase 3.2: Ausreißer-Liste mit Quick-Adjust
- [x] Phase 3.3: Noten-Histogramm & Heatmap
- [x] Phase 4.1: PDF-Export Backend (reportlab)
- [x] Phase 4.2: PDF-Export Frontend
- [x] Phase 4.3: Vorabitur-Modus mit EH-Templates
### URLs:
- Klausur-Korrektur: `/admin/klausur-korrektur`
- Fairness-Dashboard: `/admin/klausur-korrektur/[klausurId]/fairness`
### PDF-Export Endpoints:
- `GET /api/v1/students/{id}/export/gutachten` - Einzelnes Gutachten als PDF
- `GET /api/v1/students/{id}/export/annotations` - Anmerkungen als PDF
- `GET /api/v1/klausuren/{id}/export/overview` - Notenübersicht als PDF
- `GET /api/v1/klausuren/{id}/export/all-gutachten` - Alle Gutachten als PDF
### Vorabitur-Modus Endpoints:
- `GET /api/v1/vorabitur/templates` - Liste aller EH-Templates
- `GET /api/v1/vorabitur/templates/{aufgabentyp}` - Template-Details
- `POST /api/v1/klausuren/{id}/vorabitur-eh` - Custom EH erstellen
- `GET /api/v1/klausuren/{id}/vorabitur-eh` - Verknuepften EH abrufen
- `PUT /api/v1/klausuren/{id}/vorabitur-eh` - EH aktualisieren
### Verfuegbare Aufgabentypen:
- `textanalyse_pragmatisch` - Sachtexte, Reden, Kommentare
- `gedichtanalyse` - Lyrik/Gedichte
- `prosaanalyse` - Romane, Kurzgeschichten
- `dramenanalyse` - Dramatische Texte
- `eroerterung_textgebunden` - Textgebundene Eroerterung
---
## 13. Lehrer-Anleitung (Schritt-fuer-Schritt)
### 13.1 Zugang zum System
**Weg 1: Ueber das Haupt-Dashboard**
1. Oeffnen Sie `http://macmini:8000/app` im Browser
2. Klicken Sie auf die Kachel "Abiturklausuren"
3. Sie werden automatisch zur Korrektur-Oberflaeche weitergeleitet
**Weg 2: Direkter Zugang**
1. Oeffnen Sie direkt `http://macmini:3000/admin/klausur-korrektur`
### 13.2 Zwei Einstiegs-Optionen
Beim ersten Besuch sehen Sie die Willkommens-Seite mit zwei Optionen:
#### Option A: Schnellstart (Direkt hochladen)
- Ideal wenn Sie sofort loslegen moechten
- Keine manuelle Klausur-Erstellung erforderlich
- System erstellt automatisch eine Klausur im Hintergrund
**Schritte:**
1. Klicken Sie auf "Schnellstart - Direkt hochladen"
2. **Schritt 1**: Ziehen Sie Ihre eingescannten Arbeiten (PDF/JPG/PNG) in den Upload-Bereich
3. **Schritt 2**: Optional - Waehlen Sie den Aufgabentyp und beschreiben Sie die Aufgabenstellung
4. **Schritt 3**: Pruefen Sie die Zusammenfassung und klicken "Korrektur starten"
5. Sie werden automatisch zur Korrektur-Ansicht weitergeleitet
#### Option B: Neue Klausur erstellen (Standard)
- Empfohlen fuer regelmaessige Nutzung
- Volle Metadaten (Fach, Jahr, Kurs, Modus)
- Unterstuetzt Zweitkorrektur-Workflow
**Schritte:**
1. Klicken Sie auf "Neue Klausur erstellen"
2. Geben Sie Titel, Fach, Jahr und Semester ein
3. Waehlen Sie den Modus:
- **Abitur**: Fuer offizielle Abitur-Pruefungen mit NiBiS-EH
- **Vorabitur**: Fuer Uebungsklausuren mit eigenem EH
4. Bei Vorabitur: Waehlen Sie Aufgabentyp und beschreiben Sie die Aufgabenstellung
5. Klicken Sie "Klausur erstellen"
### 13.3 Arbeiten hochladen
Nach Erstellung der Klausur:
1. Oeffnen Sie die Klausur aus der Liste
2. Klicken Sie "Arbeiten hochladen"
3. Waehlen Sie die eingescannten Dateien (PDF oder Bilder)
4. Geben Sie optional anonyme IDs (z.B. "Arbeit-1", "Arbeit-2")
5. Das System startet automatisch die OCR-Erkennung
### 13.4 Korrigieren
**Korrektur-Workspace (2/3-1/3 Layout):**
- Links (2/3): Das Originaldokument mit Zoom-Funktion
- Rechts (1/3): Bewertungspanel mit Kriterien
**Schritt fuer Schritt:**
1. Oeffnen Sie eine Arbeit durch Klick auf "Korrigieren"
2. Lesen Sie die Arbeit im linken Bereich (Zoom mit +/-)
3. Setzen Sie Anmerkungen durch Klick auf das Dokument
4. Waehlen Sie den Anmerkungstyp:
- **RS** (rot): Rechtschreibfehler
- **Gram** (blau): Grammatikfehler
- **Inhalt** (gruen): Inhaltliche Anmerkungen
- **Kommentar**: Allgemeine Bemerkungen
5. Bewerten Sie die 5 Kriterien im rechten Panel:
- Rechtschreibung (15%)
- Grammatik (15%)
- Inhalt (40%)
- Struktur (15%)
- Stil (15%)
6. Klicken Sie "EH-Vorschlaege laden" fuer KI-Unterstuetzung
7. Klicken Sie "Gutachten generieren" fuer einen KI-Vorschlag
8. Bearbeiten Sie das Gutachten nach Bedarf
9. Klicken Sie "Speichern" und dann "Naechste Arbeit"
### 13.5 Fairness-Analyse
Nach Korrektur mehrerer Arbeiten:
1. Klicken Sie auf "Fairness-Dashboard" in der Klausur-Ansicht
2. Pruefen Sie:
- **Noten-Histogramm**: Ist die Verteilung realistisch?
- **Ausreisser**: Gibt es ungewoehnlich hohe/niedrige Noten?
- **Kriterien-Heatmap**: Sind Kriterien konsistent bewertet?
3. Nutzen Sie "Quick-Adjust" um Anpassungen vorzunehmen
### 13.6 PDF-Export
1. In der Klausur-Ansicht klicken Sie "PDF-Export"
2. Waehlen Sie:
- **Einzelgutachten**: PDF fuer einen Schueler
- **Alle Gutachten**: Gesamtes PDF fuer alle Arbeiten
- **Notenuebersicht**: Uebersicht aller Noten
- **Anmerkungen**: Alle Annotationen als PDF
### 13.7 Zweitkorrektur (Optional)
Fuer offizielle Abitur-Klausuren:
1. Erstkorrektur abschliessen (Status: "Abgeschlossen")
2. Klicken Sie "Zweitkorrektur starten"
3. Der Zweitkorrektor bewertet unabhaengig
4. Bei Differenz >= 3 Punkte: Einigung erforderlich
5. Bei Differenz >= 4 Punkte: Drittkorrektur wird automatisch ausgeloest
### 13.8 Haeufige Fragen
**F: Kann ich eine Korrektur unterbrechen und spaeter fortsetzen?**
A: Ja, alle Aenderungen werden automatisch gespeichert.
**F: Was passiert mit meinen Daten?**
A: Alle Daten werden lokal auf dem Schulserver gespeichert. Keine Cloud-Speicherung.
**F: Kann ich den KI-Vorschlag komplett ueberschreiben?**
A: Ja, das Gutachten ist frei editierbar. Der KI-Vorschlag ist nur ein Startpunkt.
**F: Wie funktioniert die OCR-Erkennung?**
A: Das System erkennt Handschrift automatisch. Bei schlechter Lesbarkeit koennen Sie manuell nachbessern.
---
## 14. Integration Dashboard (Port 8000)
### 14.1 Aenderungen in dashboard.py
Die Funktion `openKlausurService()` wurde aktualisiert:
```javascript
// Alte Version: Oeffnete Port 8086 (Backend)
// Neue Version: Oeffnet Port 3000 (Next.js Frontend)
function openKlausurService() {
let baseUrl;
if (window.location.hostname === 'macmini') {
baseUrl = 'http://macmini:3000';
} else {
baseUrl = 'http://localhost:3000';
}
window.open(baseUrl + '/admin/klausur-korrektur', '_blank');
}
```
### 14.2 Neue Frontend-Features
- **Willkommens-Tab**: Erster Tab fuer neue Benutzer mit Workflow-Erklaerung
- **Direktupload-Wizard**: 3-Schritt-Wizard fuer Schnellstart
- **Drag & Drop**: Arbeiten per Drag & Drop hochladen
- **localStorage-Persistenz**: System merkt sich wiederkehrende Benutzer

View File

@@ -0,0 +1,141 @@
# Compliance-Checkliste
## Wann diese Checkliste anwenden?
**AUTOMATISCH bei:**
- Neuen Features mit Nutzerdaten
- Änderungen an Datenflüssen
- KI/ML-Funktionen
- Neuen API-Endpoints
- Datenbankschema-Änderungen
---
## 1. DSGVO-Check (Datenschutz-Grundverordnung)
### Rechtsgrundlage klären
| Rechtsgrundlage | Wann verwenden |
|-----------------|----------------|
| **Einwilligung (Art. 6 Abs. 1a)** | Optionale Features, Marketing, Analytics |
| **Vertragserfüllung (Art. 6 Abs. 1b)** | Kernfunktionen der Plattform |
| **Berechtigtes Interesse (Art. 6 Abs. 1f)** | Sicherheit, Betrugsprävention |
| **Rechtliche Verpflichtung (Art. 6 Abs. 1c)** | Aufbewahrungspflichten |
### Datenminimierung
- [ ] Werden nur notwendige Daten erhoben?
- [ ] Gibt es Felder, die optional sein könnten?
- [ ] Werden Daten nach Zweckerfüllung gelöscht?
### Besondere Kategorien (Art. 9)
**ACHTUNG bei:**
- Gesundheitsdaten (Krankheitstage, Atteste)
- Biometrische Daten (Gesichtserkennung, Stimme)
- Religiöse Überzeugungen
- Politische Meinungen
**Explizite Einwilligung erforderlich!**
### Minderjährige (Art. 8)
**Breakpilot-spezifisch:**
- Unter 16 Jahren: Einwilligung der Eltern
- Altersverifikation implementieren
- Kindgerechte Datenschutzerklärung
### Betroffenenrechte sicherstellen
- [ ] **Auskunft (Art. 15):** Kann der Nutzer seine Daten einsehen?
- [ ] **Berichtigung (Art. 16):** Kann der Nutzer Daten korrigieren?
- [ ] **Löschung (Art. 17):** Kann der Nutzer Löschung beantragen?
- [ ] **Datenportabilität (Art. 20):** Export in maschinenlesbarem Format?
---
## 2. AI Act Check (KI-Verordnung)
### Risikokategorie bestimmen
| Kategorie | Beispiele | Anforderungen |
|-----------|-----------|---------------|
| **Unakzeptabel** | Social Scoring, Manipulation | ❌ VERBOTEN |
| **Hochrisiko** | Bildungszugang, Prüfungsbewertung | Strenge Auflagen |
| **Begrenzt** | Chatbots, Empfehlungen | Transparenzpflicht |
| **Minimal** | Spam-Filter, Autokorrektur | Keine Auflagen |
### Breakpilot KI-Features prüfen
| Feature | Risiko | Maßnahmen |
|---------|--------|-----------|
| Klausur-OCR | Begrenzt | Transparenz, Human-in-Loop |
| KI-Korrekturvorschläge | Hochrisiko | Audit-Log, Erklärbarkeit |
| Lernempfehlungen | Begrenzt | Transparenz |
| Spracherkennung | Begrenzt | Consent, Transparenz |
### Hochrisiko-KI Anforderungen
Wenn Hochrisiko:
- [ ] Risikomanagementsystem dokumentiert
- [ ] Qualität der Trainingsdaten sichergestellt
- [ ] Technische Dokumentation vorhanden
- [ ] Audit-Logging aktiviert
- [ ] Human Oversight möglich
- [ ] Genauigkeit/Robustheit getestet
---
## 3. Technische Maßnahmen (TOM)
### Verschlüsselung
- [ ] **Transit:** TLS 1.3 für alle Verbindungen
- [ ] **Rest:** Datenbank-Verschlüsselung
- [ ] **Secrets:** Vault für Credentials
### Zugriffskontrollen
- [ ] RBAC implementiert
- [ ] Least Privilege Prinzip
- [ ] Session-Timeouts
### Audit-Logging
```python
# Beispiel: Audit-Event loggen
audit_log.info({
"action": "data_export",
"user_id": user.id,
"timestamp": datetime.utcnow(),
"data_categories": ["grades", "personal"],
"legal_basis": "Art. 20 DSGVO"
})
```
---
## 4. Dokumentationspflichten
### Bei neuen Features aktualisieren
| Dokument | URL | Wann aktualisieren |
|----------|-----|-------------------|
| VVT | https://macmini:3002/sdk/vvt | Neue Verarbeitung |
| TOM | https://macmini:3002/sdk/tom | Neue Schutzmaßnahme |
| DSFA | https://macmini:3002/sdk/dsfa | Hochrisiko-Verarbeitung |
| Löschfristen | https://macmini:3002/sdk/loeschfristen | Neue Datenkategorie |
---
## 5. Schnell-Check (5 Fragen)
Vor jedem Feature diese 5 Fragen beantworten:
1. **WER** sind die Betroffenen? (Schüler, Lehrer, Eltern)
2. **WAS** für Daten werden verarbeitet?
3. **WARUM** werden sie verarbeitet? (Rechtsgrundlage)
4. **WIE LANGE** werden sie gespeichert?
5. **WER** hat Zugriff?
Können alle 5 Fragen beantwortet werden? → Feature ist dokumentierbar.

View File

@@ -0,0 +1,91 @@
# Dokumentations-Regeln
## Automatische Dokumentations-Aktualisierung
**WICHTIG:** Bei JEDER Code-Änderung muss die entsprechende Dokumentation aktualisiert werden!
## Wann Dokumentation aktualisieren?
### API-Änderungen
Wenn du einen Endpoint änderst, hinzufügst oder entfernst:
- Aktualisiere `/docs/api/consent-service-api.md` (Go Endpoints)
- Aktualisiere `/docs/api/backend-api.md` (Python Endpoints)
### Neue Funktionen/Klassen
Wenn du neue Funktionen, Klassen oder Module erstellst:
- Aktualisiere `/docs/consent-service/README.md` (für Go)
- Aktualisiere `/docs/backend/README.md` (für Python)
### Architektur-Änderungen
Wenn du die Systemarchitektur änderst:
- Aktualisiere `/docs/architecture/system-architecture.md`
- Aktualisiere `/docs/architecture/data-model.md` (bei DB-Änderungen)
### Neue Konfigurationsoptionen
Wenn du neue Umgebungsvariablen oder Konfigurationen hinzufügst:
- Aktualisiere die entsprechende README
- Füge zur `guides/local-development.md` hinzu
## Dokumentations-Format
### API-Endpoints dokumentieren
```markdown
### METHOD /path/to/endpoint
Kurze Beschreibung.
**Request Body:**
\`\`\`json
{
"field": "value"
}
\`\`\`
**Response (200):**
\`\`\`json
{
"result": "value"
}
\`\`\`
**Errors:**
- `400`: Beschreibung
- `401`: Beschreibung
```
### Funktionen dokumentieren
```markdown
### FunctionName (file.go:123)
\`\`\`go
func FunctionName(param Type) ReturnType
\`\`\`
**Beschreibung:** Was macht die Funktion?
**Parameter:**
- `param`: Beschreibung
**Rückgabe:** Beschreibung
```
## Checkliste nach Code-Änderungen
Vor dem Abschluss einer Aufgabe prüfe:
- [ ] Wurden neue API-Endpoints hinzugefügt? → API-Docs aktualisieren
- [ ] Wurden Datenmodelle geändert? → data-model.md aktualisieren
- [ ] Wurden neue Konfigurationen hinzugefügt? → README aktualisieren
- [ ] Wurden neue Abhängigkeiten hinzugefügt? → requirements.txt/go.mod UND Docs
- [ ] Wurde die Architektur geändert? → architecture/ aktualisieren
## Beispiel: Vollständige Dokumentation einer neuen Funktion
Wenn du z.B. `GetUserStats()` im Go Service hinzufügst:
1. **Code schreiben** in `internal/services/stats_service.go`
2. **API-Doc aktualisieren** in `docs/api/consent-service-api.md`
3. **Service-Doc aktualisieren** in `docs/consent-service/README.md`
4. **Test schreiben** (siehe testing.md)

View File

@@ -0,0 +1,250 @@
# Experimental Dashboard - Apple Weather Style UI
**Status:** In Entwicklung
**Letzte Aktualisierung:** 2026-01-24
**URL:** http://macmini:3001/dashboard-experimental
---
## Uebersicht
Das Experimental Dashboard implementiert einen **Apple Weather App Style** mit:
- Ultra-transparenten Glassmorphism-Cards (~8% Opacity)
- Dunklem Sternenhimmel-Hintergrund mit Parallax
- Weisser Schrift auf monochromem Design
- Schwebenden Nachrichten (FloatingMessage) mit ~4% Background
- Nuetzlichen Widgets: Uhr, Wetter, Kompass, Diagramme
---
## Design-Prinzipien
| Prinzip | Umsetzung |
|---------|-----------|
| **Transparenz** | Cards mit 8% Opacity, Messages mit 4% |
| **Verschmelzung** | Elemente verschmelzen mit dem Hintergrund |
| **Monochrom** | Weisse Schrift, keine bunten Akzente |
| **Subtilitaet** | Dezente Hover-Effekte, sanfte Animationen |
| **Nuetzlichkeit** | Echte Informationen (Uhrzeit, Wetter) |
---
## Dateistruktur
```
/studio-v2/
├── app/
│ └── dashboard-experimental/
│ └── page.tsx # Haupt-Dashboard (740 Zeilen)
├── components/
│ └── spatial-ui/
│ ├── index.ts # Exports
│ ├── SpatialCard.tsx # Original SpatialCard (nicht verwendet)
│ └── FloatingMessage.tsx # Schwebende Nachrichten
└── lib/
└── spatial-ui/
├── index.ts # Exports
├── depth-system.ts # Design Tokens
├── PerformanceContext.tsx # Adaptive Qualitaet
└── FocusContext.tsx # Focus-Modus
```
---
## Komponenten
### GlassCard
Ultra-transparente Card fuer alle Inhalte.
```typescript
interface GlassCardProps {
children: React.ReactNode
className?: string
onClick?: () => void
size?: 'sm' | 'md' | 'lg' // Padding: 16px, 20px, 24px
delay?: number // Einblend-Verzoegerung in ms
}
```
**Styling:**
- Background: `rgba(255, 255, 255, 0.08)` (8%)
- Hover: `rgba(255, 255, 255, 0.12)` (12%)
- Border: `1px solid rgba(255, 255, 255, 0.1)`
- Blur: 24px (adaptiv)
- Border-Radius: 24px (rounded-3xl)
### AnalogClock
Analoge Uhr mit Sekundenzeiger.
- Stunden-Zeiger: Weiss, dick
- Minuten-Zeiger: Weiss/80%, duenn
- Sekunden-Zeiger: Orange (#fb923c)
- 12 Stundenmarkierungen
- Aktualisiert jede Sekunde
### Compass
Kompass im Apple Weather Style.
```typescript
interface CompassProps {
direction?: number // Grad (0 = Nord, 90 = Ost, etc.)
}
```
- Nord-Nadel: Rot (#ef4444)
- Sued-Nadel: Weiss
- Kardinalrichtungen: N (rot), S, W, O
### BarChart
Balkendiagramm fuer Wochen-Statistiken.
```typescript
interface BarChartProps {
data: { label: string; value: number; highlight?: boolean }[]
maxValue?: number
}
```
- Highlight-Balken mit Gradient (blau → lila)
- Normale Balken: 20% weiss
- Labels unten, Werte oben
### ProgressRing
Kreisfoermiger Fortschrittsanzeiger.
```typescript
interface ProgressRingProps {
progress: number // 0-100
size?: number // Default: 80px
strokeWidth?: number // Default: 6px
label: string
value: string
color?: string // Farbe des Fortschritts
}
```
### TemperatureDisplay
Wetter-Anzeige mit Icon und Temperatur.
```typescript
interface TemperatureDisplayProps {
temp: number
condition: 'sunny' | 'cloudy' | 'rainy' | 'snowy' | 'partly_cloudy'
}
```
### FloatingMessage
Schwebende Benachrichtigungen von rechts.
**Aktuell:**
- Background: 4% Opacity
- Blur: 24px
- Border: `1px solid rgba(255, 255, 255, 0.12)`
- Auto-Dismiss mit Progress-Bar
- 3 Antwort-Optionen: Antworten, Oeffnen, Spaeter
- Typewriter-Effekt fuer Text
---
## Farbpalette
| Element | Wert |
|---------|------|
| Background | `from-slate-900 via-indigo-950 to-slate-900` |
| Card Background | `rgba(255, 255, 255, 0.08)` |
| Card Hover | `rgba(255, 255, 255, 0.12)` |
| Message Background | `rgba(255, 255, 255, 0.04)` |
| Border | `rgba(255, 255, 255, 0.1)` |
| Text Primary | `text-white` |
| Text Secondary | `text-white/50` bis `text-white/40` |
| Accent Blue | `#60a5fa` |
| Accent Purple | `#a78bfa` |
| Accent Orange | `#fb923c` (Sekundenzeiger) |
| Accent Red | `#ef4444` (Kompass Nord) |
---
## Performance-System
Das Dashboard nutzt das **PerformanceContext** fuer adaptive Qualitaet:
| Quality Level | Blur | Parallax | Animationen |
|---------------|------|----------|-------------|
| high | 24px | Ja | Spring |
| medium | 17px | Ja | Standard |
| low | 0px | Nein | Reduziert |
| minimal | 0px | Nein | Keine |
**FPS-Monitor** unten links zeigt:
- Aktuelle FPS
- Quality Level
- Blur/Parallax Status
---
## Deployment
```bash
# 1. Sync zu Mac Mini
rsync -avz --delete \
--exclude 'node_modules' --exclude '.next' --exclude '.git' \
/Users/benjaminadmin/Projekte/breakpilot-pwa/studio-v2/ \
macmini:/Users/benjaminadmin/Projekte/breakpilot-pwa/studio-v2/
# 2. Build
ssh macmini "/usr/local/bin/docker compose \
-f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml \
build --no-cache studio-v2"
# 3. Deploy
ssh macmini "/usr/local/bin/docker compose \
-f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml \
up -d studio-v2"
# 4. Testen
http://macmini:3001/dashboard-experimental
```
---
## Offene Punkte / Ideen
### Kurzfristig
- [ ] Echte Wetterdaten via API integrieren
- [ ] Kompass-Richtung dynamisch (GPS oder manuell)
- [ ] Klick auf Cards fuehrt zu Detailseiten
- [ ] Light Mode Support (aktuell nur Dark)
### Mittelfristig
- [ ] Drag & Drop fuer Card-Anordnung
- [ ] Weitere Widgets: Kalender, Termine, Erinnerungen
- [ ] Animierte Uebergaenge zwischen Seiten
- [ ] Sound-Feedback bei Interaktionen
### Langfristig
- [ ] Personalisierbare Widgets
- [ ] Dashboard als Standard-Startseite
- [ ] Mobile-optimierte Version
- [ ] Integration mit Apple Health / Fitness Daten
---
## Referenzen
- **Apple Weather App** (iOS) - Hauptinspiration
- **Dribbble Shot:** https://dribbble.com/shots/26339637-Smart-Home-Dashboard-Glassmorphism-UI
- **Design Tokens:** `/studio-v2/lib/spatial-ui/depth-system.ts`
---
## Aenderungshistorie
| Datum | Aenderung |
|-------|-----------|
| 2026-01-24 | FloatingMessage auf 4% Opacity reduziert |
| 2026-01-24 | Kompass, Balkendiagramm, Analog-Uhr hinzugefuegt |
| 2026-01-24 | Cards auf 8% Opacity reduziert |
| 2026-01-24 | Apple Weather Style implementiert |
| 2026-01-24 | Erstes Spatial UI System erstellt |

View File

@@ -0,0 +1,295 @@
# Multi-Agent Architektur - Entwicklerdokumentation
**Status:** Implementiert
**Letzte Aktualisierung:** 2025-01-15
**Modul:** `/agent-core/`
---
## 1. Übersicht
Die Multi-Agent-Architektur erweitert Breakpilot um ein verteiltes Agent-System basierend auf Mission Control Konzepten.
### Kernkomponenten
| Komponente | Pfad | Beschreibung |
|------------|------|--------------|
| Session Management | `/agent-core/sessions/` | Lifecycle & Recovery |
| Shared Brain | `/agent-core/brain/` | Langzeit-Gedächtnis |
| Orchestrator | `/agent-core/orchestrator/` | Koordination |
| SOUL Files | `/agent-core/soul/` | Agent-Persönlichkeiten |
---
## 2. Agent-Typen
| Agent | Aufgabe | SOUL-Datei |
|-------|---------|------------|
| **TutorAgent** | Lernbegleitung, Fragen beantworten | `tutor-agent.soul.md` |
| **GraderAgent** | Klausur-Korrektur, Bewertung | `grader-agent.soul.md` |
| **QualityJudge** | BQAS Qualitätsprüfung | `quality-judge.soul.md` |
| **AlertAgent** | Monitoring, Benachrichtigungen | `alert-agent.soul.md` |
| **Orchestrator** | Task-Koordination | `orchestrator.soul.md` |
---
## 3. Wichtige Dateien
### Session Management
```
agent-core/sessions/
├── session_manager.py # AgentSession, SessionManager, SessionState
├── heartbeat.py # HeartbeatMonitor, HeartbeatClient
└── checkpoint.py # CheckpointManager
```
### Shared Brain
```
agent-core/brain/
├── memory_store.py # MemoryStore, Memory (mit TTL)
├── context_manager.py # ConversationContext, ContextManager
└── knowledge_graph.py # KnowledgeGraph, Entity, Relationship
```
### Orchestrator
```
agent-core/orchestrator/
├── message_bus.py # MessageBus, AgentMessage, MessagePriority
├── supervisor.py # AgentSupervisor, AgentInfo, AgentStatus
└── task_router.py # TaskRouter, RoutingRule, RoutingResult
```
---
## 4. Datenbank-Schema
Die Migration befindet sich in:
`/backend/migrations/add_agent_core_tables.sql`
### Tabellen
1. **agent_sessions** - Session-Daten mit Checkpoints
2. **agent_memory** - Langzeit-Gedächtnis mit TTL
3. **agent_messages** - Audit-Trail für Inter-Agent Kommunikation
### Helper-Funktionen
```sql
-- Abgelaufene Memories bereinigen
SELECT cleanup_expired_agent_memory();
-- Inaktive Sessions bereinigen
SELECT cleanup_stale_agent_sessions(48); -- 48 Stunden
```
---
## 5. Integration Voice-Service
Der `EnhancedTaskOrchestrator` erweitert den bestehenden `TaskOrchestrator`:
```python
# voice-service/services/enhanced_task_orchestrator.py
from agent_core.sessions import SessionManager
from agent_core.orchestrator import MessageBus
class EnhancedTaskOrchestrator(TaskOrchestrator):
# Nutzt Session-Checkpoints für Recovery
# Routet komplexe Tasks an spezialisierte Agents
# Führt Quality-Checks via BQAS durch
```
**Wichtig:** Der Enhanced Orchestrator ist abwärtskompatibel und kann parallel zum Original verwendet werden.
---
## 6. Integration BQAS
Der `QualityJudgeAgent` integriert BQAS mit dem Multi-Agent-System:
```python
# voice-service/bqas/quality_judge_agent.py
from bqas.judge import LLMJudge
from agent_core.orchestrator import MessageBus
class QualityJudgeAgent:
# Wertet Responses in Echtzeit aus
# Nutzt Memory für konsistente Bewertungen
# Empfängt Evaluierungs-Requests via Message Bus
```
---
## 7. Code-Beispiele
### Session erstellen
```python
from agent_core.sessions import SessionManager
manager = SessionManager(redis_client=redis, db_pool=pool)
session = await manager.create_session(
agent_type="tutor-agent",
user_id="user-123"
)
```
### Memory speichern
```python
from agent_core.brain import MemoryStore
store = MemoryStore(redis_client=redis, db_pool=pool)
await store.remember(
key="student:123:progress",
value={"level": 5, "score": 85},
agent_id="tutor-agent",
ttl_days=30
)
```
### Nachricht senden
```python
from agent_core.orchestrator import MessageBus, AgentMessage
bus = MessageBus(redis_client=redis)
await bus.publish(AgentMessage(
sender="orchestrator",
receiver="grader-agent",
message_type="grade_request",
payload={"exam_id": "exam-1"}
))
```
---
## 8. Tests ausführen
```bash
# Alle Agent-Core Tests
cd agent-core && pytest -v
# Mit Coverage-Report
pytest --cov=. --cov-report=html
# Einzelne Module
pytest tests/test_session_manager.py -v
pytest tests/test_message_bus.py -v
```
---
## 9. Deployment-Schritte
### 1. Migration ausführen
```bash
psql -h localhost -U breakpilot -d breakpilot \
-f backend/migrations/add_agent_core_tables.sql
```
### 2. Voice-Service aktualisieren
```bash
# Sync zu Server
rsync -avz --exclude 'node_modules' --exclude '.git' \
/path/to/breakpilot-pwa/ server:/path/to/breakpilot-pwa/
# Container neu bauen
docker compose build --no-cache voice-service
# Starten
docker compose up -d voice-service
```
### 3. Verifizieren
```bash
# Session-Tabelle prüfen
psql -c "SELECT COUNT(*) FROM agent_sessions;"
# Memory-Tabelle prüfen
psql -c "SELECT COUNT(*) FROM agent_memory;"
```
---
## 10. Monitoring
### Metriken
| Metrik | Beschreibung |
|--------|--------------|
| `agent_session_count` | Anzahl aktiver Sessions |
| `agent_heartbeat_delay_ms` | Zeit seit letztem Heartbeat |
| `agent_message_latency_ms` | Nachrichtenlatenz |
| `agent_memory_count` | Gespeicherte Memories |
| `agent_routing_success_rate` | Erfolgreiche Routings |
### Health-Check-Endpunkte
```
GET /api/v1/agents/health # Supervisor Status
GET /api/v1/agents/sessions # Aktive Sessions
GET /api/v1/agents/memory/stats # Memory-Statistiken
```
---
## 11. Troubleshooting
### Problem: Session nicht gefunden
1. Prüfen ob Valkey läuft: `redis-cli ping`
2. Session-Timeout prüfen (default 24h)
3. Heartbeat-Status checken
### Problem: Message Bus Timeout
1. Redis Pub/Sub Status prüfen
2. Ziel-Agent registriert?
3. Timeout erhöhen (default 30s)
### Problem: Memory nicht gefunden
1. Namespace korrekt?
2. TTL abgelaufen?
3. Cleanup-Job gelaufen?
---
## 12. Erweiterungen
### Neuen Agent hinzufügen
1. SOUL-Datei erstellen in `/agent-core/soul/`
2. Routing-Regel in `task_router.py` hinzufügen
3. Handler beim Supervisor registrieren
4. Tests schreiben
### Neuen Memory-Typ hinzufügen
1. Key-Schema definieren (z.B. `student:*:progress`)
2. TTL festlegen
3. Access-Pattern dokumentieren
---
## 13. Referenzen
- **Agent-Core README:** `/agent-core/README.md`
- **Migration:** `/backend/migrations/add_agent_core_tables.sql`
- **Voice-Service Integration:** `/voice-service/services/enhanced_task_orchestrator.py`
- **BQAS Integration:** `/voice-service/bqas/quality_judge_agent.py`
- **Tests:** `/agent-core/tests/`
---
## 14. Änderungshistorie
| Datum | Version | Änderung |
|-------|---------|----------|
| 2025-01-15 | 1.0.0 | Initial Release |

View File

@@ -0,0 +1,297 @@
# Night Scheduler - Entwicklerdokumentation
**Status:** Produktiv
**Letzte Aktualisierung:** 2026-02-09
**URL:** https://macmini:3002/infrastructure/night-mode
**API:** http://macmini:8096
---
## Uebersicht
Der Night Scheduler ermoeglicht die automatische Nachtabschaltung der Docker-Services:
- Zeitgesteuerte Abschaltung (Standard: 22:00)
- Zeitgesteuerter Start (Standard: 06:00)
- Manuelle Sofortaktionen (Start/Stop)
- Dashboard-UI zur Konfiguration
---
## Architektur
```
┌─────────────────────────────────────────────────────────────┐
│ Admin Dashboard (Port 3002) │
│ /infrastructure/night-mode │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ API Proxy: /api/admin/night-mode │
│ - GET: Status abrufen │
│ - POST: Konfiguration speichern │
│ - POST /execute: Sofortaktion (start/stop) │
│ - GET /services: Service-Liste │
└─────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────┐
│ night-scheduler (Port 8096) │
│ - Python/FastAPI Container │
│ - Prueft jede Minute ob Aktion faellig │
│ - Fuehrt docker compose start/stop aus │
│ - Speichert Config in /config/night-mode.json │
└─────────────────────────────────────────────────────────────┘
```
---
## Dateien
| Pfad | Beschreibung |
|------|--------------|
| `night-scheduler/scheduler.py` | Python Scheduler mit FastAPI |
| `night-scheduler/Dockerfile` | Container mit Docker CLI |
| `night-scheduler/requirements.txt` | Dependencies |
| `night-scheduler/config/night-mode.json` | Konfigurationsdatei |
| `night-scheduler/tests/test_scheduler.py` | Unit Tests |
| `admin-v2/app/api/admin/night-mode/route.ts` | API Proxy |
| `admin-v2/app/api/admin/night-mode/execute/route.ts` | Execute Endpoint |
| `admin-v2/app/api/admin/night-mode/services/route.ts` | Services Endpoint |
| `admin-v2/app/(admin)/infrastructure/night-mode/page.tsx` | UI Seite |
---
## API Endpoints
### GET /api/night-mode
Status und Konfiguration abrufen.
**Response:**
```json
{
"config": {
"enabled": true,
"shutdown_time": "22:00",
"startup_time": "06:00",
"last_action": "startup",
"last_action_time": "2026-02-09T06:00:00",
"excluded_services": ["night-scheduler", "nginx"]
},
"current_time": "14:30:00",
"next_action": "shutdown",
"next_action_time": "22:00",
"time_until_next_action": "7h 30min",
"services_status": {
"backend": "running",
"postgres": "running"
}
}
```
### POST /api/night-mode
Konfiguration aktualisieren.
**Request:**
```json
{
"enabled": true,
"shutdown_time": "23:00",
"startup_time": "07:00",
"excluded_services": ["night-scheduler", "nginx", "vault"]
}
```
### POST /api/night-mode/execute
Sofortige Aktion ausfuehren.
**Request:**
```json
{
"action": "stop" // oder "start"
}
```
**Response:**
```json
{
"success": true,
"message": "Aktion 'stop' erfolgreich ausgefuehrt fuer 25 Services"
}
```
### GET /api/night-mode/services
Liste aller Services abrufen.
**Response:**
```json
{
"all_services": ["backend", "postgres", "valkey", ...],
"excluded_services": ["night-scheduler", "nginx"],
"status": {
"backend": "running",
"postgres": "running"
}
}
```
---
## Konfiguration
### Config-Format (night-mode.json)
```json
{
"enabled": true,
"shutdown_time": "22:00",
"startup_time": "06:00",
"last_action": "startup",
"last_action_time": "2026-02-09T06:00:00",
"excluded_services": ["night-scheduler", "nginx"]
}
```
### Umgebungsvariablen
| Variable | Default | Beschreibung |
|----------|---------|--------------|
| `COMPOSE_PROJECT_NAME` | `breakpilot-pwa` | Docker Compose Projektname |
---
## Ausgeschlossene Services
Diese Services werden NICHT gestoppt:
1. **night-scheduler** - Muss laufen, um Services zu starten
2. **nginx** - Optional, fuer HTTPS-Zugriff
Weitere Services koennen ueber die Konfiguration ausgeschlossen werden.
---
## Docker Compose Integration
```yaml
night-scheduler:
build: ./night-scheduler
container_name: breakpilot-pwa-night-scheduler
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- ./night-scheduler/config:/config
- ./docker-compose.yml:/app/docker-compose.yml:ro
environment:
- COMPOSE_PROJECT_NAME=breakpilot-pwa
ports:
- "8096:8096"
networks:
- breakpilot-pwa-network
restart: unless-stopped
```
---
## Tests ausfuehren
```bash
# Im Container
docker exec -it breakpilot-pwa-night-scheduler pytest -v
# Lokal (mit Dependencies)
cd night-scheduler
pip install -r requirements.txt
pytest -v tests/
```
---
## Deployment
```bash
# 1. Dateien synchronisieren
rsync -avz night-scheduler/ macmini:.../night-scheduler/
# 2. Container bauen
ssh macmini "docker compose -f .../docker-compose.yml build --no-cache night-scheduler"
# 3. Container starten
ssh macmini "docker compose -f .../docker-compose.yml up -d night-scheduler"
# 4. Testen
curl http://macmini:8096/health
curl http://macmini:8096/api/night-mode
```
---
## Troubleshooting
### Problem: Services werden nicht gestoppt/gestartet
1. Pruefen ob Docker Socket gemountet ist:
```bash
docker exec breakpilot-pwa-night-scheduler ls -la /var/run/docker.sock
```
2. Pruefen ob docker compose CLI verfuegbar ist:
```bash
docker exec breakpilot-pwa-night-scheduler docker compose version
```
3. Logs pruefen:
```bash
docker logs breakpilot-pwa-night-scheduler
```
### Problem: Konfiguration wird nicht gespeichert
1. Pruefen ob /config beschreibbar ist:
```bash
docker exec breakpilot-pwa-night-scheduler touch /config/test
```
2. Volume-Mount pruefen in docker-compose.yml
### Problem: API nicht erreichbar
1. Container-Status pruefen:
```bash
docker ps | grep night-scheduler
```
2. Health-Check pruefen:
```bash
curl http://localhost:8096/health
```
---
## Sicherheitshinweise
- Der Container benoetigt Zugriff auf den Docker Socket
- Nur interne Services koennen gestoppt/gestartet werden
- Keine Authentifizierung (internes Netzwerk)
- Keine sensitiven Daten in der Konfiguration
---
## Dependencies (SBOM)
| Package | Version | Lizenz |
|---------|---------|--------|
| FastAPI | 0.109.0 | MIT |
| Uvicorn | 0.27.0 | BSD-3-Clause |
| Pydantic | 2.5.3 | MIT |
| pytest | 8.0.0 | MIT |
| pytest-asyncio | 0.23.0 | Apache-2.0 |
| httpx | 0.26.0 | BSD-3-Clause |
---
## Aenderungshistorie
| Datum | Aenderung |
|-------|-----------|
| 2026-02-09 | Initiale Implementierung |

View File

@@ -0,0 +1,99 @@
# Open Source Policy
## Lizenzprüfung (AUTOMATISCH BEI JEDER DEPENDENCY)
### Erlaubte Lizenzen ✅
| Lizenz | Typ | Kommerziell OK |
|--------|-----|----------------|
| MIT | Permissive | ✅ |
| Apache-2.0 | Permissive | ✅ |
| BSD-2-Clause | Permissive | ✅ |
| BSD-3-Clause | Permissive | ✅ |
| ISC | Permissive | ✅ |
| MPL-2.0 | Weak Copyleft | ✅ |
| LGPL-2.1 / LGPL-3.0 | Weak Copyleft | ✅ (nur linking) |
| CC0-1.0 | Public Domain | ✅ |
| Unlicense | Public Domain | ✅ |
### Verbotene Lizenzen ❌
| Lizenz | Grund |
|--------|-------|
| GPL-2.0 / GPL-3.0 | Copyleft - infiziert Projekt |
| AGPL-3.0 | Network Copyleft - SaaS-Killer |
| SSPL | Server Side Public License |
| BSL | Business Source License |
| "Non-Commercial" | Keine kommerzielle Nutzung |
| "Educational Only" | Nur für Bildung |
| Proprietary | Keine OSS |
---
## Workflow bei neuer Dependency
### 1. Vor dem Hinzufügen prüfen
```bash
# NPM Package
npm view <package> license
# Python Package
pip show <package> | grep License
# Go Module
go-licenses check <module>
```
### 2. Bei Unklarheit
- README.md des Projekts lesen
- LICENSE-Datei prüfen
- SPDX-Identifier suchen
- Im Zweifel: **NICHT verwenden**
### 3. Nach dem Hinzufügen
**SBOM aktualisieren:** https://macmini:3002/infrastructure/sbom
```bash
# SBOM generieren
cd /Users/benjaminadmin/Projekte/breakpilot-pwa
# Python
pip-licenses --format=json > sbom/python-licenses.json
# Node.js
npx license-checker --json > sbom/node-licenses.json
# Go
go-licenses csv ./... > sbom/go-licenses.csv
```
---
## Grenzfälle
### Dual-Licensed Packages
- Wenn MIT **oder** GPL angeboten wird → MIT wählen
- Dokumentieren welche Lizenz gewählt wurde
### Transitive Dependencies
- Auch indirekte Abhängigkeiten prüfen
- `npm ls`, `pip-tree`, `go mod graph`
### Fonts & Assets
- Google Fonts: ✅ (OFL)
- Font Awesome Free: ✅ (CC BY 4.0 / OFL / MIT)
- Icons8: ❌ (Attribution required, kompliziert)
---
## Checkliste bei PR/Commit
Wenn neue Dependencies hinzugefügt wurden:
- [ ] Lizenz ist in der Whitelist
- [ ] SBOM wurde aktualisiert
- [ ] Keine GPL/AGPL-Abhängigkeiten eingeschleppt
- [ ] Bei Dual-License: MIT/Apache gewählt

202
.claude/rules/testing.md Normal file
View File

@@ -0,0 +1,202 @@
# Test-Regeln
## Automatische Test-Erweiterung
**WICHTIG:** Bei JEDER Code-Änderung müssen entsprechende Tests erstellt oder aktualisiert werden!
## Wann Tests schreiben?
### IMMER wenn du:
1. **Neue Funktionen** erstellst → Unit Test
2. **Neue API-Endpoints** hinzufügst → Handler Test
3. **Bugs fixst** → Regression Test (der Bug sollte nie wieder auftreten)
4. **Bestehenden Code änderst** → Bestehende Tests anpassen
## Test-Struktur
### Go Tests (Consent Service)
**Speicherort:** Im gleichen Verzeichnis wie der Code
```
internal/
├── services/
│ ├── auth_service.go
│ └── auth_service_test.go ← Test hier
├── handlers/
│ ├── handlers.go
│ └── handlers_test.go ← Test hier
└── middleware/
├── auth.go
└── middleware_test.go ← Test hier
```
**Test-Namenskonvention:**
```go
func TestFunctionName_Scenario_ExpectedResult(t *testing.T)
// Beispiele:
func TestHashPassword_ValidPassword_ReturnsHash(t *testing.T)
func TestLogin_InvalidCredentials_Returns401(t *testing.T)
func TestCreateDocument_MissingTitle_ReturnsError(t *testing.T)
```
**Test-Template:**
```go
func TestFunctionName(t *testing.T) {
// Arrange
service := &MyService{}
input := "test-input"
// Act
result, err := service.DoSomething(input)
// Assert
if err != nil {
t.Fatalf("Expected no error, got %v", err)
}
if result != expected {
t.Errorf("Expected %v, got %v", expected, result)
}
}
```
**Table-Driven Tests bevorzugen:**
```go
func TestValidateEmail(t *testing.T) {
tests := []struct {
name string
email string
expected bool
}{
{"valid email", "test@example.com", true},
{"missing @", "testexample.com", false},
{"empty", "", false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := ValidateEmail(tt.email)
if result != tt.expected {
t.Errorf("Expected %v, got %v", tt.expected, result)
}
})
}
}
```
### Python Tests (Backend)
**Speicherort:** `/backend/tests/`
```
backend/
├── consent_client.py
├── gdpr_api.py
└── tests/
├── __init__.py
├── test_consent_client.py ← Tests für consent_client.py
└── test_gdpr_api.py ← Tests für gdpr_api.py
```
**Test-Namenskonvention:**
```python
class TestClassName:
def test_method_scenario_expected_result(self):
pass
# Beispiele:
class TestConsentClient:
def test_check_consent_valid_token_returns_status(self):
pass
def test_check_consent_expired_token_raises_error(self):
pass
```
**Test-Template:**
```python
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
class TestMyFeature:
def test_sync_function(self):
# Arrange
input_data = "test"
# Act
result = my_function(input_data)
# Assert
assert result == expected
@pytest.mark.asyncio
async def test_async_function(self):
# Arrange
client = MyClient()
# Act
with patch("httpx.AsyncClient") as mock:
mock_instance = AsyncMock()
mock.return_value = mock_instance
result = await client.fetch_data()
# Assert
assert result is not None
```
## Test-Kategorien
### 1. Unit Tests (Höchste Priorität)
- Testen einzelne Funktionen/Methoden
- Keine externen Abhängigkeiten (Mocks verwenden)
- Schnell ausführbar
### 2. Integration Tests
- Testen Zusammenspiel mehrerer Komponenten
- Können echte DB verwenden (Test-DB)
### 3. Security Tests
- Auth/JWT Validierung
- Passwort-Hashing
- Berechtigungsprüfung
## Checkliste vor Abschluss
Vor dem Abschluss einer Aufgabe:
- [ ] Gibt es Tests für alle neuen Funktionen?
- [ ] Gibt es Tests für alle Edge Cases?
- [ ] Gibt es Tests für Fehlerfälle?
- [ ] Laufen alle bestehenden Tests noch? (`go test ./...` / `pytest`)
- [ ] Ist die Test-Coverage angemessen?
## Tests ausführen
```bash
# Go - Alle Tests
cd consent-service && go test -v ./...
# Go - Mit Coverage
cd consent-service && go test -cover ./...
# Python - Alle Tests
cd backend && source venv/bin/activate && pytest -v
# Python - Mit Coverage
cd backend && pytest --cov=. --cov-report=html
```
## Beispiel: Vollständiger Test-Workflow
Wenn du z.B. eine neue `GetUserStats()` Funktion im Go Service hinzufügst:
1. **Funktion schreiben** in `internal/services/stats_service.go`
2. **Test erstellen** in `internal/services/stats_service_test.go`:
```go
func TestGetUserStats_ValidUser_ReturnsStats(t *testing.T) {...}
func TestGetUserStats_InvalidUser_ReturnsError(t *testing.T) {...}
func TestGetUserStats_NoConsents_ReturnsEmptyStats(t *testing.T) {...}
```
3. **Tests ausführen**: `go test -v ./internal/services/...`
4. **Dokumentation aktualisieren** (siehe documentation.md)

View File

@@ -0,0 +1,205 @@
# Vokabel-Arbeitsblatt Generator - Entwicklerdokumentation
**Status:** Produktiv
**Letzte Aktualisierung:** 2026-02-08
**URL:** https://macmini/vocab-worksheet
---
## Uebersicht
Der Vokabel-Arbeitsblatt Generator ermoeglicht Lehrern:
- Schulbuchseiten (PDF/Bild) zu scannen
- Vokabeln automatisch per OCR zu extrahieren
- Druckfertige Arbeitsblaetter in verschiedenen Formaten zu generieren
---
## Architektur
```
Browser (studio-v2) klausur-service (Port 8086) PostgreSQL
│ │ │
│ POST /upload-pdf-info │ │
│ POST /process-single-page │ │
│ POST /generate │ │
│ POST /generate-nru │ ──── vocab_sessions ──────▶│
│ GET /worksheets/{id}/pdf │ ──── vocab_entries ───────▶│
│ │ ──── vocab_worksheets ────▶│
└────────────────────────────┘ │
```
---
## Arbeitsblatt-Formate
### Standard-Format
Klassisches Arbeitsblatt mit waehlbaren Uebungstypen:
- **Englisch → Deutsch**: Englische Woerter uebersetzen
- **Deutsch → Englisch**: Deutsche Woerter uebersetzen
- **Abschreibuebung**: Woerter mehrfach schreiben
- **Lueckensaetze**: Saetze mit Luecken ausfuellen
### NRU-Format (Neu: 2026-02-08)
Spezielles Format fuer strukturiertes Vokabellernen:
**Seite 1 (pro gescannter Seite): Vokabeltabelle**
| Englisch | Deutsch | Korrektur |
|----------|---------|-----------|
| word | (leer) | (leer) |
- Kind schreibt deutsche Uebersetzung
- Eltern korrigieren, Kind schreibt ggf. korrigierte Version
**Seite 2 (pro gescannter Seite): Lernsaetze**
| Deutscher Satz |
|-----------------------------------|
| (2 leere Zeilen fuer EN-Uebersetzung) |
- Deutscher Satz vorgegeben
- Kind schreibt englische Uebersetzung
**Automatische Trennung:**
- Einzelwoerter/Phrasen → Vokabeltabelle
- Saetze (enden mit `.!?` oder > 50 Zeichen) → Lernsaetze
---
## API-Endpoints
### Standard-Format
```
POST /api/v1/vocab/sessions/{session_id}/generate
Body: {
"worksheet_types": ["en_to_de", "de_to_en", "copy", "gap_fill"],
"title": "Vokabeln Unit 3",
"include_solutions": true,
"line_height": "normal" | "large" | "extra-large"
}
Response: { "id": "worksheet-uuid", ... }
```
### NRU-Format
```
POST /api/v1/vocab/sessions/{session_id}/generate-nru
Body: {
"title": "Vokabeltest",
"include_solutions": true,
"specific_pages": [1, 2] // optional, 1-indexed
}
Response: {
"worksheet_id": "uuid",
"statistics": {
"total_entries": 96,
"vocabulary_count": 75,
"sentence_count": 21,
"source_pages": [1, 2, 3],
"worksheet_pages": 6
},
"download_url": "/api/v1/vocab/worksheets/{id}/pdf",
"solution_url": "/api/v1/vocab/worksheets/{id}/solution"
}
```
### PDF-Download
```
GET /api/v1/vocab/worksheets/{worksheet_id}/pdf
GET /api/v1/vocab/worksheets/{worksheet_id}/solution
```
---
## Dateien
### Backend (klausur-service)
| Datei | Beschreibung |
|-------|--------------|
| `vocab_worksheet_api.py` | Haupt-API Router mit allen Endpoints |
| `nru_worksheet_generator.py` | NRU-Format HTML/PDF Generator |
| `vocab_session_store.py` | PostgreSQL Datenbankoperationen |
| `hybrid_vocab_extractor.py` | OCR-Extraktion (PaddleOCR + LLM) |
| `tesseract_vocab_extractor.py` | Tesseract OCR Fallback |
### Frontend (studio-v2)
| Datei | Beschreibung |
|-------|--------------|
| `app/vocab-worksheet/page.tsx` | Haupt-UI mit Template-Auswahl |
---
## Datenbank-Schema
```sql
-- Sessions
CREATE TABLE vocab_sessions (
id UUID PRIMARY KEY,
name VARCHAR(255),
status VARCHAR(50),
vocabulary_count INT,
source_language VARCHAR(10),
target_language VARCHAR(10),
created_at TIMESTAMP
);
-- Vokabeln
CREATE TABLE vocab_entries (
id UUID PRIMARY KEY,
session_id UUID REFERENCES vocab_sessions(id),
english TEXT,
german TEXT,
example_sentence TEXT,
source_page INT,
source_row INT,
source_column INT
);
-- Generierte Arbeitsblaetter
CREATE TABLE vocab_worksheets (
id UUID PRIMARY KEY,
session_id UUID REFERENCES vocab_sessions(id),
worksheet_types JSONB,
pdf_path VARCHAR(500),
solution_path VARCHAR(500),
generated_at TIMESTAMP
);
```
---
## Deployment
```bash
# 1. Backend synchronisieren
rsync -avz klausur-service/backend/ macmini:.../klausur-service/backend/
# 2. Frontend synchronisieren
rsync -avz studio-v2/app/vocab-worksheet/ macmini:.../studio-v2/app/vocab-worksheet/
# 3. Container neu bauen
ssh macmini "docker compose build --no-cache klausur-service studio-v2"
# 4. Container starten
ssh macmini "docker compose up -d klausur-service studio-v2"
```
---
## Erweiterung: Neue Formate hinzufuegen
1. **Backend**: Neuen Generator in `klausur-service/backend/` erstellen
2. **API**: Neuen Endpoint in `vocab_worksheet_api.py` hinzufuegen
3. **Frontend**: Format zu `worksheetFormats` Array in `page.tsx` hinzufuegen
4. **Doku**: Diese Datei aktualisieren
---
## Aenderungshistorie
| Datum | Aenderung |
|-------|-----------|
| 2026-02-08 | NRU-Format und Template-Auswahl hinzugefuegt |
| 2026-02-07 | Initiale Implementierung mit Standard-Format |

View File

@@ -0,0 +1,117 @@
# Session Status - 25. Januar 2026 (Aktualisiert)
## Zusammenfassung
Open Data School Import erfolgreich implementiert. Schulbestand von 17,610 auf 30,355 erhoeht.
---
## Erledigte Aufgaben
### 1. Studio-v2 Build-Fehler (Vorherige Session)
- **Status:** Erledigt
- **Problem:** `Module not found: Can't resolve 'pdf-lib'`
- **Loesung:** Falsches package.json auf macmini ersetzt, rsync mit --delete
### 2. Open Data School Importer
- **Status:** Erledigt
- **Datei:** `/edu-search-service/scripts/import_open_data.py`
- **Erfolgreich importiert:**
- **NRW:** 5,637 Schulen (CSV von schulministerium.nrw.de)
- **Berlin:** 930 Schulen (WFS/GeoJSON von gdi.berlin.de)
- **Hamburg:** 543 Schulen (WFS/GML von geodienste.hamburg.de)
---
## Aktuelle Schulstatistiken
```
Total: 30,355 Schulen
Nach Bundesland:
NW: 14,962 (inkl. Open Data Import)
BY: 2,803
NI: 2,192
BE: 1,475 (inkl. WFS Import)
SN: 1,425
SH: 1,329
HE: 1,290
RP: 1,066
HH: 902 (inkl. WFS Import)
TH: 799
BB: 562
SL: 533
MV: 367
ST: 250
BW: 200 (nur JedeSchule.de - BW Daten kostenpflichtig!)
HB: 200
```
---
## Open Data Importer - Verfuegbare Quellen
| Bundesland | Status | Quelle | Format |
|------------|--------|--------|--------|
| NW | Funktioniert | schulministerium.nrw.de | CSV |
| BE | Funktioniert | gdi.berlin.de | WFS/GeoJSON |
| HH | Funktioniert | geodienste.hamburg.de | WFS/GML |
| SN | 404 Error | schuldatenbank.sachsen.de | API |
| BW | Kostenpflichtig | LOBW | - |
| BY | Kein Open Data | - | - |
---
## Importer-Nutzung
```bash
# Alle verfuegbaren Quellen importieren
cd /Users/benjaminadmin/Projekte/breakpilot-pwa/edu-search-service/scripts
python3 import_open_data.py --all --url http://macmini:8088
# Einzelnes Bundesland (Dry-Run)
python3 import_open_data.py --state NW --dry-run
# Mit Server-URL
python3 import_open_data.py --state HH --url http://macmini:8088
```
---
## Offene Punkte
### Bundeslaender ohne Open Data
- **BW:** Schuldaten muessen GEKAUFT werden (LOBW)
- **BY:** Keine Open Data API gefunden
- **NI, HE, RP, etc.:** Keine zentralen Open Data Quellen bekannt
### Moegliche weitere Quellen
- OSM (OpenStreetMap) - amenity=school
- Statistisches Bundesamt
- Lokale Schultraeger-Verzeichnisse
---
## Container-Status auf macmini
| Container | Port | Status |
|-----------|------|--------|
| website | 3000 | Laeuft |
| studio-v2 | 3001 | Laeuft |
| edu-search-service | 8088 | Laeuft |
---
## Wichtige URLs
- School Directory: http://macmini:3000/admin/school-directory
- School Stats API: http://macmini:8088/api/v1/schools/stats
- School Search API: http://macmini:8088/api/v1/schools?q=NAME
---
## Naechste moegliche Schritte
1. **OSM Import testen** - OpenStreetMap hat Schuldaten (amenity=school)
2. **Weitere WFS-Quellen suchen** - Andere Bundeslaender koennten Geo-Portale haben
3. **Deduplizierung** - Pruefen ob durch multiple Imports Duplikate entstanden sind

36
.claude/settings.json Normal file
View File

@@ -0,0 +1,36 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "python3 ~/.claude/hooks/breakpilot-post-edit.py",
"timeout": 15000
}
]
}
],
"Stop": [
{
"hooks": [
{
"type": "prompt",
"prompt": "Überprüfe ob bei dieser Aufgabe:\n1. Dokumentation aktualisiert werden muss (neue API, neue Funktion, Architektur-Änderung)\n2. Tests geschrieben/aktualisiert werden müssen (neue Funktion, Bug-Fix, Code-Änderung)\n3. Ein ADR (Architecture Decision Record) erstellt werden sollte (neues Modul, Technologiewechsel, signifikante Architektur-Entscheidung)\n\nWenn etwas fehlt, antworte mit {\"decision\": \"block\", \"reason\": \"Fehlend: [Details]\"}\nWenn alles erledigt ist, antworte mit {\"decision\": \"approve\", \"reason\": \"Alle Dokumentation, Tests und ADRs sind aktuell\"}",
"timeout": 30000
}
]
}
]
},
"permissions": {
"allow": [
"Read(docs/**)",
"Read(.claude/**)",
"Read(backend/tests/**)",
"Read(consent-service/**/*_test.go)",
"Write(docs/adr/**)"
]
}
}

View File

@@ -0,0 +1,82 @@
{
"permissions": {
"allow": [
"Bash(textutil -convert txt:*)",
"Bash(find:*)",
"Bash(grep:*)",
"Bash(wc:*)",
"Bash(/bin/bash -c \"source venv/bin/activate && pip install pyjwt --quiet 2>/dev/null && python -c \"\"import sys; sys.path.insert(0, ''.''); from llm_gateway.models.chat import ChatMessage; print(''Models import OK'')\"\"\")",
"Bash(/Users/benjaminadmin/Projekte/breakpilot-pwa/backend/venv/bin/python:*)",
"Bash(./venv/bin/pip install:*)",
"Bash(brew install:*)",
"Bash(brew services start:*)",
"Bash(ollama list:*)",
"Bash(ollama pull:*)",
"Bash(export LLM_GATEWAY_ENABLED=true)",
"Bash(export LLM_GATEWAY_DEBUG=true)",
"Bash(export LLM_API_KEYS=test-key-123)",
"Bash(export ANTHROPIC_API_KEY=\"$ANTHROPIC_API_KEY\")",
"Bash(source:*)",
"Bash(pytest:*)",
"Bash(./venv/bin/pytest:*)",
"Bash(python3 -m pytest:*)",
"Bash(export TAVILY_API_KEY=\"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\")",
"Bash(python3:*)",
"Bash(curl:*)",
"Bash(pip3 install:*)",
"WebSearch",
"Bash(export ALERTS_AGENT_ENABLED=true)",
"Bash(export LLM_API_KEYS=test-key)",
"WebFetch(domain:docs.vast.ai)",
"Bash(docker compose:*)",
"Bash(docker ps:*)",
"Bash(docker inspect:*)",
"Bash(docker logs:*)",
"Bash(ls:*)",
"Bash(docker exec:*)",
"WebFetch(domain:www.librechat.ai)",
"Bash(export TAVILY_API_KEY=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)",
"Bash(/Users/benjaminadmin/Projekte/breakpilot-pwa/backend/venv/bin/pip install:*)",
"Bash(/Users/benjaminadmin/Projekte/breakpilot-pwa/backend/venv/bin/pytest -v tests/test_integration/test_librechat_tavily.py -x)",
"WebFetch(domain:vast.ai)",
"Bash(/Users/benjaminadmin/Projekte/breakpilot-pwa/backend/venv/bin/pytest tests/test_infra/test_vast_client.py tests/test_infra/test_vast_power.py -v --tb=short)",
"Bash(go build:*)",
"Bash(go test:*)",
"Bash(npm install)",
"Bash(/usr/local/bin/node:*)",
"Bash(/opt/homebrew/bin/node --version)",
"Bash(docker --version:*)",
"Bash(docker build:*)",
"Bash(docker images:*)",
"Bash(/Users/benjaminadmin/Projekte/breakpilot-pwa/backend/venv/bin/pytest:*)",
"Bash(npm test:*)",
"Bash(/opt/homebrew/bin/node /opt/homebrew/bin/npm test -- --passWithNoTests)",
"Bash(/usr/libexec/java_home:*)",
"Bash(/opt/homebrew/bin/node:*)",
"Bash(docker restart:*)",
"Bash(tree:*)",
"Bash(go mod tidy:*)",
"Bash(go mod vendor:*)",
"Bash(python -m pytest:*)",
"Bash(lsof:*)",
"Bash(python scripts/load_initial_seeds.py:*)",
"Bash(python:*)",
"Bash(docker cp:*)",
"Bash(node --check:*)",
"Bash(cat:*)",
"Bash(DATABASE_URL='postgresql://breakpilot:breakpilot123@localhost:5432/breakpilot_db' python3:*)",
"Bash(docker volume:*)",
"Bash(docker stop:*)",
"Bash(docker rm:*)",
"Bash(docker run:*)",
"Bash(docker network:*)",
"Bash(breakpilot-edu-search:latest)",
"Bash(jq:*)",
"Bash(docker port:*)",
"Bash(/dev/null curl -X POST http://localhost:8086/v1/crawl/queue -H 'Authorization: Bearer dev-key' -H 'Content-Type: application/json' -d '{\"\"\"\"university_id\"\"\"\": \"\"\"\"783333a1-91a3-4015-9299-45d10537dae4\"\"\"\", \"\"\"\"priority\"\"\"\": 10}')",
"Bash(1)",
"WebFetch(domain:uol.de)",
"Bash(xargs:*)"
]
}
}

View File

@@ -0,0 +1,237 @@
# Systematisches Debug-Framework
## Trigger
Dieses Skill aktivieren bei:
- Fehlermeldungen / Exceptions
- Unerwartetes Verhalten
- Performance-Probleme
- "Es funktioniert nicht"
---
## Phase 1: Reproduzieren (5 min max)
### Ziel: Bug in einen Test verwandeln
```bash
# 1. Exakte Schritte dokumentieren
# 2. Fehlermeldung/Symptom notieren
# 3. Umgebung identifizieren
```
**Fragen:**
- [ ] Ist der Bug reproduzierbar?
- [ ] Tritt er nur in bestimmten Umgebungen auf?
- [ ] Seit wann tritt er auf? (letzter Deploy?)
### Breakpilot-spezifisch: Welcher Service?
```bash
# Container-Status prüfen
ssh macmini "docker compose ps | grep -E '(Exit|unhealthy)'"
# Logs der letzten 5 Minuten
ssh macmini "docker compose logs --since 5m <service-name>"
```
| Symptom | Wahrscheinlicher Service |
|---------|-------------------------|
| Login fehlgeschlagen | consent-service, backend |
| 502 Bad Gateway | nginx, upstream-service |
| Langsame Suche | qdrant, embedding-service |
| Upload-Fehler | minio, backend |
| OCR-Fehler | paddleocr-service, klausur-service |
---
## Phase 2: Hypothesen bilden (5 min max)
### 3-5 mögliche Ursachen auflisten
| # | Hypothese | Wahrscheinlichkeit | Test |
|---|-----------|-------------------|------|
| 1 | | Hoch/Mittel/Niedrig | |
| 2 | | | |
| 3 | | | |
### Häufige Ursachen bei Breakpilot
**Container/Docker:**
- Container nicht gestartet
- Volume-Mount-Problem
- Netzwerk zwischen Containern unterbrochen
- Resource-Limits erreicht
**Datenbank:**
- Connection Pool erschöpft
- Migration nicht ausgeführt
- Deadlock
**API/Backend:**
- JWT abgelaufen
- CORS-Fehler
- Rate-Limit erreicht
- Falscher Content-Type
**Frontend:**
- Cache-Problem (Safari!)
- Build nicht aktualisiert
- Umgebungsvariable fehlt
---
## Phase 3: Systematisch eliminieren (10 min max)
### Reihenfolge: Schnellste Tests zuerst
```bash
# 1. Ist der Service überhaupt erreichbar?
curl -s https://macmini:8000/health | jq
# 2. Container-Logs auf Fehler prüfen
ssh macmini "docker compose logs --tail 50 <service> 2>&1 | grep -iE '(error|exception|failed|traceback)'"
# 3. Datenbank-Verbindung testen
ssh macmini "docker exec breakpilot-pwa-postgres pg_isready"
# 4. Redis/Valkey erreichbar?
ssh macmini "docker exec breakpilot-pwa-valkey valkey-cli ping"
```
### Hypothese testen
Für jede Hypothese:
1. **Test definieren** (wie prüfen wir das?)
2. **Test ausführen**
3. **Ergebnis:** Bestätigt ✅ / Widerlegt ❌
---
## Phase 4: Root Cause identifizieren
### Nicht das Symptom behandeln!
**Schlecht:** "Ich starte den Container neu"
**Gut:** "Der Container crashed wegen OOM → Memory-Limit erhöhen"
### 5-Why-Methode
```
Problem: API gibt 500 zurück
↓ Warum?
Datenbank-Query failed
↓ Warum?
Connection Pool erschöpft
↓ Warum?
Connections werden nicht freigegeben
↓ Warum?
Exception vor connection.close()
↓ Warum?
Fehlendes try/finally
→ ROOT CAUSE: Fehlendes Resource-Cleanup
```
---
## Phase 5: Fix implementieren
### Checkliste vor dem Fix
- [ ] Root Cause verstanden (nicht nur Symptom)
- [ ] Fix adressiert Root Cause
- [ ] Keine Seiteneffekte erwartet
- [ ] Test geschrieben, der Regression verhindert
### Fix-Template
```python
# VORHER: Bug
def fetch_data():
conn = get_connection()
result = conn.query(...) # Exception hier → Leak!
conn.close()
return result
# NACHHER: Fix mit Erklärung
def fetch_data():
"""Fetch data with proper connection handling.
Fixed: Connection leak when query raises exception.
See: https://macmini:3003/.../issues/123
"""
conn = get_connection()
try:
result = conn.query(...)
return result
finally:
conn.close() # Immer ausgeführt
```
---
## Phase 6: Regression verhindern
### Test schreiben
```python
def test_connection_released_on_error():
"""Regression test for connection leak bug.
Issue: #123
Root cause: Missing finally block in fetch_data()
"""
initial_connections = get_pool_size()
with pytest.raises(DatabaseError):
fetch_data_with_bad_query()
# Connection should be returned to pool
assert get_pool_size() == initial_connections
```
### Dokumentieren
```bash
# Was haben wir gelernt?
# → docs-src/development/debugging-notes.md ergänzen
```
---
## Quick Reference: Breakpilot Debug Commands
```bash
# Alle Container-Status
ssh macmini "docker compose ps"
# Logs eines Services (live)
ssh macmini "docker compose logs -f <service>"
# In Container einloggen
ssh macmini "docker exec -it breakpilot-pwa-<service> sh"
# PostgreSQL Query
ssh macmini "docker exec breakpilot-pwa-postgres psql -U breakpilot -d breakpilot_db -c 'SELECT 1'"
# Netzwerk-Debug
ssh macmini "docker exec breakpilot-pwa-backend curl -s http://consent-service:8081/health"
# Resource-Nutzung
ssh macmini "docker stats --no-stream"
# Vault-Status
ssh macmini "docker exec breakpilot-pwa-vault vault status"
```
---
## Anti-Patterns vermeiden
| ❌ Nicht machen | ✅ Stattdessen |
|-----------------|----------------|
| Random Code ändern | Hypothese bilden, dann testen |
| console.log überall | Gezielt an verdächtigen Stellen |
| Container neustarten und hoffen | Root Cause finden |
| Stundenlang alleine debuggen | Nach 30 min Hilfe holen |
| Fix ohne Test | Immer Regression-Test schreiben |

31
.docker/build-ci-images.sh Executable file
View File

@@ -0,0 +1,31 @@
#!/bin/bash
# Build CI Docker Images for BreakPilot
# Run this script on the Mac Mini to build the custom CI images
set -e
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
PROJECT_DIR="$(dirname "$SCRIPT_DIR")"
echo "=== Building BreakPilot CI Images ==="
echo "Project directory: $PROJECT_DIR"
cd "$PROJECT_DIR"
# Build Python CI image with WeasyPrint
echo ""
echo "Building breakpilot/python-ci:3.12 ..."
docker build \
-t breakpilot/python-ci:3.12 \
-t breakpilot/python-ci:latest \
-f .docker/python-ci.Dockerfile \
.
echo ""
echo "=== Build complete ==="
echo ""
echo "Images built:"
docker images | grep breakpilot/python-ci
echo ""
echo "To use in Woodpecker CI, the image is already configured in .woodpecker/main.yml"

View File

@@ -0,0 +1,51 @@
# Custom Python CI Image with WeasyPrint Dependencies
# Build: docker build -t breakpilot/python-ci:3.12 -f .docker/python-ci.Dockerfile .
#
# This image includes all system libraries needed for:
# - WeasyPrint (PDF generation)
# - psycopg2 (PostgreSQL)
# - General Python testing
FROM python:3.12-slim
LABEL maintainer="BreakPilot Team"
LABEL description="Python 3.12 with WeasyPrint and test dependencies for CI"
# Install system dependencies in a single layer
RUN apt-get update && apt-get install -y --no-install-recommends \
# WeasyPrint dependencies
libpango-1.0-0 \
libpangocairo-1.0-0 \
libpangoft2-1.0-0 \
libgdk-pixbuf-2.0-0 \
libffi-dev \
libcairo2 \
libcairo2-dev \
libgirepository1.0-dev \
gir1.2-pango-1.0 \
# PostgreSQL client (for psycopg2)
libpq-dev \
# Build tools (for some pip packages)
gcc \
g++ \
# Useful utilities
curl \
git \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Pre-install commonly used Python packages for faster CI
RUN pip install --no-cache-dir \
pytest \
pytest-cov \
pytest-asyncio \
pytest-json-report \
psycopg2-binary \
weasyprint \
httpx
# Set working directory
WORKDIR /app
# Default command
CMD ["python", "--version"]

115
.env.dev Normal file
View File

@@ -0,0 +1,115 @@
# ============================================
# BreakPilot PWA - DEVELOPMENT Environment
# ============================================
# Usage: cp .env.dev .env
# Or: ./scripts/env-switch.sh dev
# ============================================
# ============================================
# Environment Identifier
# ============================================
ENVIRONMENT=development
COMPOSE_PROJECT_NAME=breakpilot-dev
# ============================================
# HashiCorp Vault (Secrets Management)
# ============================================
# In development, use the local Vault instance with dev token
VAULT_ADDR=http://localhost:8200
VAULT_DEV_TOKEN=breakpilot-dev-token
# ============================================
# Database
# ============================================
POSTGRES_USER=breakpilot
POSTGRES_PASSWORD=breakpilot_dev_123
POSTGRES_DB=breakpilot_dev
DATABASE_URL=postgres://breakpilot:breakpilot_dev_123@postgres:5432/breakpilot_dev?sslmode=disable
# Synapse DB (Matrix)
SYNAPSE_DB_PASSWORD=synapse_dev_123
# ============================================
# Authentication
# ============================================
# Development only - NOT for production!
JWT_SECRET=dev-jwt-secret-not-for-production-32chars
JWT_REFRESH_SECRET=dev-refresh-secret-32chars-change-me
# ============================================
# Service URLs (Development)
# ============================================
FRONTEND_URL=http://localhost:8000
BACKEND_URL=http://localhost:8000
CONSENT_SERVICE_URL=http://localhost:8081
BILLING_SERVICE_URL=http://localhost:8083
SCHOOL_SERVICE_URL=http://localhost:8084
KLAUSUR_SERVICE_URL=http://localhost:8086
WEBSITE_URL=http://localhost:3000
# ============================================
# E-Mail (Mailpit for Development)
# ============================================
# Mailpit catches all emails - view at http://localhost:8025
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_NAME=BreakPilot Dev
SMTP_FROM_ADDR=dev@breakpilot.local
# ============================================
# MinIO (Object Storage)
# ============================================
MINIO_ROOT_USER=breakpilot_dev
MINIO_ROOT_PASSWORD=breakpilot_dev_123
MINIO_ENDPOINT=localhost:9000
# ============================================
# Qdrant (Vector DB)
# ============================================
QDRANT_URL=http://localhost:6333
# ============================================
# API Keys (Optional for Dev)
# ============================================
# Leave empty for offline development
# Or add your test keys here
ANTHROPIC_API_KEY=
ANTHROPIC_DEFAULT_MODEL=claude-sonnet-4-20250514
ANTHROPIC_ENABLED=false
VAST_API_KEY=
VAST_INSTANCE_ID=
CONTROL_API_KEY=
VAST_AUTO_SHUTDOWN=true
VAST_AUTO_SHUTDOWN_MINUTES=30
VLLM_BASE_URL=
VLLM_ENABLED=false
# ============================================
# Embedding Configuration
# ============================================
# "local" = sentence-transformers (no API key needed)
# "openai" = OpenAI API (requires OPENAI_API_KEY)
EMBEDDING_BACKEND=local
# ============================================
# Stripe (Billing - Test Mode)
# ============================================
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=
# ============================================
# Debug Settings
# ============================================
DEBUG=true
GIN_MODE=debug
LOG_LEVEL=debug
# ============================================
# Jitsi (Video Conferencing)
# ============================================
JITSI_PUBLIC_URL=http://localhost:8443

124
.env.example Normal file
View File

@@ -0,0 +1,124 @@
# BreakPilot PWA - Environment Configuration
# Kopieren Sie diese Datei nach .env und passen Sie die Werte an
# ================================================
# Allgemein
# ================================================
ENVIRONMENT=development
# ENVIRONMENT=production
# ================================================
# Sicherheit
# ================================================
# WICHTIG: In Produktion sichere Schluessel verwenden!
# Generieren mit: openssl rand -hex 32
JWT_SECRET=CHANGE_ME_RUN_openssl_rand_hex_32
JWT_REFRESH_SECRET=CHANGE_ME_RUN_openssl_rand_hex_32
# ================================================
# Keycloak (Optional - fuer Produktion empfohlen)
# ================================================
# Wenn Keycloak konfiguriert ist, wird es fuer Authentifizierung verwendet.
# Ohne Keycloak wird lokales JWT verwendet (gut fuer Entwicklung).
#
# KEYCLOAK_SERVER_URL=https://keycloak.breakpilot.app
# KEYCLOAK_REALM=breakpilot
# KEYCLOAK_CLIENT_ID=breakpilot-backend
# KEYCLOAK_CLIENT_SECRET=your-client-secret
# KEYCLOAK_VERIFY_SSL=true
# ================================================
# E-Mail Konfiguration
# ================================================
# === ENTWICKLUNG (Mailpit - Standardwerte) ===
# Mailpit fängt alle E-Mails ab und zeigt sie unter http://localhost:8025
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_NAME=BreakPilot
SMTP_FROM_ADDR=noreply@breakpilot.app
FRONTEND_URL=http://localhost:8000
# === PRODUKTION (Beispiel für verschiedene Provider) ===
# --- Option 1: Eigener Mailserver ---
# SMTP_HOST=mail.ihredomain.de
# SMTP_PORT=587
# SMTP_USERNAME=noreply@ihredomain.de
# SMTP_PASSWORD=ihr-sicheres-passwort
# SMTP_FROM_NAME=BreakPilot
# SMTP_FROM_ADDR=noreply@ihredomain.de
# FRONTEND_URL=https://app.ihredomain.de
# --- Option 2: SendGrid ---
# SMTP_HOST=smtp.sendgrid.net
# SMTP_PORT=587
# SMTP_USERNAME=apikey
# SMTP_PASSWORD=SG.xxxxxxxxxxxxxxxxxxxxx
# SMTP_FROM_NAME=BreakPilot
# SMTP_FROM_ADDR=noreply@ihredomain.de
# --- Option 3: Mailgun ---
# SMTP_HOST=smtp.mailgun.org
# SMTP_PORT=587
# SMTP_USERNAME=postmaster@mg.ihredomain.de
# SMTP_PASSWORD=ihr-mailgun-passwort
# SMTP_FROM_NAME=BreakPilot
# SMTP_FROM_ADDR=noreply@mg.ihredomain.de
# --- Option 4: Amazon SES ---
# SMTP_HOST=email-smtp.eu-central-1.amazonaws.com
# SMTP_PORT=587
# SMTP_USERNAME=AKIAXXXXXXXXXXXXXXXX
# SMTP_PASSWORD=ihr-ses-secret
# SMTP_FROM_NAME=BreakPilot
# SMTP_FROM_ADDR=noreply@ihredomain.de
# ================================================
# Datenbank
# ================================================
POSTGRES_USER=breakpilot
POSTGRES_PASSWORD=breakpilot123
POSTGRES_DB=breakpilot_db
DATABASE_URL=postgres://breakpilot:breakpilot123@localhost:5432/breakpilot_db?sslmode=disable
# ================================================
# Optional: AI Integration
# ================================================
# ANTHROPIC_API_KEY=your-anthropic-api-key-here
# ================================================
# Breakpilot Drive - Lernspiel
# ================================================
# Aktiviert Datenbank-Speicherung fuer Spielsessions
GAME_USE_DATABASE=true
# LLM fuer Quiz-Fragen-Generierung (optional)
# Wenn nicht gesetzt, werden statische Fragen verwendet
GAME_LLM_MODEL=llama-3.1-8b
GAME_LLM_FALLBACK_MODEL=claude-3-haiku
# Feature Flags
GAME_REQUIRE_AUTH=false
GAME_REQUIRE_BILLING=false
GAME_ENABLE_LEADERBOARDS=true
# Task-Kosten fuer Billing (wenn aktiviert)
GAME_SESSION_TASK_COST=1.0
GAME_QUICK_SESSION_TASK_COST=0.5
# ================================================
# Woodpecker CI/CD
# ================================================
# URL zum Woodpecker Server
WOODPECKER_URL=http://woodpecker-server:8000
# API Token für Dashboard-Integration (Pipeline-Start)
# Erstellen unter: http://macmini:8090 → User Settings → Personal Access Tokens
WOODPECKER_TOKEN=
# ================================================
# Debug
# ================================================
DEBUG=false

113
.env.staging Normal file
View File

@@ -0,0 +1,113 @@
# ============================================
# BreakPilot PWA - STAGING Environment
# ============================================
# Usage: cp .env.staging .env
# Or: ./scripts/env-switch.sh staging
# ============================================
# ============================================
# Environment Identifier
# ============================================
ENVIRONMENT=staging
COMPOSE_PROJECT_NAME=breakpilot-staging
# ============================================
# HashiCorp Vault (Secrets Management)
# ============================================
# In staging, still use dev token but with staging secrets path
VAULT_ADDR=http://localhost:8200
VAULT_DEV_TOKEN=breakpilot-staging-token
# ============================================
# Database (Separate from Dev!)
# ============================================
POSTGRES_USER=breakpilot
POSTGRES_PASSWORD=staging_secure_password_change_this
POSTGRES_DB=breakpilot_staging
DATABASE_URL=postgres://breakpilot:staging_secure_password_change_this@postgres:5432/breakpilot_staging?sslmode=disable
# Synapse DB (Matrix)
SYNAPSE_DB_PASSWORD=synapse_staging_secure_123
# ============================================
# Authentication
# ============================================
# Staging secrets - more secure than dev, but not production
JWT_SECRET=staging-jwt-secret-32chars-change-me-now
JWT_REFRESH_SECRET=staging-refresh-secret-32chars-secure
# ============================================
# Service URLs (Staging - Different Ports)
# ============================================
FRONTEND_URL=http://localhost:8001
BACKEND_URL=http://localhost:8001
CONSENT_SERVICE_URL=http://localhost:8091
BILLING_SERVICE_URL=http://localhost:8093
SCHOOL_SERVICE_URL=http://localhost:8094
KLAUSUR_SERVICE_URL=http://localhost:8096
WEBSITE_URL=http://localhost:3001
# ============================================
# E-Mail (Still Mailpit for Safety)
# ============================================
# Mailpit catches all emails - no accidental sends to real users
SMTP_HOST=mailpit
SMTP_PORT=1025
SMTP_USERNAME=
SMTP_PASSWORD=
SMTP_FROM_NAME=BreakPilot Staging
SMTP_FROM_ADDR=staging@breakpilot.local
# ============================================
# MinIO (Object Storage)
# ============================================
MINIO_ROOT_USER=breakpilot_staging
MINIO_ROOT_PASSWORD=staging_minio_secure_123
MINIO_ENDPOINT=localhost:9002
# ============================================
# Qdrant (Vector DB)
# ============================================
QDRANT_URL=http://localhost:6335
# ============================================
# API Keys (Test Keys for Staging)
# ============================================
# Use test/sandbox API keys here
ANTHROPIC_API_KEY=
ANTHROPIC_DEFAULT_MODEL=claude-sonnet-4-20250514
ANTHROPIC_ENABLED=false
VAST_API_KEY=
VAST_INSTANCE_ID=
CONTROL_API_KEY=
VAST_AUTO_SHUTDOWN=true
VAST_AUTO_SHUTDOWN_MINUTES=30
VLLM_BASE_URL=
VLLM_ENABLED=false
# ============================================
# Embedding Configuration
# ============================================
EMBEDDING_BACKEND=local
# ============================================
# Stripe (Billing - Test Mode)
# ============================================
# Use Stripe TEST keys (sk_test_...)
STRIPE_SECRET_KEY=
STRIPE_PUBLISHABLE_KEY=
STRIPE_WEBHOOK_SECRET=
# ============================================
# Debug Settings (Reduced in Staging)
# ============================================
DEBUG=false
GIN_MODE=release
LOG_LEVEL=info
# ============================================
# Jitsi (Video Conferencing)
# ============================================
JITSI_PUBLIC_URL=http://localhost:8444

132
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,132 @@
# Dependabot Configuration for BreakPilot PWA
# This file configures Dependabot to automatically check for outdated dependencies
# and create pull requests to update them
version: 2
updates:
# Go dependencies (consent-service)
- package-ecosystem: "gomod"
directory: "/consent-service"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "go"
- "security"
commit-message:
prefix: "deps(go):"
groups:
go-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"
# Python dependencies (backend)
- package-ecosystem: "pip"
directory: "/backend"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "python"
- "security"
commit-message:
prefix: "deps(python):"
groups:
python-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"
# Node.js dependencies (website)
- package-ecosystem: "npm"
directory: "/website"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "javascript"
- "security"
commit-message:
prefix: "deps(npm):"
groups:
npm-minor:
patterns:
- "*"
update-types:
- "minor"
- "patch"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
open-pull-requests-limit: 5
labels:
- "dependencies"
- "github-actions"
commit-message:
prefix: "deps(actions):"
# Docker base images
- package-ecosystem: "docker"
directory: "/consent-service"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
labels:
- "dependencies"
- "docker"
- "security"
commit-message:
prefix: "deps(docker):"
- package-ecosystem: "docker"
directory: "/backend"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
labels:
- "dependencies"
- "docker"
- "security"
commit-message:
prefix: "deps(docker):"
- package-ecosystem: "docker"
directory: "/website"
schedule:
interval: "weekly"
day: "monday"
time: "06:00"
timezone: "Europe/Berlin"
labels:
- "dependencies"
- "docker"
- "security"
commit-message:
prefix: "deps(docker):"

503
.github/workflows/ci.yml vendored Normal file
View File

@@ -0,0 +1,503 @@
name: CI/CD Pipeline
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
env:
GO_VERSION: '1.21'
PYTHON_VERSION: '3.11'
NODE_VERSION: '20'
POSTGRES_USER: breakpilot
POSTGRES_PASSWORD: breakpilot123
POSTGRES_DB: breakpilot_test
REGISTRY: ghcr.io
IMAGE_PREFIX: ${{ github.repository_owner }}/breakpilot
jobs:
# ==========================================
# Go Consent Service Tests
# ==========================================
go-tests:
name: Go Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: ${{ env.POSTGRES_USER }}
POSTGRES_PASSWORD: ${{ env.POSTGRES_PASSWORD }}
POSTGRES_DB: ${{ env.POSTGRES_DB }}
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
cache-dependency-path: consent-service/go.sum
- name: Download dependencies
working-directory: ./consent-service
run: go mod download
- name: Run Go Vet
working-directory: ./consent-service
run: go vet ./...
- name: Run Unit Tests
working-directory: ./consent-service
run: go test -v -race -coverprofile=coverage.out ./...
env:
DATABASE_URL: postgres://${{ env.POSTGRES_USER }}:${{ env.POSTGRES_PASSWORD }}@localhost:5432/${{ env.POSTGRES_DB }}?sslmode=disable
JWT_SECRET: test-jwt-secret-for-ci
JWT_REFRESH_SECRET: test-refresh-secret-for-ci
- name: Check Coverage
working-directory: ./consent-service
run: |
go tool cover -func=coverage.out
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
echo "Total coverage: ${COVERAGE}%"
if (( $(echo "$COVERAGE < 50" | bc -l) )); then
echo "::warning::Coverage is below 50%"
fi
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./consent-service/coverage.out
flags: go
name: go-coverage
continue-on-error: true
# ==========================================
# Python Backend Tests
# ==========================================
python-tests:
name: Python Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
cache: 'pip'
cache-dependency-path: backend/requirements.txt
- name: Install dependencies
working-directory: ./backend
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov pytest-asyncio httpx
- name: Run Python Tests
working-directory: ./backend
run: pytest -v --cov=. --cov-report=xml --cov-report=term-missing
continue-on-error: true
- name: Upload coverage to Codecov
uses: codecov/codecov-action@v4
with:
files: ./backend/coverage.xml
flags: python
name: python-coverage
continue-on-error: true
# ==========================================
# Node.js Website Tests
# ==========================================
website-tests:
name: Website Tests
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: ${{ env.NODE_VERSION }}
cache: 'npm'
cache-dependency-path: website/package-lock.json
- name: Install dependencies
working-directory: ./website
run: npm ci
- name: Run TypeScript check
working-directory: ./website
run: npx tsc --noEmit
continue-on-error: true
- name: Run ESLint
working-directory: ./website
run: npm run lint
continue-on-error: true
- name: Build website
working-directory: ./website
run: npm run build
env:
NEXT_PUBLIC_BILLING_API_URL: http://localhost:8083
NEXT_PUBLIC_APP_URL: http://localhost:3000
# ==========================================
# Linting
# ==========================================
lint:
name: Linting
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: ${{ env.GO_VERSION }}
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v4
with:
version: latest
working-directory: ./consent-service
args: --timeout=5m
continue-on-error: true
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: ${{ env.PYTHON_VERSION }}
- name: Install Python linters
run: pip install flake8 black isort
- name: Run flake8
working-directory: ./backend
run: flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
continue-on-error: true
- name: Check Black formatting
working-directory: ./backend
run: black --check --diff .
continue-on-error: true
# ==========================================
# Security Scan
# ==========================================
security:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
exit-code: '0'
continue-on-error: true
- name: Run Go security check
uses: securego/gosec@master
with:
args: '-no-fail -fmt sarif -out results.sarif ./consent-service/...'
continue-on-error: true
# ==========================================
# Docker Build & Push
# ==========================================
docker-build:
name: Docker Build & Push
runs-on: ubuntu-latest
needs: [go-tests, python-tests, website-tests]
permissions:
contents: read
packages: write
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Extract metadata for consent-service
id: meta-consent
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-consent-service
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push consent-service
uses: docker/build-push-action@v5
with:
context: ./consent-service
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-consent.outputs.tags }}
labels: ${{ steps.meta-consent.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for backend
id: meta-backend
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push backend
uses: docker/build-push-action@v5
with:
context: ./backend
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-backend.outputs.tags }}
labels: ${{ steps.meta-backend.outputs.labels }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Extract metadata for website
id: meta-website
uses: docker/metadata-action@v5
with:
images: ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-website
tags: |
type=ref,event=branch
type=ref,event=pr
type=sha,prefix=
type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' }}
- name: Build and push website
uses: docker/build-push-action@v5
with:
context: ./website
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.meta-website.outputs.tags }}
labels: ${{ steps.meta-website.outputs.labels }}
build-args: |
NEXT_PUBLIC_BILLING_API_URL=${{ vars.NEXT_PUBLIC_BILLING_API_URL || 'http://localhost:8083' }}
NEXT_PUBLIC_APP_URL=${{ vars.NEXT_PUBLIC_APP_URL || 'http://localhost:3000' }}
cache-from: type=gha
cache-to: type=gha,mode=max
# ==========================================
# Integration Tests
# ==========================================
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
needs: [docker-build]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Start services with Docker Compose
run: |
docker compose up -d postgres mailpit
sleep 10
- name: Run consent-service
working-directory: ./consent-service
run: |
go build -o consent-service ./cmd/server
./consent-service &
sleep 5
env:
DATABASE_URL: postgres://breakpilot:breakpilot123@localhost:5432/breakpilot_db?sslmode=disable
JWT_SECRET: test-jwt-secret
JWT_REFRESH_SECRET: test-refresh-secret
SMTP_HOST: localhost
SMTP_PORT: 1025
- name: Health Check
run: |
curl -f http://localhost:8081/health || exit 1
- name: Run Integration Tests
run: |
# Test Auth endpoints
curl -s http://localhost:8081/api/v1/auth/health
# Test Document endpoints
curl -s http://localhost:8081/api/v1/documents
continue-on-error: true
- name: Stop services
if: always()
run: docker compose down
# ==========================================
# Deploy to Staging
# ==========================================
deploy-staging:
name: Deploy to Staging
runs-on: ubuntu-latest
needs: [docker-build, integration-tests]
if: github.ref == 'refs/heads/develop' && github.event_name == 'push'
environment:
name: staging
url: https://staging.breakpilot.app
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to staging server
env:
STAGING_HOST: ${{ secrets.STAGING_HOST }}
STAGING_USER: ${{ secrets.STAGING_USER }}
STAGING_SSH_KEY: ${{ secrets.STAGING_SSH_KEY }}
run: |
# This is a placeholder for actual deployment
# Configure based on your staging infrastructure
echo "Deploying to staging environment..."
echo "Images to deploy:"
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-consent-service:develop"
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend:develop"
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-website:develop"
# Example: SSH deployment (uncomment when configured)
# mkdir -p ~/.ssh
# echo "$STAGING_SSH_KEY" > ~/.ssh/id_rsa
# chmod 600 ~/.ssh/id_rsa
# ssh -o StrictHostKeyChecking=no $STAGING_USER@$STAGING_HOST "cd /opt/breakpilot && docker compose pull && docker compose up -d"
- name: Notify deployment
run: |
echo "## Staging Deployment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Successfully deployed to staging environment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Deployed images:**" >> $GITHUB_STEP_SUMMARY
echo "- consent-service: \`develop\`" >> $GITHUB_STEP_SUMMARY
echo "- backend: \`develop\`" >> $GITHUB_STEP_SUMMARY
echo "- website: \`develop\`" >> $GITHUB_STEP_SUMMARY
# ==========================================
# Deploy to Production
# ==========================================
deploy-production:
name: Deploy to Production
runs-on: ubuntu-latest
needs: [docker-build, integration-tests]
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
environment:
name: production
url: https://breakpilot.app
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Deploy to production server
env:
PROD_HOST: ${{ secrets.PROD_HOST }}
PROD_USER: ${{ secrets.PROD_USER }}
PROD_SSH_KEY: ${{ secrets.PROD_SSH_KEY }}
run: |
# This is a placeholder for actual deployment
# Configure based on your production infrastructure
echo "Deploying to production environment..."
echo "Images to deploy:"
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-consent-service:latest"
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-backend:latest"
echo " - ${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-website:latest"
# Example: SSH deployment (uncomment when configured)
# mkdir -p ~/.ssh
# echo "$PROD_SSH_KEY" > ~/.ssh/id_rsa
# chmod 600 ~/.ssh/id_rsa
# ssh -o StrictHostKeyChecking=no $PROD_USER@$PROD_HOST "cd /opt/breakpilot && docker compose pull && docker compose up -d"
- name: Notify deployment
run: |
echo "## Production Deployment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "Successfully deployed to production environment" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Deployed images:**" >> $GITHUB_STEP_SUMMARY
echo "- consent-service: \`latest\`" >> $GITHUB_STEP_SUMMARY
echo "- backend: \`latest\`" >> $GITHUB_STEP_SUMMARY
echo "- website: \`latest\`" >> $GITHUB_STEP_SUMMARY
# ==========================================
# Summary
# ==========================================
summary:
name: CI Summary
runs-on: ubuntu-latest
needs: [go-tests, python-tests, website-tests, lint, security, docker-build, integration-tests]
if: always()
steps:
- name: Check job results
run: |
echo "## CI/CD Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Job | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Go Tests | ${{ needs.go-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Python Tests | ${{ needs.python-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Website Tests | ${{ needs.website-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Linting | ${{ needs.lint.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Security | ${{ needs.security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker Build | ${{ needs.docker-build.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Integration Tests | ${{ needs.integration-tests.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Docker Images" >> $GITHUB_STEP_SUMMARY
echo "Images are pushed to: \`${{ env.REGISTRY }}/${{ env.IMAGE_PREFIX }}-*\`" >> $GITHUB_STEP_SUMMARY

222
.github/workflows/security.yml vendored Normal file
View File

@@ -0,0 +1,222 @@
name: Security Scanning
on:
push:
branches: [main, develop]
pull_request:
branches: [main, develop]
schedule:
# Run security scans weekly on Sundays at midnight
- cron: '0 0 * * 0'
jobs:
# ==========================================
# Secret Scanning
# ==========================================
secret-scan:
name: Secret Scanning
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: TruffleHog Secret Scan
uses: trufflesecurity/trufflehog@main
with:
extra_args: --only-verified
- name: GitLeaks Secret Scan
uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
continue-on-error: true
# ==========================================
# Dependency Vulnerability Scanning
# ==========================================
dependency-scan:
name: Dependency Vulnerability Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Run Trivy vulnerability scanner (filesystem)
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
severity: 'CRITICAL,HIGH'
format: 'sarif'
output: 'trivy-fs-results.sarif'
continue-on-error: true
- name: Upload Trivy scan results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-fs-results.sarif'
continue-on-error: true
# ==========================================
# Go Security Scan
# ==========================================
go-security:
name: Go Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run Gosec Security Scanner
uses: securego/gosec@master
with:
args: '-no-fail -fmt sarif -out gosec-results.sarif ./consent-service/...'
continue-on-error: true
- name: Upload Gosec results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'gosec-results.sarif'
continue-on-error: true
- name: Run govulncheck
working-directory: ./consent-service
run: |
go install golang.org/x/vuln/cmd/govulncheck@latest
govulncheck ./... || true
# ==========================================
# Python Security Scan
# ==========================================
python-security:
name: Python Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: '3.11'
- name: Install safety
run: pip install safety bandit
- name: Run Safety (dependency check)
working-directory: ./backend
run: safety check -r requirements.txt --full-report || true
- name: Run Bandit (code security scan)
working-directory: ./backend
run: bandit -r . -f sarif -o bandit-results.sarif --exit-zero
- name: Upload Bandit results to GitHub Security tab
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: './backend/bandit-results.sarif'
continue-on-error: true
# ==========================================
# Node.js Security Scan
# ==========================================
node-security:
name: Node.js Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '20'
- name: Install dependencies
working-directory: ./website
run: npm ci
- name: Run npm audit
working-directory: ./website
run: npm audit --audit-level=high || true
# ==========================================
# Docker Image Scanning
# ==========================================
docker-security:
name: Docker Image Security
runs-on: ubuntu-latest
needs: [go-security, python-security, node-security]
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Build consent-service image
run: docker build -t breakpilot/consent-service:scan ./consent-service
- name: Run Trivy on consent-service
uses: aquasecurity/trivy-action@master
with:
image-ref: 'breakpilot/consent-service:scan'
severity: 'CRITICAL,HIGH'
format: 'sarif'
output: 'trivy-consent-results.sarif'
continue-on-error: true
- name: Build backend image
run: docker build -t breakpilot/backend:scan ./backend
- name: Run Trivy on backend
uses: aquasecurity/trivy-action@master
with:
image-ref: 'breakpilot/backend:scan'
severity: 'CRITICAL,HIGH'
format: 'sarif'
output: 'trivy-backend-results.sarif'
continue-on-error: true
- name: Upload Trivy results
uses: github/codeql-action/upload-sarif@v3
with:
sarif_file: 'trivy-consent-results.sarif'
continue-on-error: true
# ==========================================
# Security Summary
# ==========================================
security-summary:
name: Security Summary
runs-on: ubuntu-latest
needs: [secret-scan, dependency-scan, go-security, python-security, node-security, docker-security]
if: always()
steps:
- name: Create security summary
run: |
echo "## Security Scan Results" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "| Scan Type | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-----------|--------|" >> $GITHUB_STEP_SUMMARY
echo "| Secret Scanning | ${{ needs.secret-scan.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Dependency Scanning | ${{ needs.dependency-scan.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Go Security | ${{ needs.go-security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Python Security | ${{ needs.python-security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Node.js Security | ${{ needs.node-security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "| Docker Security | ${{ needs.docker-security.result }} |" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "### Notes" >> $GITHUB_STEP_SUMMARY
echo "- Results are uploaded to the GitHub Security tab" >> $GITHUB_STEP_SUMMARY
echo "- Weekly scheduled scans run on Sundays" >> $GITHUB_STEP_SUMMARY

244
.github/workflows/test.yml vendored Normal file
View File

@@ -0,0 +1,244 @@
name: Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
go-tests:
name: Go Tests
runs-on: ubuntu-latest
services:
postgres:
image: postgres:16-alpine
env:
POSTGRES_USER: breakpilot
POSTGRES_PASSWORD: breakpilot123
POSTGRES_DB: breakpilot_test
ports:
- 5432:5432
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
cache: true
cache-dependency-path: consent-service/go.sum
- name: Install Dependencies
working-directory: ./consent-service
run: go mod download
- name: Run Tests
working-directory: ./consent-service
env:
DATABASE_URL: postgres://breakpilot:breakpilot123@localhost:5432/breakpilot_test?sslmode=disable
JWT_SECRET: test-secret-key-for-ci
JWT_REFRESH_SECRET: test-refresh-secret-for-ci
run: |
go test -v -race -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
- name: Check Coverage Threshold
working-directory: ./consent-service
run: |
COVERAGE=$(go tool cover -func=coverage.out | grep total | awk '{print $3}' | sed 's/%//')
echo "Total Coverage: $COVERAGE%"
if (( $(echo "$COVERAGE < 70.0" | bc -l) )); then
echo "Coverage $COVERAGE% is below threshold 70%"
exit 1
fi
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./consent-service/coverage.out
flags: go
name: go-coverage
python-tests:
name: Python Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
cache: 'pip'
cache-dependency-path: backend/requirements.txt
- name: Install Dependencies
working-directory: ./backend
run: |
pip install --upgrade pip
pip install -r requirements.txt
pip install pytest pytest-cov pytest-asyncio
- name: Run Tests
working-directory: ./backend
env:
CONSENT_SERVICE_URL: http://localhost:8081
JWT_SECRET: test-secret-key-for-ci
run: |
pytest -v --cov=. --cov-report=xml --cov-report=term
- name: Check Coverage Threshold
working-directory: ./backend
run: |
COVERAGE=$(python -c "import xml.etree.ElementTree as ET; tree = ET.parse('coverage.xml'); print(tree.getroot().attrib['line-rate'])")
COVERAGE_PCT=$(echo "$COVERAGE * 100" | bc)
echo "Total Coverage: ${COVERAGE_PCT}%"
if (( $(echo "$COVERAGE_PCT < 60.0" | bc -l) )); then
echo "Coverage ${COVERAGE_PCT}% is below threshold 60%"
exit 1
fi
- name: Upload Coverage to Codecov
uses: codecov/codecov-action@v3
with:
files: ./backend/coverage.xml
flags: python
name: python-coverage
integration-tests:
name: Integration Tests
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Start Services
run: |
docker-compose up -d
docker-compose ps
- name: Wait for Postgres
run: |
timeout 60 bash -c 'until docker-compose exec -T postgres pg_isready -U breakpilot; do sleep 2; done'
- name: Wait for Consent Service
run: |
timeout 60 bash -c 'until curl -f http://localhost:8081/health; do sleep 2; done'
- name: Wait for Backend
run: |
timeout 60 bash -c 'until curl -f http://localhost:8000/health; do sleep 2; done'
- name: Wait for Mailpit
run: |
timeout 60 bash -c 'until curl -f http://localhost:8025/api/v1/info; do sleep 2; done'
- name: Run Integration Tests
run: |
chmod +x ./scripts/integration-tests.sh
./scripts/integration-tests.sh
- name: Show Service Logs on Failure
if: failure()
run: |
echo "=== Consent Service Logs ==="
docker-compose logs consent-service
echo "=== Backend Logs ==="
docker-compose logs backend
echo "=== Postgres Logs ==="
docker-compose logs postgres
- name: Cleanup
if: always()
run: docker-compose down -v
lint-go:
name: Go Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Go
uses: actions/setup-go@v5
with:
go-version: '1.21'
- name: Run golangci-lint
uses: golangci/golangci-lint-action@v3
with:
version: latest
working-directory: consent-service
args: --timeout=5m
lint-python:
name: Python Lint
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: '3.10'
- name: Install Dependencies
run: |
pip install flake8 black mypy
- name: Run Black
working-directory: ./backend
run: black --check .
- name: Run Flake8
working-directory: ./backend
run: flake8 . --max-line-length=120 --exclude=venv
security-scan:
name: Security Scan
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Run Trivy Security Scan
uses: aquasecurity/trivy-action@master
with:
scan-type: 'fs'
scan-ref: '.'
format: 'sarif'
output: 'trivy-results.sarif'
- name: Upload Trivy Results to GitHub Security
uses: github/codeql-action/upload-sarif@v2
if: always()
with:
sarif_file: 'trivy-results.sarif'
all-checks:
name: All Checks Passed
runs-on: ubuntu-latest
needs: [go-tests, python-tests, integration-tests, lint-go, lint-python, security-scan]
steps:
- name: All Tests Passed
run: echo "All tests and checks passed successfully!"

186
.gitignore vendored Normal file
View File

@@ -0,0 +1,186 @@
# ============================================
# BreakPilot PWA - Git Ignore
# ============================================
# Environment files (keep examples only)
.env
.env.local
*.env.local
# Keep examples and environment templates
!.env.example
!.env.dev
!.env.staging
# .env.prod should NOT be in repo (contains production secrets)
# ============================================
# Python
# ============================================
__pycache__/
*.py[cod]
*$py.class
*.so
.Python
venv/
ENV/
.venv/
*.egg-info/
.eggs/
*.egg
.pytest_cache/
htmlcov/
.coverage
.coverage.*
coverage.xml
*.cover
# ============================================
# Node.js
# ============================================
node_modules/
.next/
out/
dist/
build/
.npm
.yarn-integrity
*.tsbuildinfo
# ============================================
# Go
# ============================================
*.exe
*.exe~
*.dll
*.dylib
*.test
*.out
vendor/
# ============================================
# Docker
# ============================================
# Don't ignore docker-compose files
# Ignore volume data if mounted locally
backups/
*.sql.gz
*.sql
# ============================================
# IDE & Editors
# ============================================
.idea/
.vscode/
*.swp
*.swo
*~
.project
.classpath
.settings/
*.sublime-workspace
*.sublime-project
# ============================================
# OS Files
# ============================================
.DS_Store
.DS_Store?
._*
.Spotlight-V100
.Trashes
ehthumbs.db
Thumbs.db
# ============================================
# Secrets & Credentials
# ============================================
secrets/
*.pem
*.key
*.crt
*.p12
*.pfx
credentials.json
service-account.json
# ============================================
# Logs
# ============================================
*.log
logs/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# ============================================
# Build Artifacts
# ============================================
*.zip
*.tar.gz
*.rar
# ============================================
# Temporary Files
# ============================================
tmp/
temp/
*.tmp
*.temp
# ============================================
# Test Results
# ============================================
test-results/
playwright-report/
coverage/
# ============================================
# ML Models (large files)
# ============================================
*.pt
*.pth
*.onnx
*.safetensors
models/
.claude/settings.local.json
# ============================================
# IDE Plugins & AI Tools
# ============================================
.continue/
CLAUDE_CONTINUE.md
# ============================================
# Misplaced / Large Directories
# ============================================
backend/BreakpilotDrive/
backend/website/
backend/screenshots/
**/za-download-9/
# ============================================
# Debug & Temp Artifacts
# ============================================
*.command
ssh_key*.txt
anleitung.txt
fix_permissions.txt
# ============================================
# Compiled Go Binaries
# ============================================
billing-service/billing-service
consent-service/server
edu-search-service/server
edu-search-service/edu-search-service
# ============================================
# Large Document Archives (PDFs, DOCX)
# ============================================
docs/za-download/
docs/za-download-2/
docs/za-download-3/
*.pdf
*.docx
*.xlsx
*.pptx

77
.gitleaks.toml Normal file
View File

@@ -0,0 +1,77 @@
# Gitleaks Configuration for BreakPilot
# https://github.com/gitleaks/gitleaks
#
# Run locally: gitleaks detect --source . -v
# Pre-commit: gitleaks protect --staged -v
title = "BreakPilot Gitleaks Configuration"
# Use the default rules plus custom rules
[extend]
useDefault = true
# Custom rules for BreakPilot-specific patterns
[[rules]]
id = "anthropic-api-key"
description = "Anthropic API Key"
regex = '''sk-ant-api[0-9a-zA-Z-_]{20,}'''
tags = ["api", "anthropic"]
keywords = ["sk-ant-api"]
[[rules]]
id = "vast-api-key"
description = "vast.ai API Key"
regex = '''(?i)(vast[_-]?api[_-]?key|vast[_-]?key)\s*[=:]\s*['"]?([a-zA-Z0-9-_]{20,})['"]?'''
tags = ["api", "vast"]
keywords = ["vast"]
[[rules]]
id = "stripe-secret-key"
description = "Stripe Secret Key"
regex = '''sk_live_[0-9a-zA-Z]{24,}'''
tags = ["api", "stripe"]
keywords = ["sk_live"]
[[rules]]
id = "stripe-restricted-key"
description = "Stripe Restricted Key"
regex = '''rk_live_[0-9a-zA-Z]{24,}'''
tags = ["api", "stripe"]
keywords = ["rk_live"]
[[rules]]
id = "jwt-secret-hardcoded"
description = "Hardcoded JWT Secret"
regex = '''(?i)(jwt[_-]?secret|jwt[_-]?key)\s*[=:]\s*['"]([^'"]{32,})['"]'''
tags = ["secret", "jwt"]
keywords = ["jwt"]
# Allowlist for false positives
[allowlist]
description = "Global allowlist"
paths = [
'''\.env\.example$''',
'''\.env\.template$''',
'''docs/.*\.md$''',
'''SBOM\.md$''',
'''.*_test\.py$''',
'''.*_test\.go$''',
'''test_.*\.py$''',
'''.*\.bak$''',
'''node_modules/.*''',
'''venv/.*''',
'''\.git/.*''',
]
# Specific commit allowlist (for already-rotated secrets)
commits = []
# Regex patterns to ignore
regexes = [
'''REPLACE_WITH_REAL_.*''',
'''your-.*-key-change-in-production''',
'''breakpilot-dev-.*''',
'''DEVELOPMENT-ONLY-.*''',
'''placeholder.*''',
'''example.*key''',
]

152
.pre-commit-config.yaml Normal file
View File

@@ -0,0 +1,152 @@
# Pre-commit Hooks für BreakPilot
# Installation: pip install pre-commit && pre-commit install
# Aktivierung: pre-commit install
repos:
# Go Hooks
- repo: local
hooks:
- id: go-test
name: Go Tests
entry: bash -c 'cd consent-service && go test -short ./...'
language: system
pass_filenames: false
files: \.go$
stages: [commit]
- id: go-fmt
name: Go Format
entry: bash -c 'cd consent-service && gofmt -l -w .'
language: system
pass_filenames: false
files: \.go$
stages: [commit]
- id: go-vet
name: Go Vet
entry: bash -c 'cd consent-service && go vet ./...'
language: system
pass_filenames: false
files: \.go$
stages: [commit]
- id: golangci-lint
name: Go Lint (golangci-lint)
entry: bash -c 'cd consent-service && golangci-lint run --timeout=5m'
language: system
pass_filenames: false
files: \.go$
stages: [commit]
# Python Hooks
- repo: local
hooks:
- id: pytest
name: Python Tests
entry: bash -c 'cd backend && pytest -x'
language: system
pass_filenames: false
files: \.py$
stages: [commit]
- id: black
name: Black Format
entry: black
language: python
types: [python]
args: [--line-length=120]
stages: [commit]
- id: flake8
name: Flake8 Lint
entry: flake8
language: python
types: [python]
args: [--max-line-length=120, --exclude=venv]
stages: [commit]
# General Hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: trailing-whitespace
name: Trim Trailing Whitespace
- id: end-of-file-fixer
name: Fix End of Files
- id: check-yaml
name: Check YAML
args: [--allow-multiple-documents]
- id: check-json
name: Check JSON
- id: check-added-large-files
name: Check Large Files
args: [--maxkb=500]
- id: detect-private-key
name: Detect Private Keys
- id: mixed-line-ending
name: Fix Mixed Line Endings
# Security Checks
- repo: https://github.com/Yelp/detect-secrets
rev: v1.4.0
hooks:
- id: detect-secrets
name: Detect Secrets
args: ['--baseline', '.secrets.baseline']
exclude: |
(?x)^(
.*\.lock|
.*\.sum|
package-lock\.json
)$
# =============================================
# DevSecOps: Gitleaks (Secrets Detection)
# =============================================
- repo: https://github.com/gitleaks/gitleaks
rev: v8.18.1
hooks:
- id: gitleaks
name: Gitleaks (secrets detection)
entry: gitleaks protect --staged -v --config .gitleaks.toml
language: golang
pass_filenames: false
# =============================================
# DevSecOps: Semgrep (SAST)
# =============================================
- repo: https://github.com/returntocorp/semgrep
rev: v1.52.0
hooks:
- id: semgrep
name: Semgrep (SAST)
args:
- --config=auto
- --config=.semgrep.yml
- --severity=ERROR
types_or: [python, javascript, typescript, go]
stages: [commit]
# =============================================
# DevSecOps: Bandit (Python Security)
# =============================================
- repo: https://github.com/PyCQA/bandit
rev: 1.7.6
hooks:
- id: bandit
name: Bandit (Python security)
args: ["-r", "backend/", "-ll", "-x", "backend/tests/*"]
files: ^backend/.*\.py$
stages: [commit]
# Branch Protection
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v4.5.0
hooks:
- id: no-commit-to-branch
name: Protect main/develop branches
args: ['--branch', 'main', '--branch', 'develop']
# Configuration
default_stages: [commit]
fail_fast: false

147
.semgrep.yml Normal file
View File

@@ -0,0 +1,147 @@
# Semgrep Configuration for BreakPilot
# https://semgrep.dev/
#
# Run locally: semgrep scan --config auto
# Run with this config: semgrep scan --config .semgrep.yml
rules:
# =============================================
# Python/FastAPI Security Rules
# =============================================
- id: hardcoded-secret-in-string
patterns:
- pattern-either:
- pattern: |
$VAR = "...$SECRET..."
- pattern: |
$VAR = '...$SECRET...'
message: "Potential hardcoded secret detected. Use environment variables or Vault."
languages: [python]
severity: WARNING
metadata:
category: security
cwe: "CWE-798: Use of Hard-coded Credentials"
- id: sql-injection-fastapi
patterns:
- pattern-either:
- pattern: |
$CURSOR.execute(f"...{$USER_INPUT}...")
- pattern: |
$CURSOR.execute("..." + $USER_INPUT + "...")
- pattern: |
$CURSOR.execute("..." % $USER_INPUT)
message: "Potential SQL injection. Use parameterized queries."
languages: [python]
severity: ERROR
metadata:
category: security
cwe: "CWE-89: SQL Injection"
owasp: "A03:2021 - Injection"
- id: command-injection
patterns:
- pattern-either:
- pattern: os.system($USER_INPUT)
- pattern: subprocess.call($USER_INPUT, shell=True)
- pattern: subprocess.run($USER_INPUT, shell=True)
- pattern: subprocess.Popen($USER_INPUT, shell=True)
message: "Potential command injection. Avoid shell=True with user input."
languages: [python]
severity: ERROR
metadata:
category: security
cwe: "CWE-78: OS Command Injection"
owasp: "A03:2021 - Injection"
- id: insecure-jwt-algorithm
patterns:
- pattern: jwt.decode(..., algorithms=["none"], ...)
- pattern: jwt.decode(..., algorithms=["HS256"], verify=False, ...)
message: "Insecure JWT algorithm or verification disabled."
languages: [python]
severity: ERROR
metadata:
category: security
cwe: "CWE-347: Improper Verification of Cryptographic Signature"
- id: path-traversal
patterns:
- pattern: open(... + $USER_INPUT + ...)
- pattern: open(f"...{$USER_INPUT}...")
- pattern: Path(...) / $USER_INPUT
message: "Potential path traversal. Validate and sanitize file paths."
languages: [python]
severity: WARNING
metadata:
category: security
cwe: "CWE-22: Path Traversal"
- id: insecure-pickle
patterns:
- pattern: pickle.loads($DATA)
- pattern: pickle.load($FILE)
message: "Pickle deserialization is insecure. Use JSON or other safe formats."
languages: [python]
severity: WARNING
metadata:
category: security
cwe: "CWE-502: Deserialization of Untrusted Data"
# =============================================
# Go Security Rules
# =============================================
- id: go-sql-injection
patterns:
- pattern: |
$DB.Query(fmt.Sprintf("...", $USER_INPUT))
- pattern: |
$DB.Exec(fmt.Sprintf("...", $USER_INPUT))
message: "Potential SQL injection in Go. Use parameterized queries."
languages: [go]
severity: ERROR
metadata:
category: security
cwe: "CWE-89: SQL Injection"
- id: go-hardcoded-credentials
patterns:
- pattern: |
$VAR := "..."
- metavariable-regex:
metavariable: $VAR
regex: (password|secret|apiKey|api_key|token)
message: "Potential hardcoded credential. Use environment variables."
languages: [go]
severity: WARNING
metadata:
category: security
cwe: "CWE-798: Use of Hard-coded Credentials"
# =============================================
# JavaScript/TypeScript Security Rules
# =============================================
- id: js-xss-innerhtml
patterns:
- pattern: $EL.innerHTML = $USER_INPUT
message: "Potential XSS via innerHTML. Use textContent or sanitize input."
languages: [javascript, typescript]
severity: WARNING
metadata:
category: security
cwe: "CWE-79: Cross-site Scripting"
owasp: "A03:2021 - Injection"
- id: js-eval
patterns:
- pattern: eval($CODE)
- pattern: new Function($CODE)
message: "Avoid eval() and new Function() with dynamic input."
languages: [javascript, typescript]
severity: ERROR
metadata:
category: security
cwe: "CWE-95: Improper Neutralization of Directives in Dynamically Evaluated Code"

66
.trivy.yaml Normal file
View File

@@ -0,0 +1,66 @@
# Trivy Configuration for BreakPilot
# https://trivy.dev/
#
# Run: trivy image breakpilot-pwa-backend:latest
# Run filesystem: trivy fs .
# Run config: trivy config .
# Scan settings
scan:
# Security checks to perform
security-checks:
- vuln # Vulnerabilities
- config # Misconfigurations
- secret # Secrets in files
# Vulnerability settings
vulnerability:
# Vulnerability types to scan for
type:
- os # OS packages
- library # Application dependencies
# Ignore unfixed vulnerabilities
ignore-unfixed: false
# Severity settings
severity:
- CRITICAL
- HIGH
- MEDIUM
# - LOW # Uncomment to include low severity
# Output format
format: table
# Exit code on findings
exit-code: 1
# Timeout
timeout: 10m
# Cache directory
cache-dir: /tmp/trivy-cache
# Skip files/directories
skip-dirs:
- node_modules
- venv
- .venv
- __pycache__
- .git
- .idea
- .vscode
skip-files:
- "*.md"
- "*.txt"
- "*.log"
# Ignore specific vulnerabilities (add after review)
ignorefile: .trivyignore
# SBOM generation
sbom:
format: cyclonedx
output: sbom.json

9
.trivyignore Normal file
View File

@@ -0,0 +1,9 @@
# Trivy Ignore File for BreakPilot
# Add vulnerability IDs to ignore after security review
# Format: CVE-XXXX-XXXXX or GHSA-xxxx-xxxx-xxxx
# Example (remove after adding real ignores):
# CVE-2021-12345 # Reason: Not exploitable in our context
# Reviewed and accepted risks:
# (Add vulnerabilities here after security team review)

132
.woodpecker/auto-fix.yml Normal file
View File

@@ -0,0 +1,132 @@
# Woodpecker CI Auto-Fix Pipeline
# Automatische Reparatur fehlgeschlagener Tests
#
# Laeuft taeglich um 2:00 Uhr nachts
# Analysiert offene Backlog-Items und versucht automatische Fixes
when:
- event: cron
cron: "0 2 * * *" # Taeglich um 2:00 Uhr
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
extra_hosts:
- macmini:192.168.178.100
steps:
# ========================================
# 1. Fetch Failed Tests from Backlog
# ========================================
fetch-backlog:
image: curlimages/curl:latest
commands:
- |
curl -s "http://backend:8000/api/tests/backlog?status=open&priority=critical" \
-o backlog-critical.json
curl -s "http://backend:8000/api/tests/backlog?status=open&priority=high" \
-o backlog-high.json
- echo "=== Kritische Tests ==="
- cat backlog-critical.json | head -50
- echo "=== Hohe Prioritaet ==="
- cat backlog-high.json | head -50
# ========================================
# 2. Analyze and Classify Errors
# ========================================
analyze-errors:
image: python:3.12-slim
commands:
- pip install --quiet jq-py
- |
python3 << 'EOF'
import json
import os
def classify_error(error_type, error_msg):
"""Klassifiziert Fehler nach Auto-Fix-Potential"""
auto_fixable = {
'nil_pointer': 'high',
'import_error': 'high',
'undefined_variable': 'medium',
'type_error': 'medium',
'assertion': 'low',
'timeout': 'low',
'logic_error': 'manual'
}
return auto_fixable.get(error_type, 'manual')
# Lade Backlog
try:
with open('backlog-critical.json') as f:
critical = json.load(f)
with open('backlog-high.json') as f:
high = json.load(f)
except:
print("Keine Backlog-Daten gefunden")
exit(0)
all_items = critical.get('items', []) + high.get('items', [])
auto_fix_candidates = []
for item in all_items:
fix_potential = classify_error(
item.get('error_type', 'unknown'),
item.get('error_message', '')
)
if fix_potential in ['high', 'medium']:
auto_fix_candidates.append({
'id': item.get('id'),
'test_name': item.get('test_name'),
'error_type': item.get('error_type'),
'fix_potential': fix_potential
})
print(f"Auto-Fix Kandidaten: {len(auto_fix_candidates)}")
with open('auto-fix-candidates.json', 'w') as f:
json.dump(auto_fix_candidates, f, indent=2)
EOF
depends_on:
- fetch-backlog
# ========================================
# 3. Generate Fix Suggestions (Placeholder)
# ========================================
generate-fixes:
image: python:3.12-slim
commands:
- |
echo "Auto-Fix Generation ist in Phase 4 geplant"
echo "Aktuell werden nur Vorschlaege generiert"
# Hier wuerde Claude API oder anderer LLM aufgerufen werden
# python3 scripts/auto-fix-agent.py auto-fix-candidates.json
echo "Fix-Vorschlaege wuerden hier generiert werden"
depends_on:
- analyze-errors
# ========================================
# 4. Report Results
# ========================================
report-results:
image: curlimages/curl:latest
commands:
- |
curl -X POST "http://backend:8000/api/tests/auto-fix/report" \
-H "Content-Type: application/json" \
-d "{
\"run_date\": \"$(date -Iseconds)\",
\"candidates_found\": $(cat auto-fix-candidates.json | wc -l),
\"fixes_attempted\": 0,
\"fixes_successful\": 0,
\"status\": \"analysis_only\"
}" || true
when:
status: [success, failure]

View File

@@ -0,0 +1,37 @@
# One-time pipeline to build the custom Python CI image
# Trigger manually, then delete this file
#
# This builds the breakpilot/python-ci:3.12 image on the CI runner
when:
- event: manual
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
extra_hosts:
- macmini:192.168.178.100
steps:
build-python-ci-image:
image: docker:27-cli
volumes:
- /var/run/docker.sock:/var/run/docker.sock
commands:
- |
echo "=== Building breakpilot/python-ci:3.12 ==="
docker build \
-t breakpilot/python-ci:3.12 \
-t breakpilot/python-ci:latest \
-f .docker/python-ci.Dockerfile \
.
echo ""
echo "=== Build complete ==="
docker images | grep breakpilot/python-ci
echo ""
echo "Image is now available for CI pipelines!"

161
.woodpecker/integration.yml Normal file
View File

@@ -0,0 +1,161 @@
# Integration Tests Pipeline
# Separate Datei weil Services auf Pipeline-Ebene definiert werden muessen
#
# Diese Pipeline laeuft parallel zur main.yml und testet:
# - Database Connectivity (PostgreSQL)
# - Cache Connectivity (Valkey/Redis)
# - Service-to-Service Kommunikation
#
# Dokumentation: docs/testing/integration-test-environment.md
when:
- event: [push, pull_request]
branch: [main, develop]
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
extra_hosts:
- macmini:192.168.178.100
# Services auf Pipeline-Ebene (NICHT Step-Ebene!)
# Diese Services sind fuer ALLE Steps verfuegbar
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: breakpilot
POSTGRES_PASSWORD: breakpilot_test
POSTGRES_DB: breakpilot_test
valkey:
image: valkey/valkey:8-alpine
steps:
wait-for-services:
image: postgres:16-alpine
commands:
- |
echo "=== Waiting for PostgreSQL ==="
for i in $(seq 1 30); do
if pg_isready -h postgres -U breakpilot; then
echo "PostgreSQL ready after $i attempts!"
break
fi
echo "Attempt $i/30: PostgreSQL not ready, waiting..."
sleep 2
done
# Final check
if ! pg_isready -h postgres -U breakpilot; then
echo "ERROR: PostgreSQL not ready after 30 attempts"
exit 1
fi
- |
echo "=== Waiting for Valkey ==="
# Install redis-cli in postgres alpine image
apk add --no-cache redis > /dev/null 2>&1 || true
for i in $(seq 1 30); do
if redis-cli -h valkey ping 2>/dev/null | grep -q PONG; then
echo "Valkey ready after $i attempts!"
break
fi
echo "Attempt $i/30: Valkey not ready, waiting..."
sleep 2
done
# Final check
if ! redis-cli -h valkey ping 2>/dev/null | grep -q PONG; then
echo "ERROR: Valkey not ready after 30 attempts"
exit 1
fi
- echo "=== All services ready ==="
integration-tests:
image: breakpilot/python-ci:3.12
environment:
CI: "true"
DATABASE_URL: postgresql://breakpilot:breakpilot_test@postgres:5432/breakpilot_test
VALKEY_URL: redis://valkey:6379
REDIS_URL: redis://valkey:6379
SKIP_INTEGRATION_TESTS: "false"
SKIP_DB_TESTS: "false"
SKIP_WEASYPRINT_TESTS: "false"
# Test-spezifische Umgebungsvariablen
ENVIRONMENT: "testing"
JWT_SECRET: "test-secret-key-for-integration-tests"
TEACHER_REQUIRE_AUTH: "false"
GAME_USE_DATABASE: "false"
commands:
- |
set -uo pipefail
mkdir -p .ci-results
cd backend
# PYTHONPATH setzen damit lokale Module gefunden werden
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
echo "=== Installing dependencies ==="
pip install --quiet --no-cache-dir -r requirements.txt
echo "=== Running Integration Tests ==="
set +e
python -m pytest tests/test_integration/ -v \
--tb=short \
--json-report \
--json-report-file=../.ci-results/test-integration.json
TEST_EXIT=$?
set -e
# Ergebnisse auswerten
if [ -f ../.ci-results/test-integration.json ]; then
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-integration.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-integration.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-integration.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-integration.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
else
echo "WARNUNG: Keine JSON-Ergebnisse gefunden"
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
echo "{\"service\":\"integration-tests\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-integration.json
cat ../.ci-results/results-integration.json
echo ""
echo "=== Integration Test Summary ==="
echo "Total: $TOTAL | Passed: $PASSED | Failed: $FAILED | Skipped: $SKIPPED"
if [ "$TEST_EXIT" -ne "0" ]; then
echo "Integration tests failed with exit code $TEST_EXIT"
exit 1
fi
depends_on:
- wait-for-services
report-integration-results:
image: curlimages/curl:8.10.1
commands:
- |
set -uo pipefail
echo "=== Sende Integration Test-Ergebnisse an Dashboard ==="
if [ -f .ci-results/results-integration.json ]; then
echo "Sending integration test results..."
curl -f -sS -X POST "http://backend:8000/api/tests/ci-result" \
-H "Content-Type: application/json" \
-d "{
\"pipeline_id\": \"${CI_PIPELINE_NUMBER}\",
\"commit\": \"${CI_COMMIT_SHA}\",
\"branch\": \"${CI_COMMIT_BRANCH}\",
\"status\": \"${CI_PIPELINE_STATUS:-unknown}\",
\"test_results\": $(cat .ci-results/results-integration.json)
}" || echo "WARNUNG: Konnte Ergebnisse nicht an Dashboard senden"
else
echo "Keine Integration-Ergebnisse zum Senden gefunden"
fi
echo "=== Integration Test-Ergebnisse gesendet ==="
when:
status: [success, failure]
depends_on:
- integration-tests

669
.woodpecker/main.yml Normal file
View File

@@ -0,0 +1,669 @@
# Woodpecker CI Main Pipeline
# BreakPilot PWA - CI/CD Pipeline
#
# Plattform: ARM64 (Apple Silicon Mac Mini)
#
# Strategie:
# - Tests laufen bei JEDEM Push/PR
# - Test-Ergebnisse werden an Dashboard gesendet
# - Builds/Scans laufen nur bei Tags oder manuell
# - Deployment nur manuell (Sicherheit)
when:
- event: [push, pull_request, manual, tag]
branch: [main, develop]
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
extra_hosts:
- macmini:192.168.178.100
variables:
- &golang_image golang:1.23-alpine
- &python_image python:3.12-slim
- &python_ci_image breakpilot/python-ci:3.12 # Custom image with WeasyPrint
- &nodejs_image node:20-alpine
- &docker_image docker:27-cli
steps:
# ========================================
# STAGE 1: Lint (nur bei PRs)
# ========================================
go-lint:
image: golangci/golangci-lint:v1.55-alpine
commands:
- cd consent-service && golangci-lint run --timeout 5m ./...
- cd ../billing-service && golangci-lint run --timeout 5m ./...
- cd ../school-service && golangci-lint run --timeout 5m ./...
when:
event: pull_request
python-lint:
image: *python_image
commands:
- pip install --quiet ruff black
- ruff check backend/ --output-format=github || true
- black --check backend/ || true
when:
event: pull_request
# ========================================
# STAGE 2: Unit Tests mit JSON-Ausgabe
# Ergebnisse werden im Workspace gespeichert (.ci-results/)
# ========================================
test-go-consent:
image: *golang_image
environment:
CGO_ENABLED: "0"
commands:
- |
set -euo pipefail
apk add --no-cache jq bash
mkdir -p .ci-results
if [ ! -d "consent-service" ]; then
echo '{"service":"consent-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-consent.json
echo "WARNUNG: consent-service Verzeichnis nicht gefunden"
exit 0
fi
cd consent-service
set +e
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-consent.json
TEST_EXIT=$?
set -e
# JSON-Zeilen extrahieren und mit jq zählen
JSON_FILE="../.ci-results/test-consent.json"
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
else
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
[ -z "$COVERAGE" ] && COVERAGE=0
echo "{\"service\":\"consent-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-consent.json
cat ../.ci-results/results-consent.json
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
if [ "$FAILED" -gt "0" ]; then
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
fi
test-go-billing:
image: *golang_image
environment:
CGO_ENABLED: "0"
commands:
- |
set -euo pipefail
apk add --no-cache jq bash
mkdir -p .ci-results
if [ ! -d "billing-service" ]; then
echo '{"service":"billing-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-billing.json
echo "WARNUNG: billing-service Verzeichnis nicht gefunden"
exit 0
fi
cd billing-service
set +e
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-billing.json
TEST_EXIT=$?
set -e
# JSON-Zeilen extrahieren und mit jq zählen
JSON_FILE="../.ci-results/test-billing.json"
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
else
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
[ -z "$COVERAGE" ] && COVERAGE=0
echo "{\"service\":\"billing-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-billing.json
cat ../.ci-results/results-billing.json
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
if [ "$FAILED" -gt "0" ]; then
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
fi
test-go-school:
image: *golang_image
environment:
CGO_ENABLED: "0"
commands:
- |
set -euo pipefail
apk add --no-cache jq bash
mkdir -p .ci-results
if [ ! -d "school-service" ]; then
echo '{"service":"school-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-school.json
echo "WARNUNG: school-service Verzeichnis nicht gefunden"
exit 0
fi
cd school-service
set +e
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-school.json
TEST_EXIT=$?
set -e
# JSON-Zeilen extrahieren und mit jq zählen
JSON_FILE="../.ci-results/test-school.json"
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
else
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
[ -z "$COVERAGE" ] && COVERAGE=0
echo "{\"service\":\"school-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-school.json
cat ../.ci-results/results-school.json
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
if [ "$FAILED" -gt "0" ]; then
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
fi
test-go-edu-search:
image: *golang_image
environment:
CGO_ENABLED: "0"
commands:
- |
set -euo pipefail
apk add --no-cache jq bash
mkdir -p .ci-results
if [ ! -d "edu-search-service" ]; then
echo '{"service":"edu-search-service","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-edu-search.json
echo "WARNUNG: edu-search-service Verzeichnis nicht gefunden"
exit 0
fi
cd edu-search-service
set +e
go test -v -json -coverprofile=coverage.out ./internal/... 2>&1 | tee ../.ci-results/test-edu-search.json
TEST_EXIT=$?
set -e
# JSON-Zeilen extrahieren und mit jq zählen
JSON_FILE="../.ci-results/test-edu-search.json"
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
else
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
[ -z "$COVERAGE" ] && COVERAGE=0
echo "{\"service\":\"edu-search-service\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-edu-search.json
cat ../.ci-results/results-edu-search.json
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
if [ "$FAILED" -gt "0" ]; then
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
fi
test-go-ai-compliance:
image: *golang_image
environment:
CGO_ENABLED: "0"
commands:
- |
set -euo pipefail
apk add --no-cache jq bash
mkdir -p .ci-results
if [ ! -d "ai-compliance-sdk" ]; then
echo '{"service":"ai-compliance-sdk","framework":"go","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-ai-compliance.json
echo "WARNUNG: ai-compliance-sdk Verzeichnis nicht gefunden"
exit 0
fi
cd ai-compliance-sdk
set +e
go test -v -json -coverprofile=coverage.out ./... 2>&1 | tee ../.ci-results/test-ai-compliance.json
TEST_EXIT=$?
set -e
# JSON-Zeilen extrahieren und mit jq zählen
JSON_FILE="../.ci-results/test-ai-compliance.json"
if grep -q '^{' "$JSON_FILE" 2>/dev/null; then
TOTAL=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="run" and .Test != null)] | length')
PASSED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="pass" and .Test != null)] | length')
FAILED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="fail" and .Test != null)] | length')
SKIPPED=$(grep '^{' "$JSON_FILE" | jq -s '[.[] | select(.Action=="skip" and .Test != null)] | length')
else
echo "WARNUNG: Keine JSON-Zeilen in $JSON_FILE gefunden (Build-Fehler?)"
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
COVERAGE=$(go tool cover -func=coverage.out 2>/dev/null | tail -1 | awk '{print $3}' | tr -d '%' || echo "0")
[ -z "$COVERAGE" ] && COVERAGE=0
echo "{\"service\":\"ai-compliance-sdk\",\"framework\":\"go\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":$COVERAGE}" > ../.ci-results/results-ai-compliance.json
cat ../.ci-results/results-ai-compliance.json
# Backlog-Strategie: Fehler werden gemeldet aber Pipeline laeuft weiter
if [ "$FAILED" -gt "0" ]; then
echo "WARNUNG: $FAILED Tests fehlgeschlagen - werden ins Backlog geschrieben"
fi
test-python-backend:
image: *python_ci_image
environment:
CI: "true"
DATABASE_URL: "postgresql://test:test@localhost:5432/test_db"
SKIP_DB_TESTS: "true"
SKIP_WEASYPRINT_TESTS: "false"
SKIP_INTEGRATION_TESTS: "true"
commands:
- |
set -uo pipefail
mkdir -p .ci-results
if [ ! -d "backend" ]; then
echo '{"service":"backend","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-backend.json
echo "WARNUNG: backend Verzeichnis nicht gefunden"
exit 0
fi
cd backend
# Set PYTHONPATH to current directory (backend) so local packages like classroom_engine, alerts_agent are found
# IMPORTANT: Use absolute path and export before pip install to ensure modules are available
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
# Test tools are pre-installed in breakpilot/python-ci image
# Only install project-specific dependencies
pip install --quiet --no-cache-dir -r requirements.txt
# NOTE: PostgreSQL service removed - tests that require DB are skipped via SKIP_DB_TESTS=true
# For full integration tests, use: docker compose -f docker-compose.test.yml up -d
set +e
# Use python -m pytest to ensure PYTHONPATH is properly applied before pytest starts
python -m pytest tests/ -v --tb=short --cov=. --cov-report=term-missing --json-report --json-report-file=../.ci-results/test-backend.json
TEST_EXIT=$?
set -e
if [ -f ../.ci-results/test-backend.json ]; then
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-backend.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
else
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
echo "{\"service\":\"backend\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-backend.json
cat ../.ci-results/results-backend.json
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
test-python-voice:
image: *python_image
environment:
CI: "true"
commands:
- |
set -uo pipefail
mkdir -p .ci-results
if [ ! -d "voice-service" ]; then
echo '{"service":"voice-service","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-voice.json
echo "WARNUNG: voice-service Verzeichnis nicht gefunden"
exit 0
fi
cd voice-service
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
pip install --quiet --no-cache-dir -r requirements.txt
pip install --quiet --no-cache-dir pytest-json-report
set +e
python -m pytest tests/ -v --tb=short --json-report --json-report-file=../.ci-results/test-voice.json
TEST_EXIT=$?
set -e
if [ -f ../.ci-results/test-voice.json ]; then
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-voice.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
else
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
echo "{\"service\":\"voice-service\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-voice.json
cat ../.ci-results/results-voice.json
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
test-bqas-golden:
image: *python_image
commands:
- |
set -uo pipefail
mkdir -p .ci-results
if [ ! -d "voice-service/tests/bqas" ]; then
echo '{"service":"bqas-golden","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-bqas-golden.json
echo "WARNUNG: voice-service/tests/bqas Verzeichnis nicht gefunden"
exit 0
fi
cd voice-service
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
pip install --quiet --no-cache-dir -r requirements.txt
pip install --quiet --no-cache-dir pytest-json-report pytest-asyncio
set +e
python -m pytest tests/bqas/test_golden.py tests/bqas/test_regression.py tests/bqas/test_synthetic.py -v --tb=short --json-report --json-report-file=../.ci-results/test-bqas-golden.json
TEST_EXIT=$?
set -e
if [ -f ../.ci-results/test-bqas-golden.json ]; then
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-golden.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
else
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
echo "{\"service\":\"bqas-golden\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-bqas-golden.json
cat ../.ci-results/results-bqas-golden.json
# BQAS tests may skip if Ollama not available - don't fail pipeline
if [ "$FAILED" -gt "0" ]; then exit 1; fi
test-bqas-rag:
image: *python_image
commands:
- |
set -uo pipefail
mkdir -p .ci-results
if [ ! -d "voice-service/tests/bqas" ]; then
echo '{"service":"bqas-rag","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-bqas-rag.json
echo "WARNUNG: voice-service/tests/bqas Verzeichnis nicht gefunden"
exit 0
fi
cd voice-service
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
pip install --quiet --no-cache-dir -r requirements.txt
pip install --quiet --no-cache-dir pytest-json-report pytest-asyncio
set +e
python -m pytest tests/bqas/test_rag.py tests/bqas/test_notifier.py -v --tb=short --json-report --json-report-file=../.ci-results/test-bqas-rag.json
TEST_EXIT=$?
set -e
if [ -f ../.ci-results/test-bqas-rag.json ]; then
TOTAL=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
PASSED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
FAILED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
SKIPPED=$(python3 -c "import json; d=json.load(open('../.ci-results/test-bqas-rag.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
else
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
echo "{\"service\":\"bqas-rag\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-bqas-rag.json
cat ../.ci-results/results-bqas-rag.json
# BQAS tests may skip if Ollama not available - don't fail pipeline
if [ "$FAILED" -gt "0" ]; then exit 1; fi
test-python-klausur:
image: *python_image
environment:
CI: "true"
commands:
- |
set -uo pipefail
mkdir -p .ci-results
if [ ! -d "klausur-service/backend" ]; then
echo '{"service":"klausur-service","framework":"pytest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-klausur.json
echo "WARNUNG: klausur-service/backend Verzeichnis nicht gefunden"
exit 0
fi
cd klausur-service/backend
# Set PYTHONPATH to current directory so local modules like hyde, hybrid_search, etc. are found
export PYTHONPATH="$(pwd):${PYTHONPATH:-}"
pip install --quiet --no-cache-dir -r requirements.txt 2>/dev/null || pip install --quiet --no-cache-dir fastapi uvicorn pytest pytest-asyncio pytest-json-report
pip install --quiet --no-cache-dir pytest-json-report
set +e
python -m pytest tests/ -v --tb=short --json-report --json-report-file=../../.ci-results/test-klausur.json
TEST_EXIT=$?
set -e
if [ -f ../../.ci-results/test-klausur.json ]; then
TOTAL=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('total',0))" 2>/dev/null || echo "0")
PASSED=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('passed',0))" 2>/dev/null || echo "0")
FAILED=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('failed',0))" 2>/dev/null || echo "0")
SKIPPED=$(python3 -c "import json; d=json.load(open('../../.ci-results/test-klausur.json')); print(d.get('summary',{}).get('skipped',0))" 2>/dev/null || echo "0")
else
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
echo "{\"service\":\"klausur-service\",\"framework\":\"pytest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../../.ci-results/results-klausur.json
cat ../../.ci-results/results-klausur.json
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
test-nodejs-h5p:
image: *nodejs_image
commands:
- |
set -uo pipefail
mkdir -p .ci-results
if [ ! -d "h5p-service" ]; then
echo '{"service":"h5p-service","framework":"jest","total":0,"passed":0,"failed":0,"skipped":0,"coverage":0}' > .ci-results/results-h5p.json
echo "WARNUNG: h5p-service Verzeichnis nicht gefunden"
exit 0
fi
cd h5p-service
npm ci --silent 2>/dev/null || npm install --silent
set +e
npm run test:ci -- --json --outputFile=../.ci-results/test-h5p.json 2>&1
TEST_EXIT=$?
set -e
if [ -f ../.ci-results/test-h5p.json ]; then
TOTAL=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numTotalTests || 0)" 2>/dev/null || echo "0")
PASSED=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numPassedTests || 0)" 2>/dev/null || echo "0")
FAILED=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numFailedTests || 0)" 2>/dev/null || echo "0")
SKIPPED=$(node -e "const d=require('../.ci-results/test-h5p.json'); console.log(d.numPendingTests || 0)" 2>/dev/null || echo "0")
else
TOTAL=0; PASSED=0; FAILED=0; SKIPPED=0
fi
[ -z "$TOTAL" ] && TOTAL=0
[ -z "$PASSED" ] && PASSED=0
[ -z "$FAILED" ] && FAILED=0
[ -z "$SKIPPED" ] && SKIPPED=0
echo "{\"service\":\"h5p-service\",\"framework\":\"jest\",\"total\":$TOTAL,\"passed\":$PASSED,\"failed\":$FAILED,\"skipped\":$SKIPPED,\"coverage\":0}" > ../.ci-results/results-h5p.json
cat ../.ci-results/results-h5p.json
if [ "$TEST_EXIT" -ne "0" ]; then exit 1; fi
# ========================================
# STAGE 2.5: Integration Tests
# ========================================
# Integration Tests laufen in separater Pipeline:
# .woodpecker/integration.yml
# (benötigt Pipeline-Level Services für PostgreSQL und Valkey)
# ========================================
# STAGE 3: Test-Ergebnisse an Dashboard senden
# ========================================
report-test-results:
image: curlimages/curl:8.10.1
commands:
- |
set -uo pipefail
echo "=== Sende Test-Ergebnisse an Dashboard ==="
echo "Pipeline Status: ${CI_PIPELINE_STATUS:-unknown}"
ls -la .ci-results/ || echo "Verzeichnis nicht gefunden"
PIPELINE_STATUS="${CI_PIPELINE_STATUS:-unknown}"
for f in .ci-results/results-*.json; do
[ -f "$f" ] || continue
echo "Sending: $f"
curl -f -sS -X POST "http://backend:8000/api/tests/ci-result" \
-H "Content-Type: application/json" \
-d "{
\"pipeline_id\": \"${CI_PIPELINE_NUMBER}\",
\"commit\": \"${CI_COMMIT_SHA}\",
\"branch\": \"${CI_COMMIT_BRANCH}\",
\"status\": \"${PIPELINE_STATUS}\",
\"test_results\": $(cat "$f")
}" || echo "WARNUNG: Konnte $f nicht senden"
done
echo "=== Test-Ergebnisse gesendet ==="
when:
status: [success, failure]
depends_on:
- test-go-consent
- test-go-billing
- test-go-school
- test-go-edu-search
- test-go-ai-compliance
- test-python-backend
- test-python-voice
- test-bqas-golden
- test-bqas-rag
- test-python-klausur
- test-nodejs-h5p
# ========================================
# STAGE 4: Build & Security (nur Tags/manuell)
# ========================================
build-consent-service:
image: *docker_image
commands:
- docker build -t breakpilot/consent-service:${CI_COMMIT_SHA:0:8} ./consent-service
- docker tag breakpilot/consent-service:${CI_COMMIT_SHA:0:8} breakpilot/consent-service:latest
- echo "Built breakpilot/consent-service:${CI_COMMIT_SHA:0:8}"
when:
- event: tag
- event: manual
build-backend:
image: *docker_image
commands:
- docker build -t breakpilot/backend:${CI_COMMIT_SHA:0:8} ./backend
- docker tag breakpilot/backend:${CI_COMMIT_SHA:0:8} breakpilot/backend:latest
- echo "Built breakpilot/backend:${CI_COMMIT_SHA:0:8}"
when:
- event: tag
- event: manual
build-voice-service:
image: *docker_image
commands:
- |
if [ -d ./voice-service ]; then
docker build -t breakpilot/voice-service:${CI_COMMIT_SHA:0:8} ./voice-service
docker tag breakpilot/voice-service:${CI_COMMIT_SHA:0:8} breakpilot/voice-service:latest
echo "Built breakpilot/voice-service:${CI_COMMIT_SHA:0:8}"
else
echo "voice-service Verzeichnis nicht gefunden - ueberspringe"
fi
when:
- event: tag
- event: manual
generate-sbom:
image: *golang_image
commands:
- |
echo "Installing syft for ARM64..."
wget -qO- https://raw.githubusercontent.com/anchore/syft/main/install.sh | sh -s -- -b /usr/local/bin
syft dir:./consent-service -o cyclonedx-json > sbom-consent.json
syft dir:./backend -o cyclonedx-json > sbom-backend.json
if [ -d ./voice-service ]; then
syft dir:./voice-service -o cyclonedx-json > sbom-voice.json
fi
echo "SBOMs generated successfully"
when:
- event: tag
- event: manual
vulnerability-scan:
image: *golang_image
commands:
- |
echo "Installing grype for ARM64..."
wget -qO- https://raw.githubusercontent.com/anchore/grype/main/install.sh | sh -s -- -b /usr/local/bin
grype sbom:sbom-consent.json -o table --fail-on critical || true
grype sbom:sbom-backend.json -o table --fail-on critical || true
if [ -f sbom-voice.json ]; then
grype sbom:sbom-voice.json -o table --fail-on critical || true
fi
when:
- event: tag
- event: manual
depends_on:
- generate-sbom
# ========================================
# STAGE 5: Deploy (nur manuell)
# ========================================
deploy-production:
image: *docker_image
commands:
- echo "Deploying to production..."
- docker compose -f docker-compose.yml pull || true
- docker compose -f docker-compose.yml up -d --remove-orphans || true
when:
event: manual
depends_on:
- build-consent-service
- build-backend

314
.woodpecker/security.yml Normal file
View File

@@ -0,0 +1,314 @@
# Woodpecker CI Security Pipeline
# Dedizierte Security-Scans fuer DevSecOps
#
# Laeuft taeglich via Cron und bei jedem PR
when:
- event: cron
cron: "0 3 * * *" # Taeglich um 3:00 Uhr
- event: pull_request
clone:
git:
image: woodpeckerci/plugin-git
settings:
depth: 1
extra_hosts:
- macmini:192.168.178.100
steps:
# ========================================
# Static Analysis
# ========================================
semgrep-scan:
image: returntocorp/semgrep:latest
commands:
- semgrep scan --config auto --json -o semgrep-results.json . || true
- |
if [ -f semgrep-results.json ]; then
echo "=== Semgrep Findings ==="
cat semgrep-results.json | head -100
fi
when:
event: [pull_request, cron]
bandit-python:
image: python:3.12-slim
commands:
- pip install --quiet bandit
- bandit -r backend/ -f json -o bandit-results.json || true
- |
if [ -f bandit-results.json ]; then
echo "=== Bandit Findings ==="
cat bandit-results.json | head -50
fi
when:
event: [pull_request, cron]
gosec-go:
image: securego/gosec:latest
commands:
- gosec -fmt json -out gosec-consent.json ./consent-service/... || true
- gosec -fmt json -out gosec-billing.json ./billing-service/... || true
- echo "Go Security Scan abgeschlossen"
when:
event: [pull_request, cron]
# ========================================
# Secrets Detection
# ========================================
gitleaks-scan:
image: zricethezav/gitleaks:latest
commands:
- gitleaks detect --source . --report-format json --report-path gitleaks-report.json || true
- |
if [ -s gitleaks-report.json ]; then
echo "=== WARNUNG: Potentielle Secrets gefunden ==="
cat gitleaks-report.json
else
echo "Keine Secrets gefunden"
fi
trufflehog-scan:
image: trufflesecurity/trufflehog:latest
commands:
- trufflehog filesystem . --json > trufflehog-results.json 2>&1 || true
- echo "TruffleHog Scan abgeschlossen"
# ========================================
# Dependency Vulnerabilities
# ========================================
npm-audit:
image: node:20-alpine
commands:
- cd website && npm audit --json > ../npm-audit-website.json || true
- cd ../studio-v2 && npm audit --json > ../npm-audit-studio.json || true
- cd ../admin-v2 && npm audit --json > ../npm-audit-admin.json || true
- echo "NPM Audit abgeschlossen"
when:
event: [pull_request, cron]
pip-audit:
image: python:3.12-slim
commands:
- pip install --quiet pip-audit
- pip-audit -r backend/requirements.txt --format json -o pip-audit-backend.json || true
- pip-audit -r voice-service/requirements.txt --format json -o pip-audit-voice.json || true
- echo "Pip Audit abgeschlossen"
when:
event: [pull_request, cron]
go-vulncheck:
image: golang:1.21-alpine
commands:
- go install golang.org/x/vuln/cmd/govulncheck@latest
- cd consent-service && govulncheck ./... || true
- cd ../billing-service && govulncheck ./... || true
- echo "Go Vulncheck abgeschlossen"
when:
event: [pull_request, cron]
# ========================================
# Container Security
# ========================================
trivy-filesystem:
image: aquasec/trivy:latest
commands:
- trivy fs --severity HIGH,CRITICAL --format json -o trivy-fs.json . || true
- echo "Trivy Filesystem Scan abgeschlossen"
when:
event: cron
# ========================================
# SBOM Generation (taeglich)
# ========================================
daily-sbom:
image: anchore/syft:latest
commands:
- mkdir -p sbom-reports
- syft dir:. -o cyclonedx-json > sbom-reports/sbom-full-$(date +%Y%m%d).json
- echo "SBOM generiert"
when:
event: cron
# ========================================
# AUTO-FIX: Dependency Vulnerabilities
# Laeuft nur bei Cron (nightly), nicht bei PRs
# ========================================
auto-fix-npm:
image: node:20-alpine
commands:
- apk add --no-cache git
- |
echo "=== Auto-Fix: NPM Dependencies ==="
FIXES_APPLIED=0
for dir in website studio-v2 admin-v2 h5p-service; do
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
echo "Pruefe $dir..."
cd $dir
# Speichere Hash vor Fix
BEFORE=$(md5sum package-lock.json 2>/dev/null || echo "none")
# npm audit fix (ohne --force fuer sichere Updates)
npm audit fix --package-lock-only 2>/dev/null || true
# Pruefe ob Aenderungen
AFTER=$(md5sum package-lock.json 2>/dev/null || echo "none")
if [ "$BEFORE" != "$AFTER" ]; then
echo " -> Fixes angewendet in $dir"
FIXES_APPLIED=$((FIXES_APPLIED + 1))
fi
cd ..
fi
done
echo "NPM Auto-Fix abgeschlossen: $FIXES_APPLIED Projekte aktualisiert"
echo "NPM_FIXES=$FIXES_APPLIED" >> /tmp/autofix-results.env
when:
event: cron
auto-fix-python:
image: python:3.12-slim
commands:
- apt-get update && apt-get install -y git
- pip install --quiet pip-audit
- |
echo "=== Auto-Fix: Python Dependencies ==="
FIXES_APPLIED=0
for reqfile in backend/requirements.txt voice-service/requirements.txt klausur-service/backend/requirements.txt; do
if [ -f "$reqfile" ]; then
echo "Pruefe $reqfile..."
DIR=$(dirname $reqfile)
# pip-audit mit --fix (aktualisiert requirements.txt)
pip-audit -r $reqfile --fix 2>/dev/null || true
# Pruefe ob requirements.txt geaendert wurde
if git diff --quiet $reqfile 2>/dev/null; then
echo " -> Keine Aenderungen in $reqfile"
else
echo " -> Fixes angewendet in $reqfile"
FIXES_APPLIED=$((FIXES_APPLIED + 1))
fi
fi
done
echo "Python Auto-Fix abgeschlossen: $FIXES_APPLIED Dateien aktualisiert"
echo "PYTHON_FIXES=$FIXES_APPLIED" >> /tmp/autofix-results.env
when:
event: cron
auto-fix-go:
image: golang:1.21-alpine
commands:
- apk add --no-cache git
- |
echo "=== Auto-Fix: Go Dependencies ==="
FIXES_APPLIED=0
for dir in consent-service billing-service school-service edu-search ai-compliance-sdk; do
if [ -d "$dir" ] && [ -f "$dir/go.mod" ]; then
echo "Pruefe $dir..."
cd $dir
# Go mod tidy und update
go get -u ./... 2>/dev/null || true
go mod tidy 2>/dev/null || true
# Pruefe ob go.mod/go.sum geaendert wurden
if git diff --quiet go.mod go.sum 2>/dev/null; then
echo " -> Keine Aenderungen in $dir"
else
echo " -> Updates angewendet in $dir"
FIXES_APPLIED=$((FIXES_APPLIED + 1))
fi
cd ..
fi
done
echo "Go Auto-Fix abgeschlossen: $FIXES_APPLIED Module aktualisiert"
echo "GO_FIXES=$FIXES_APPLIED" >> /tmp/autofix-results.env
when:
event: cron
# ========================================
# Commit & Push Auto-Fixes
# ========================================
commit-security-fixes:
image: alpine/git:latest
commands:
- |
echo "=== Commit Security Fixes ==="
# Git konfigurieren
git config --global user.email "security-bot@breakpilot.de"
git config --global user.name "Security Bot"
git config --global --add safe.directory /woodpecker/src
# Pruefe ob es Aenderungen gibt
if git diff --quiet && git diff --cached --quiet; then
echo "Keine Security-Fixes zum Committen"
exit 0
fi
# Zeige was geaendert wurde
echo "Geaenderte Dateien:"
git status --short
# Stage alle relevanten Dateien
git add -A \
*/package-lock.json \
*/requirements.txt \
*/go.mod \
*/go.sum \
2>/dev/null || true
# Commit erstellen
TIMESTAMP=$(date +%Y-%m-%d)
git commit -m "fix(security): auto-fix vulnerable dependencies [$TIMESTAMP]
Automatische Sicherheitsupdates durch CI/CD Pipeline:
- npm audit fix fuer Node.js Projekte
- pip-audit --fix fuer Python Projekte
- go get -u fuer Go Module
Co-Authored-By: Security Bot <security-bot@breakpilot.de>" || echo "Nichts zu committen"
# Push zum Repository
git push origin HEAD:main || echo "Push fehlgeschlagen - manueller Review erforderlich"
echo "Security-Fixes committed und gepusht"
when:
event: cron
status: success
# ========================================
# Report to Dashboard
# ========================================
update-security-dashboard:
image: curlimages/curl:latest
commands:
- |
curl -X POST "http://backend:8000/api/security/scan-results" \
-H "Content-Type: application/json" \
-d "{
\"scan_type\": \"daily\",
\"timestamp\": \"$(date -Iseconds)\",
\"tools\": [\"semgrep\", \"bandit\", \"gosec\", \"gitleaks\", \"trivy\"]
}" || true
when:
status: [success, failure]
event: cron

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,566 @@
# BreakPilot Consent Management System - Projektplan
## Executive Summary
Dieses Dokument beschreibt den Plan zur Entwicklung eines vollständigen Consent Management Systems (CMS) für BreakPilot. Das System wird komplett neu entwickelt und ersetzt das bestehende Policy Vault System, das Bugs enthält und nicht optimal funktioniert.
---
## Technologie-Entscheidung: Warum welche Sprache?
### Backend-Optionen im Vergleich
| Kriterium | Rust | Go | Python (FastAPI) | TypeScript (NestJS) |
|-----------|------|-----|------------------|---------------------|
| **Performance** | Exzellent | Sehr gut | Gut | Gut |
| **Memory Safety** | Garantiert | GC | GC | GC |
| **Entwicklungsgeschwindigkeit** | Langsam | Mittel | Schnell | Schnell |
| **Lernkurve** | Steil | Flach | Flach | Mittel |
| **Ecosystem für Web** | Wachsend | Sehr gut | Exzellent | Exzellent |
| **Integration mit BreakPilot** | Neu | Neu | Bereits vorhanden | Möglich |
| **Team-Erfahrung** | ? | ? | Vorhanden | Möglich |
### Empfehlung: **Python (FastAPI)** oder **Go**
#### Option A: Python mit FastAPI (Empfohlen für schnelle Integration)
**Vorteile:**
- Bereits im BreakPilot-Projekt verwendet
- Schnelle Entwicklung
- Exzellente Dokumentation (automatisch generiert)
- Einfache Integration mit bestehendem Code
- Type Hints für bessere Code-Qualität
- Async/Await Support
**Nachteile:**
- Langsamer als Rust/Go bei hoher Last
- GIL-Einschränkungen bei CPU-intensiven Tasks
#### Option B: Go (Empfohlen für Microservice-Architektur)
**Vorteile:**
- Extrem schnell und effizient
- Exzellent für Microservices
- Einfache Deployment (Single Binary)
- Gute Concurrency
- Statische Typisierung
**Nachteile:**
- Neuer Tech-Stack im Projekt
- Getrennte Codebasis von BreakPilot
#### Option C: Rust (Für maximale Performance & Sicherheit)
**Vorteile:**
- Höchste Performance
- Memory Safety ohne GC
- Exzellente Sicherheit
- WebAssembly-Support
**Nachteile:**
- Sehr steile Lernkurve
- Längere Entwicklungszeit (2-3x)
- Kleineres Web-Ecosystem
- Komplexere Fehlerbehandlung
### Finale Empfehlung
**Für BreakPilot empfehle ich: Go (Golang)**
Begründung:
1. **Unabhängiger Microservice** - Das CMS sollte als eigenständiger Service laufen
2. **Performance** - Consent-Checks müssen schnell sein (bei jedem API-Call)
3. **Einfaches Deployment** - Single Binary, ideal für Container
4. **Gute Balance** - Schneller als Python, einfacher als Rust
5. **Zukunftssicher** - Moderne Sprache mit wachsendem Ecosystem
---
## Systemarchitektur
```
┌─────────────────────────────────────────────────────────────────────────┐
│ BreakPilot Ecosystem │
├─────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ BreakPilot │ │ Consent Admin │ │ BreakPilot │ │
│ │ Studio (Web) │ │ Dashboard │ │ Mobile Apps │ │
│ │ (Python/HTML) │ │ (Vue.js/React) │ │ (iOS/Android) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ API Gateway / Proxy │ │
│ └────────────┬────────────┘ │
│ │ │
│ ┌─────────────────────┼─────────────────────┐ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ BreakPilot API │ │ Consent Service │ │ Auth Service │ │
│ │ (Python/FastAPI)│ │ (Go) │ │ (Go) │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────┐ │
│ │ PostgreSQL │ │
│ │ (Shared Database) │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
```
---
## Projektphasen
### Phase 1: Grundlagen & Datenbank (Woche 1-2)
**Ziel:** Datenbank-Schema und Basis-Services
#### 1.1 Datenbank-Design
- [ ] Users-Tabelle (Integration mit BreakPilot Auth)
- [ ] Legal Documents (AGB, Datenschutz, Community Guidelines, etc.)
- [ ] Document Versions (Versionierung mit Freigabe-Workflow)
- [ ] User Consents (Welcher User hat wann was zugestimmt)
- [ ] Cookie Categories (Notwendig, Funktional, Marketing, Analytics)
- [ ] Cookie Consents (Granulare Cookie-Zustimmungen)
- [ ] Audit Log (DSGVO-konforme Protokollierung)
#### 1.2 Go Backend Setup
- [ ] Projekt-Struktur mit Clean Architecture
- [ ] Database Layer (sqlx oder GORM)
- [ ] Migration System
- [ ] Config Management
- [ ] Logging & Error Handling
### Phase 2: Core Consent Service (Woche 3-4)
**Ziel:** Kern-Funktionalität für Consent-Management
#### 2.1 Document Management API
- [ ] CRUD für Legal Documents
- [ ] Versionierung mit Diff-Tracking
- [ ] Draft/Published/Archived Status
- [ ] Mehrsprachigkeit (DE, EN, etc.)
#### 2.2 Consent Tracking API
- [ ] User Consent erstellen/abrufen
- [ ] Consent History pro User
- [ ] Bulk-Consent für mehrere Dokumente
- [ ] Consent Withdrawal (Widerruf)
#### 2.3 Cookie Consent API
- [ ] Cookie-Kategorien verwalten
- [ ] Granulare Cookie-Einstellungen
- [ ] Consent-Banner Konfiguration
### Phase 3: Admin Dashboard (Woche 5-6)
**Ziel:** Web-Interface für Administratoren
#### 3.1 Admin Frontend (Vue.js oder React)
- [ ] Login/Auth (Integration mit BreakPilot)
- [ ] Dashboard mit Statistiken
- [ ] Document Editor (Rich Text)
- [ ] Version Management UI
- [ ] User Consent Übersicht
- [ ] Cookie Management UI
#### 3.2 Freigabe-Workflow
- [ ] Draft → Review → Approved → Published
- [ ] Benachrichtigungen bei neuen Versionen
- [ ] Rollback-Funktion
### Phase 4: BreakPilot Integration (Woche 7-8)
**Ziel:** Integration in BreakPilot Studio
#### 4.1 User-facing Features
- [ ] "Legal" Button in Einstellungen
- [ ] Consent-Historie anzeigen
- [ ] Cookie-Präferenzen ändern
- [ ] Datenauskunft anfordern (DSGVO Art. 15)
#### 4.2 Cookie Banner
- [ ] Cookie-Consent-Modal beim ersten Besuch
- [ ] Granulare Auswahl der Kategorien
- [ ] "Alle akzeptieren" / "Nur notwendige"
- [ ] Persistente Speicherung
#### 4.3 Consent-Check Middleware
- [ ] Automatische Prüfung bei API-Calls
- [ ] Blocking bei fehlender Zustimmung
- [ ] Marketing-Opt-out respektieren
### Phase 5: Data Subject Rights (Woche 9-10)
**Ziel:** DSGVO-Compliance Features
#### 5.1 Datenauskunft (Art. 15 DSGVO)
- [ ] API für "Welche Daten haben wir?"
- [ ] Export als JSON/PDF
- [ ] Automatisierte Bereitstellung
#### 5.2 Datenlöschung (Art. 17 DSGVO)
- [ ] "Recht auf Vergessenwerden"
- [ ] Anonymisierung statt Löschung (wo nötig)
- [ ] Audit Trail für Löschungen
#### 5.3 Datenportabilität (Art. 20 DSGVO)
- [ ] Export in maschinenlesbarem Format
- [ ] Download-Funktion im Frontend
### Phase 6: Testing & Security (Woche 11-12)
**Ziel:** Absicherung und Qualität
#### 6.1 Testing
- [ ] Unit Tests (>80% Coverage)
- [ ] Integration Tests
- [ ] E2E Tests für kritische Flows
- [ ] Performance Tests
#### 6.2 Security
- [ ] Security Audit
- [ ] Penetration Testing
- [ ] Rate Limiting
- [ ] Input Validation
- [ ] SQL Injection Prevention
- [ ] XSS Protection
---
## Datenbank-Schema (Entwurf)
```sql
-- Benutzer (Integration mit BreakPilot)
CREATE TABLE users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
external_id VARCHAR(255) UNIQUE, -- BreakPilot User ID
email VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Rechtliche Dokumente
CREATE TABLE legal_documents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
type VARCHAR(50) NOT NULL, -- 'terms', 'privacy', 'cookies', 'community'
name VARCHAR(255) NOT NULL,
description TEXT,
is_mandatory BOOLEAN DEFAULT true,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW()
);
-- Dokumentversionen
CREATE TABLE document_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
document_id UUID REFERENCES legal_documents(id) ON DELETE CASCADE,
version VARCHAR(20) NOT NULL, -- Semver: 1.0.0, 1.1.0, etc.
language VARCHAR(5) DEFAULT 'de', -- ISO 639-1
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL, -- HTML oder Markdown
summary TEXT, -- Kurze Zusammenfassung der Änderungen
status VARCHAR(20) DEFAULT 'draft', -- draft, review, approved, published, archived
published_at TIMESTAMPTZ,
created_by UUID REFERENCES users(id),
approved_by UUID REFERENCES users(id),
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(document_id, version, language)
);
-- Benutzer-Zustimmungen
CREATE TABLE user_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
document_version_id UUID REFERENCES document_versions(id),
consented BOOLEAN NOT NULL,
ip_address INET,
user_agent TEXT,
consented_at TIMESTAMPTZ DEFAULT NOW(),
withdrawn_at TIMESTAMPTZ,
UNIQUE(user_id, document_version_id)
);
-- Cookie-Kategorien
CREATE TABLE cookie_categories (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(100) NOT NULL, -- 'necessary', 'functional', 'analytics', 'marketing'
display_name_de VARCHAR(255) NOT NULL,
display_name_en VARCHAR(255),
description_de TEXT,
description_en TEXT,
is_mandatory BOOLEAN DEFAULT false,
sort_order INT DEFAULT 0,
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Cookie-Zustimmungen
CREATE TABLE cookie_consents (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID REFERENCES users(id) ON DELETE CASCADE,
category_id UUID REFERENCES cookie_categories(id),
consented BOOLEAN NOT NULL,
consented_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
UNIQUE(user_id, category_id)
);
-- Audit Log (DSGVO-konform)
CREATE TABLE consent_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID,
action VARCHAR(50) NOT NULL, -- 'consent_given', 'consent_withdrawn', 'data_export', 'data_delete'
entity_type VARCHAR(50), -- 'document', 'cookie_category'
entity_id UUID,
details JSONB,
ip_address INET,
user_agent TEXT,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Indizes für Performance
CREATE INDEX idx_user_consents_user ON user_consents(user_id);
CREATE INDEX idx_user_consents_version ON user_consents(document_version_id);
CREATE INDEX idx_cookie_consents_user ON cookie_consents(user_id);
CREATE INDEX idx_audit_log_user ON consent_audit_log(user_id);
CREATE INDEX idx_audit_log_created ON consent_audit_log(created_at);
```
---
## API-Endpoints (Entwurf)
### Public API (für BreakPilot Frontend)
```
# Dokumente abrufen
GET /api/v1/documents # Alle aktiven Dokumente
GET /api/v1/documents/:type # Dokument nach Typ (terms, privacy)
GET /api/v1/documents/:type/latest # Neueste publizierte Version
# Consent Management
POST /api/v1/consent # Zustimmung erteilen
GET /api/v1/consent/my # Meine Zustimmungen
GET /api/v1/consent/check/:documentType # Prüfen ob zugestimmt
DELETE /api/v1/consent/:id # Zustimmung widerrufen
# Cookie Consent
GET /api/v1/cookies/categories # Cookie-Kategorien
POST /api/v1/cookies/consent # Cookie-Präferenzen setzen
GET /api/v1/cookies/consent/my # Meine Cookie-Einstellungen
# Data Subject Rights (DSGVO)
GET /api/v1/privacy/my-data # Alle meine Daten abrufen
POST /api/v1/privacy/export # Datenexport anfordern
POST /api/v1/privacy/delete # Löschung anfordern
```
### Admin API (für Admin Dashboard)
```
# Document Management
GET /api/v1/admin/documents # Alle Dokumente (mit Drafts)
POST /api/v1/admin/documents # Neues Dokument
PUT /api/v1/admin/documents/:id # Dokument bearbeiten
DELETE /api/v1/admin/documents/:id # Dokument löschen
# Version Management
GET /api/v1/admin/versions/:docId # Alle Versionen eines Dokuments
POST /api/v1/admin/versions # Neue Version erstellen
PUT /api/v1/admin/versions/:id # Version bearbeiten
POST /api/v1/admin/versions/:id/publish # Version veröffentlichen
POST /api/v1/admin/versions/:id/archive # Version archivieren
# Cookie Categories
GET /api/v1/admin/cookies/categories # Alle Kategorien
POST /api/v1/admin/cookies/categories # Neue Kategorie
PUT /api/v1/admin/cookies/categories/:id
DELETE /api/v1/admin/cookies/categories/:id
# Statistics & Reports
GET /api/v1/admin/stats/consents # Consent-Statistiken
GET /api/v1/admin/stats/cookies # Cookie-Statistiken
GET /api/v1/admin/audit-log # Audit Log (mit Filter)
```
---
## Consent-Check Middleware (Konzept)
```go
// middleware/consent_check.go
func ConsentCheckMiddleware(requiredConsent string) gin.HandlerFunc {
return func(c *gin.Context) {
userID := c.GetString("user_id")
// Prüfe ob User zugestimmt hat
hasConsent, err := consentService.CheckConsent(userID, requiredConsent)
if err != nil {
c.AbortWithStatusJSON(500, gin.H{"error": "Consent check failed"})
return
}
if !hasConsent {
c.AbortWithStatusJSON(403, gin.H{
"error": "consent_required",
"document_type": requiredConsent,
"message": "Sie müssen den Nutzungsbedingungen zustimmen",
})
return
}
c.Next()
}
}
// Verwendung in BreakPilot
router.POST("/api/worksheets",
authMiddleware,
ConsentCheckMiddleware("terms"),
worksheetHandler.Create,
)
```
---
## Cookie-Banner Flow
```
┌─────────────────────────────────────────────────────────────┐
│ Erster Besuch │
├─────────────────────────────────────────────────────────────┤
│ │
│ 1. User öffnet BreakPilot │
│ │ │
│ ▼ │
│ 2. Check: Hat User Cookie-Consent gegeben? │
│ │ │
│ ┌─────────┴─────────┐ │
│ │ Nein │ Ja │
│ ▼ ▼ │
│ 3. Zeige Cookie Lade gespeicherte │
│ Banner Präferenzen │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────┐ │
│ │ Cookie Consent Banner │ │
│ ├─────────────────────────────────────────┤ │
│ │ Wir verwenden Cookies, um Ihnen die │ │
│ │ beste Erfahrung zu bieten. │ │
│ │ │ │
│ │ ☑ Notwendig (immer aktiv) │ │
│ │ ☐ Funktional │ │
│ │ ☐ Analytics │ │
│ │ ☐ Marketing │ │
│ │ │ │
│ │ [Alle akzeptieren] [Auswahl speichern] │ │
│ │ [Nur notwendige] [Mehr erfahren] │ │
│ └─────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Legal-Bereich im BreakPilot Frontend (Mockup)
```
┌─────────────────────────────────────────────────────────────┐
│ Einstellungen > Legal │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Meine Zustimmungen │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ✓ Allgemeine Geschäftsbedingungen │ │
│ │ Version 2.1 · Zugestimmt am 15.11.2024 │ │
│ │ [Ansehen] [Widerrufen] │ │
│ │ │ │
│ │ ✓ Datenschutzerklärung │ │
│ │ Version 3.0 · Zugestimmt am 15.11.2024 │ │
│ │ [Ansehen] [Widerrufen] │ │
│ │ │ │
│ │ ✓ Community Guidelines │ │
│ │ Version 1.2 · Zugestimmt am 15.11.2024 │ │
│ │ [Ansehen] [Widerrufen] │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Cookie-Einstellungen │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ ☑ Notwendige Cookies (erforderlich) │ │
│ │ ☑ Funktionale Cookies │ │
│ │ ☐ Analytics Cookies │ │
│ │ ☐ Marketing Cookies │ │
│ │ │ │
│ │ [Einstellungen speichern] │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Meine Daten (DSGVO) │ │
│ ├─────────────────────────────────────────────────────┤ │
│ │ │ │
│ │ [Meine Daten exportieren] │ │
│ │ Erhalten Sie eine Kopie aller Ihrer gespeicherten │ │
│ │ Daten als JSON-Datei. │ │
│ │ │ │
│ │ [Account löschen] │ │
│ │ Alle Ihre Daten werden unwiderruflich gelöscht. │ │
│ │ │ │
│ └─────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
```
---
## Nächste Schritte
### Sofort (diese Woche)
1. **Entscheidung:** Go oder Python für Backend?
2. **Projekt-Setup:** Repository anlegen
3. **Datenbank:** Schema finalisieren und migrieren
### Kurzfristig (nächste 2 Wochen)
1. Core API implementieren
2. Basis-Integration in BreakPilot
### Mittelfristig (nächste 4-6 Wochen)
1. Admin Dashboard
2. Cookie Banner
3. DSGVO-Features
---
## Offene Fragen
1. **Sprache:** Go oder Python für das Backend?
2. **Admin Dashboard:** Eigenes Frontend oder in BreakPilot integriert?
3. **Hosting:** Gleicher Server wie BreakPilot oder separater Service?
4. **Auth:** Shared Authentication mit BreakPilot oder eigenes System?
5. **Datenbank:** Shared PostgreSQL oder eigene Instanz?
---
## Ressourcen-Schätzung
| Phase | Aufwand (Tage) | Beschreibung |
|-------|---------------|--------------|
| Phase 1 | 5-7 | Datenbank & Setup |
| Phase 2 | 8-10 | Core Consent Service |
| Phase 3 | 10-12 | Admin Dashboard |
| Phase 4 | 8-10 | BreakPilot Integration |
| Phase 5 | 5-7 | DSGVO Features |
| Phase 6 | 5-7 | Testing & Security |
| **Gesamt** | **41-53** | ~8-10 Wochen |
---
*Dokument erstellt am: 12. Dezember 2024*
*Version: 1.0*

473
CONTENT_SERVICE_SETUP.md Normal file
View File

@@ -0,0 +1,473 @@
# BreakPilot Content Service - Setup & Deployment Guide
## 🎯 Übersicht
Der BreakPilot Content Service ist eine vollständige Educational Content Management Plattform mit:
-**Content Service API** (FastAPI) - Educational Content Management
-**MinIO S3 Storage** - File Storage für Videos, PDFs, Bilder
-**H5P Service** - Interactive Educational Content (Quizzes, etc.)
-**Matrix Feed Integration** - Content Publishing zu Matrix Spaces
-**PostgreSQL** - Content Metadata Storage
-**Creative Commons Licensing** - CC-BY, CC-BY-SA, etc.
-**Rating & Download Tracking** - Analytics & Impact Scoring
## 🚀 Quick Start
### 1. Alle Services starten
```bash
# Haupt-Services + Content Services starten
docker-compose \
-f docker-compose.yml \
-f docker-compose.content.yml \
up -d
# Logs verfolgen
docker-compose -f docker-compose.yml -f docker-compose.content.yml logs -f
```
### 2. Verfügbare Services
| Service | URL | Beschreibung |
|---------|-----|--------------|
| Content Service API | http://localhost:8002 | REST API für Content Management |
| MinIO Console | http://localhost:9001 | Storage Dashboard (User: minioadmin, Pass: minioadmin123) |
| H5P Service | http://localhost:8003 | Interactive Content Editor |
| Content DB | localhost:5433 | PostgreSQL Database |
### 3. API Dokumentation
Content Service API Docs:
```
http://localhost:8002/docs
```
## 📦 Installation (Development)
### Content Service (Backend)
```bash
cd backend/content_service
# Virtual Environment erstellen
python3 -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
# Dependencies installieren
pip install -r requirements.txt
# Environment Variables
cp .env.example .env
# Database Migrations
alembic upgrade head
# Service starten
uvicorn main:app --reload --port 8002
```
### H5P Service
```bash
cd h5p-service
# Dependencies installieren
npm install
# Service starten
npm start
```
### Creator Dashboard (Frontend)
```bash
cd frontend/creator-studio
# Dependencies installieren
npm install
# Development Server
npm run dev
```
## 🔧 Konfiguration
### Environment Variables
Erstelle `.env` im Projekt-Root:
```env
# Content Service
CONTENT_DB_URL=postgresql://breakpilot:breakpilot123@localhost:5433/breakpilot_content
MINIO_ENDPOINT=localhost:9000
MINIO_ACCESS_KEY=minioadmin
MINIO_SECRET_KEY=minioadmin123
MINIO_BUCKET=breakpilot-content
# Matrix Integration
MATRIX_HOMESERVER=http://localhost:8008
MATRIX_ACCESS_TOKEN=your-matrix-token-here
MATRIX_BOT_USER=@breakpilot-bot:localhost
MATRIX_FEED_ROOM=!breakpilot-feed:localhost
# OAuth2 (consent-service)
CONSENT_SERVICE_URL=http://localhost:8081
JWT_SECRET=your-jwt-secret-here
# H5P Service
H5P_BASE_URL=http://localhost:8003
H5P_STORAGE_PATH=/app/h5p-content
```
## 📝 Content Service API Endpoints
### Content Management
```bash
# Create Content
POST /api/v1/content
{
"title": "5-Minuten Yoga für Grundschule",
"description": "Bewegungspause mit einfachen Yoga-Übungen",
"content_type": "video",
"category": "movement",
"license": "CC-BY-SA-4.0",
"age_min": 6,
"age_max": 10,
"tags": ["yoga", "bewegung", "pause"]
}
# Upload File
POST /api/v1/upload
Content-Type: multipart/form-data
file: <video-file>
# Add Files to Content
POST /api/v1/content/{content_id}/files
{
"file_urls": ["http://minio:9000/breakpilot-content/..."]
}
# Publish Content (→ Matrix Feed)
POST /api/v1/content/{content_id}/publish
# List Content (with filters)
GET /api/v1/content?category=movement&age_min=6&age_max=10
# Get Content Details
GET /api/v1/content/{content_id}
# Rate Content
POST /api/v1/content/{content_id}/rate
{
"stars": 5,
"comment": "Sehr hilfreich für meine Klasse!"
}
```
### H5P Interactive Content
```bash
# Get H5P Editor
GET http://localhost:8003/h5p/editor/new
# Save H5P Content
POST http://localhost:8003/h5p/editor
{
"library": "H5P.InteractiveVideo 1.22",
"params": { ... }
}
# Play H5P Content
GET http://localhost:8003/h5p/play/{contentId}
# Export as .h5p File
GET http://localhost:8003/h5p/export/{contentId}
```
## 🎨 Creator Workflow
### 1. Content erstellen
```javascript
// Creator Dashboard
const content = await createContent({
title: "Mathe-Quiz: Einmaleins",
description: "Interaktives Quiz zum Üben des Einmaleins",
content_type: "h5p",
category: "math",
license: "CC-BY-SA-4.0",
age_min: 7,
age_max: 9
});
```
### 2. Files hochladen
```javascript
// Upload Video/PDF/Images
const file = document.querySelector('#fileInput').files[0];
const formData = new FormData();
formData.append('file', file);
const response = await fetch('/api/v1/upload', {
method: 'POST',
body: formData
});
const { file_url } = await response.json();
```
### 3. Publish to Matrix Feed
```javascript
// Publish → Matrix Spaces
await publishContent(content.id);
// → Content erscheint in #movement, #math, etc.
```
## 📊 Matrix Feed Integration
### Matrix Spaces Struktur
```
#breakpilot (Root Space)
├── #feed (Chronologischer Content Feed)
├── #bewegung (Kategorie: Movement)
├── #mathe (Kategorie: Math)
├── #steam (Kategorie: STEAM)
└── #sprache (Kategorie: Language)
```
### Content Message Format
Wenn Content published wird, erscheint in Matrix:
```
📹 5-Minuten Yoga für Grundschule
Bewegungspause mit einfachen Yoga-Übungen für den Unterricht
📝 Von: Max Mustermann
🏃 Kategorie: movement
👥 Alter: 6-10 Jahre
⚖️ Lizenz: CC-BY-SA-4.0
🏷️ Tags: yoga, bewegung, pause
[📥 Inhalt ansehen/herunterladen]
```
## 🔐 Creative Commons Lizenzen
Verfügbare Lizenzen:
- `CC-BY-4.0` - Attribution (Namensnennung)
- `CC-BY-SA-4.0` - Attribution + ShareAlike (empfohlen)
- `CC-BY-NC-4.0` - Attribution + NonCommercial
- `CC-BY-NC-SA-4.0` - Attribution + NonCommercial + ShareAlike
- `CC0-1.0` - Public Domain
### Lizenz-Workflow
```python
# Bei Content-Erstellung: Creator wählt Lizenz
content.license = "CC-BY-SA-4.0"
# System validiert:
Nur erlaubte Lizenzen
Lizenz-Badge wird angezeigt
Lizenz-Link zu Creative Commons
```
## 📈 Analytics & Impact Scoring
### Download Tracking
```python
# Automatisch getrackt bei Download
POST /api/v1/content/{content_id}/download
# → Zähler erhöht
# → Download-Event gespeichert
# → Für Impact-Score verwendet
```
### Creator Statistics
```bash
# Get Creator Stats
GET /api/v1/stats/creator/{creator_id}
{
"total_contents": 12,
"total_downloads": 347,
"total_views": 1203,
"avg_rating": 4.7,
"impact_score": 892.5,
"content_breakdown": {
"movement": 5,
"math": 4,
"steam": 3
}
}
```
## 🧪 Testing
### API Tests
```bash
# Pytest
cd backend/content_service
pytest tests/
# Mit Coverage
pytest --cov=. --cov-report=html
```
### Integration Tests
```bash
# Test Content Upload Flow
curl -X POST http://localhost:8002/api/v1/content \
-H "Content-Type: application/json" \
-d '{
"title": "Test Content",
"content_type": "pdf",
"category": "math",
"license": "CC-BY-SA-4.0"
}'
```
## 🐳 Docker Commands
```bash
# Build einzelnen Service
docker-compose -f docker-compose.content.yml build content-service
# Nur Content Services starten
docker-compose -f docker-compose.content.yml up -d
# Logs einzelner Service
docker-compose logs -f content-service
# Service neu starten
docker-compose restart content-service
# Alle stoppen
docker-compose -f docker-compose.yml -f docker-compose.content.yml down
# Mit Volumes löschen (Achtung: Datenverlust!)
docker-compose -f docker-compose.yml -f docker-compose.content.yml down -v
```
## 🗄️ Database Migrations
```bash
cd backend/content_service
# Neue Migration erstellen
alembic revision --autogenerate -m "Add new field"
# Migration anwenden
alembic upgrade head
# Zurückrollen
alembic downgrade -1
```
## 📱 Frontend Development
### Creator Studio
```bash
cd frontend/creator-studio
# Install dependencies
npm install
# Development
npm run dev # → http://localhost:3000
# Build
npm run build
# Preview Production Build
npm run preview
```
## 🔒 DSGVO Compliance
### Datenminimierung
- ✅ Nur notwendige Metadaten gespeichert
- ✅ Keine Schülerdaten
- ✅ IP-Adressen anonymisiert nach 7 Tagen
- ✅ User kann Content/Account löschen
### Datenexport
```bash
# User Data Export
GET /api/v1/user/export
→ JSON mit allen Daten des Users
```
## 🚨 Troubleshooting
### MinIO Connection Failed
```bash
# Check MinIO status
docker-compose logs minio
# Test connection
curl http://localhost:9000/minio/health/live
```
### Content Service Database Connection
```bash
# Check PostgreSQL
docker-compose logs content-db
# Connect manually
docker exec -it breakpilot-pwa-content-db psql -U breakpilot -d breakpilot_content
```
### H5P Service Not Starting
```bash
# Check logs
docker-compose logs h5p-service
# Rebuild
docker-compose build h5p-service
docker-compose up -d h5p-service
```
## 📚 Weitere Dokumentation
- [Architekturempfehlung](./backend/docs/Architekturempfehlung%20für%20Breakpilot%20%20Offene,%20modulare%20Bildungsplattform%20im%20DACH-Raum.pdf)
- [Content Service API](./backend/content_service/README.md)
- [H5P Integration](./h5p-service/README.md)
- [Matrix Feed Setup](./docs/matrix-feed-setup.md)
## 🎉 Next Steps
1. ✅ Services starten (siehe Quick Start)
2. ✅ Creator Account erstellen
3. ✅ Ersten Content hochladen
4. ✅ H5P Interactive Content erstellen
5. ✅ Content publishen → Matrix Feed
6. ✅ Teacher Discovery UI testen
7. 🔜 OAuth2 SSO mit consent-service integrieren
8. 🔜 Production Deployment vorbereiten
## 💡 Support
Bei Fragen oder Problemen:
- GitHub Issues: https://github.com/breakpilot/breakpilot-pwa/issues
- Matrix Chat: #breakpilot-dev:matrix.org
- Email: dev@breakpilot.app

427
IMPLEMENTATION_SUMMARY.md Normal file
View File

@@ -0,0 +1,427 @@
# 🎓 BreakPilot Content Service - Implementierungs-Zusammenfassung
## ✅ Vollständig implementierte Sprints
### **Sprint 1-2: Content Service Foundation** ✅
**Backend (FastAPI):**
- ✅ Complete Database Schema (PostgreSQL)
- `Content` Model mit allen Metadaten
- `Rating` Model für Teacher Reviews
- `Tag` System für Content Organization
- `Download` Tracking für Impact Scoring
- ✅ Pydantic Schemas für API Validation
- ✅ Full CRUD API für Content Management
- ✅ Upload API für Files (Video, PDF, Images, Audio)
- ✅ Search & Filter Endpoints
- ✅ Analytics & Statistics Endpoints
**Storage:**
- ✅ MinIO S3-kompatible Object Storage
- ✅ Automatic Bucket Creation
- ✅ Public Read Policy für Content
- ✅ File Upload Integration
- ✅ Presigned URLs für private Files
**Files Created:**
```
backend/content_service/
├── models.py # Database Models
├── schemas.py # Pydantic Schemas
├── database.py # DB Configuration
├── main.py # FastAPI Application
├── storage.py # MinIO Integration
├── requirements.txt # Python Dependencies
└── Dockerfile # Container Definition
```
---
### **Sprint 3-4: Matrix Feed Integration** ✅
**Matrix Client:**
- ✅ Matrix SDK Integration (matrix-nio)
- ✅ Content Publishing to Matrix Spaces
- ✅ Formatted Messages (Plain Text + HTML)
- ✅ Category-based Room Routing
- ✅ Rich Metadata for Content
- ✅ Reactions & Threading Support
**Matrix Spaces Struktur:**
```
#breakpilot:server.de (Root Space)
├── #feed (Chronologischer Content Feed)
├── #bewegung (Movement Category)
├── #mathe (Math Category)
├── #steam (STEAM Category)
└── #sprache (Language Category)
```
**Files Created:**
```
backend/content_service/
└── matrix_client.py # Matrix Integration
```
**Features:**
- ✅ Auto-publish on Content.status = PUBLISHED
- ✅ Rich HTML Formatting mit Thumbnails
- ✅ CC License Badges in Messages
- ✅ Direct Links zu Content
- ✅ Category-specific Posting
---
### **Sprint 5-6: Rating & Download Tracking** ✅
**Rating System:**
- ✅ 5-Star Rating System
- ✅ Text Comments
- ✅ Average Rating Calculation
- ✅ Rating Count Tracking
- ✅ One Rating per User (Update möglich)
**Download Tracking:**
- ✅ Event-based Download Logging
- ✅ User-specific Tracking
- ✅ IP Anonymization (nach 7 Tagen)
- ✅ Download Counter
- ✅ Impact Score Foundation
**Analytics:**
- ✅ Platform-wide Statistics
- ✅ Creator Statistics
- ✅ Content Breakdown by Category
- ✅ Downloads, Views, Ratings
---
### **Sprint 7-8: H5P Interactive Content** ✅
**H5P Service (Node.js):**
- ✅ Self-hosted H5P Server
- ✅ H5P Editor Integration
- ✅ H5P Player
- ✅ File-based Content Storage
- ✅ Library Management
- ✅ Export as .h5p Files
- ✅ Import .h5p Files
**Supported H5P Content Types:**
- ✅ Interactive Video
- ✅ Course Presentation
- ✅ Quiz (Multiple Choice)
- ✅ Drag & Drop
- ✅ Timeline
- ✅ Memory Game
- ✅ Fill in the Blanks
- ✅ 50+ weitere Content Types
**Files Created:**
```
h5p-service/
├── server.js # H5P Express Server
├── package.json # Node Dependencies
└── Dockerfile # Container Definition
```
**Integration:**
- ✅ Content Service → H5P Service API
- ✅ H5P Content ID in Content Model
- ✅ Automatic Publishing to Matrix
---
### **Sprint 7-8: Creative Commons Licensing** ✅
**Lizenz-System:**
- ✅ CC-BY-4.0
- ✅ CC-BY-SA-4.0 (Recommended)
- ✅ CC-BY-NC-4.0
- ✅ CC-BY-NC-SA-4.0
- ✅ CC0-1.0 (Public Domain)
**Features:**
- ✅ License Validation bei Upload
- ✅ License Selector in Creator Studio
- ✅ License Badges in UI
- ✅ Direct Links zu Creative Commons
- ✅ Matrix Messages mit License Info
---
### **Sprint 7-8: DSGVO Compliance** ✅
**Privacy by Design:**
- ✅ Datenminimierung (nur notwendige Daten)
- ✅ EU Server Hosting
- ✅ IP Anonymization
- ✅ User Data Export API
- ✅ Account Deletion
- ✅ No Schülerdaten
**Transparency:**
- ✅ Clear License Information
- ✅ Open Source Code
- ✅ Transparent Analytics
---
## 🐳 Docker Infrastructure
**docker-compose.content.yml:**
```yaml
Services:
- minio (Object Storage)
- content-db (PostgreSQL)
- content-service (FastAPI)
- h5p-service (Node.js H5P)
Volumes:
- minio_data
- content_db_data
- h5p_content
Networks:
- breakpilot-pwa-network (external)
```
---
## 📊 Architektur-Übersicht
```
┌─────────────────────────────────────────────────────────┐
│ BREAKPILOT CONTENT PLATFORM │
├─────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌───────────┐ │
│ │ Creator │───▶│ Content │───▶│ Matrix │ │
│ │ Studio │ │ Service │ │ Feed │ │
│ │ (Vue.js) │ │ (FastAPI) │ │ (Synapse) │ │
│ └──────────────┘ └──────┬───────┘ └───────────┘ │
│ │ │
│ ┌────────┴────────┐ │
│ │ │ │
│ ┌──────▼─────┐ ┌─────▼─────┐ │
│ │ MinIO │ │ H5P │ │
│ │ Storage │ │ Service │ │
│ └────────────┘ └───────────┘ │
│ │ │ │
│ ┌──────▼─────────────────▼─────┐ │
│ │ PostgreSQL Database │ │
│ └──────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌───────────┐ │
│ │ Teacher │────────────────────────▶│ Content │ │
│ │ Discovery │ Search & Download │ Player │ │
│ │ UI │ │ │ │
│ └──────────────┘ └───────────┘ │
└─────────────────────────────────────────────────────────┘
```
---
## 🚀 Deployment
### Quick Start
```bash
# 1. Startup Script ausführbar machen
chmod +x scripts/start-content-services.sh
# 2. Alle Services starten
./scripts/start-content-services.sh
# ODER manuell:
docker-compose \
-f docker-compose.yml \
-f docker-compose.content.yml \
up -d
```
### URLs nach Start
| Service | URL | Credentials |
|---------|-----|-------------|
| Content Service API | http://localhost:8002/docs | - |
| MinIO Console | http://localhost:9001 | minioadmin / minioadmin123 |
| H5P Editor | http://localhost:8003/h5p/editor/new | - |
| Content Database | localhost:5433 | breakpilot / breakpilot123 |
---
## 📝 Content Creation Workflow
### 1. Creator erstellt Content
```javascript
// POST /api/v1/content
{
"title": "5-Minuten Yoga",
"description": "Bewegungspause für Grundschüler",
"content_type": "video",
"category": "movement",
"license": "CC-BY-SA-4.0",
"age_min": 6,
"age_max": 10,
"tags": ["yoga", "bewegung"]
}
```
### 2. Upload Media Files
```javascript
// POST /api/v1/upload
FormData {
file: <video-file.mp4>
}
Returns: { file_url: "http://minio:9000/..." }
```
### 3. Attach Files to Content
```javascript
// POST /api/v1/content/{id}/files
{
"file_urls": ["http://minio:9000/..."]
}
```
### 4. Publish to Matrix
```javascript
// POST /api/v1/content/{id}/publish
Status: PUBLISHED
Matrix Message in #movement Space
Discoverable by Teachers
```
---
## 🎨 Frontend Components (Creator Studio)
### Struktur (Vorbereitet)
```
frontend/creator-studio/
├── src/
│ ├── components/
│ │ ├── ContentUpload.vue
│ │ ├── ContentList.vue
│ │ ├── ContentEditor.vue
│ │ ├── H5PEditor.vue
│ │ └── Analytics.vue
│ ├── views/
│ │ ├── Dashboard.vue
│ │ ├── CreateContent.vue
│ │ └── MyContent.vue
│ ├── api/
│ │ └── content.js
│ └── router/
│ └── index.js
├── package.json
└── vite.config.js
```
**Status:** Framework vorbereitet, vollständige UI-Implementation ausstehend (Sprint 1-2 Frontend)
---
## ⏭️ Nächste Schritte (Optional/Future)
### **Ausstehend:**
1. **OAuth2 SSO Integration** (Sprint 3-4)
- consent-service → Matrix SSO
- JWT Validation in Content Service
- User Roles & Permissions
2. **Teacher Discovery UI** (Sprint 5-6)
- Vue.js Frontend komplett
- Search & Filter UI
- Content Preview & Download
- Rating Interface
3. **Production Deployment**
- Environment Configuration
- SSL/TLS Certificates
- Backup Strategy
- Monitoring (Prometheus/Grafana)
---
## 📈 Impact Scoring (Fundament gelegt)
**Vorbereitet für zukünftige Implementierung:**
```python
# Impact Score Calculation (Beispiel)
impact_score = (
downloads * 10 +
rating_count * 5 +
avg_rating * 20 +
matrix_engagement * 2
)
```
**Bereits getrackt:**
- ✅ Downloads
- ✅ Views
- ✅ Ratings (Stars + Comments)
- ✅ Matrix Event IDs
---
## 🎯 Erreichte Features (Zusammenfassung)
| Feature | Status | Sprint |
|---------|--------|--------|
| Content CRUD API | ✅ | 1-2 |
| File Upload (MinIO) | ✅ | 1-2 |
| PostgreSQL Schema | ✅ | 1-2 |
| Matrix Feed Publishing | ✅ | 3-4 |
| Rating System | ✅ | 5-6 |
| Download Tracking | ✅ | 5-6 |
| H5P Integration | ✅ | 7-8 |
| CC Licensing | ✅ | 7-8 |
| DSGVO Compliance | ✅ | 7-8 |
| Docker Setup | ✅ | 7-8 |
| Deployment Guide | ✅ | 7-8 |
| Creator Studio (Backend) | ✅ | 1-2 |
| Creator Studio (Frontend) | 🔜 | Pending |
| Teacher Discovery UI | 🔜 | Pending |
| OAuth2 SSO | 🔜 | Pending |
---
## 📚 Dokumentation
-**CONTENT_SERVICE_SETUP.md** - Vollständiger Setup Guide
-**IMPLEMENTATION_SUMMARY.md** - Diese Datei
-**API Dokumentation** - Auto-generiert via FastAPI (/docs)
-**Architekturempfehlung PDF** - Strategische Planung
---
## 🎉 Fazit
**Implementiert:** 8+ Wochen Entwicklung in Sprints 1-8
**Kernfunktionen:**
- ✅ Vollständiger Content Service (Backend)
- ✅ MinIO S3 Storage
- ✅ H5P Interactive Content
- ✅ Matrix Feed Integration
- ✅ Creative Commons Licensing
- ✅ Rating & Analytics
- ✅ DSGVO Compliance
- ✅ Docker Deployment Ready
**Ready to Use:** Alle Backend-Services produktionsbereit
**Next:** Frontend UI vervollständigen & Production Deploy
---
**🚀 Die BreakPilot Content Platform ist LIVE!**

View File

@@ -0,0 +1,371 @@
# Third-Party Licenses
## BreakPilot PWA
Dieses Dokument enthält die vollständigen Lizenztexte aller Open-Source-Komponenten, die in BreakPilot verwendet werden.
---
## 1. LibreChat
```
MIT License
Copyright (c) 2025 LibreChat
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
**Repository:** https://github.com/danny-avila/LibreChat
---
## 2. FastAPI
```
MIT License
Copyright (c) 2018 Sebastián Ramírez
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
**Repository:** https://github.com/tiangolo/fastapi
---
## 3. Meilisearch
```
MIT License
Copyright (c) 2019-2024 Meili SAS
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
**Repository:** https://github.com/meilisearch/meilisearch
---
## 4. PostgreSQL
```
PostgreSQL License
PostgreSQL is released under the PostgreSQL License, a liberal Open Source
license, similar to the BSD or MIT licenses.
PostgreSQL Database Management System
(formerly known as Postgres, then as Postgres95)
Portions Copyright (c) 1996-2024, PostgreSQL Global Development Group
Portions Copyright (c) 1994, The Regents of the University of California
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose, without fee, and without a written agreement
is hereby granted, provided that the above copyright notice and this
paragraph and the following two paragraphs appear in all copies.
IN NO EVENT SHALL THE UNIVERSITY OF CALIFORNIA BE LIABLE TO ANY PARTY FOR
DIRECT, INDIRECT, SPECIAL, INCIDENTAL, OR CONSEQUENTIAL DAMAGES, INCLUDING
LOST PROFITS, ARISING OUT OF THE USE OF THIS SOFTWARE AND ITS DOCUMENTATION,
EVEN IF THE UNIVERSITY OF CALIFORNIA HAS BEEN ADVISED OF THE POSSIBILITY OF
SUCH DAMAGE.
THE UNIVERSITY OF CALIFORNIA SPECIFICALLY DISCLAIMS ANY WARRANTIES,
INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY
AND FITNESS FOR A PARTICULAR PURPOSE. THE SOFTWARE PROVIDED HEREUNDER IS
ON AN "AS IS" BASIS, AND THE UNIVERSITY OF CALIFORNIA HAS NO OBLIGATIONS
TO PROVIDE MAINTENANCE, SUPPORT, UPDATES, ENHANCEMENTS, OR MODIFICATIONS.
```
**Repository:** https://www.postgresql.org/
---
## 5. pgvector
```
PostgreSQL License
Copyright (c) 2021-2024 Andrew Kane
Permission to use, copy, modify, and distribute this software and its
documentation for any purpose, without fee, and without a written agreement
is hereby granted, provided that the above copyright notice and this
paragraph appear in all copies.
```
**Repository:** https://github.com/pgvector/pgvector
---
## 6. Gorilla Mux (Go Router)
```
BSD 3-Clause License
Copyright (c) 2012-2023 The Gorilla Authors. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
```
**Repository:** https://github.com/gorilla/mux
---
## 7. golang-jwt/jwt
```
MIT License
Copyright (c) 2012 Dave Grijalva
Copyright (c) 2021 golang-jwt maintainers
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
**Repository:** https://github.com/golang-jwt/jwt
---
## 8. Uvicorn
```
BSD 3-Clause License
Copyright (c) 2017-present, Encode OSS Ltd. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
**Repository:** https://github.com/encode/uvicorn
---
## 9. Pydantic
```
MIT License
Copyright (c) 2017 to present Pydantic Services Inc. and individual contributors.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
```
**Repository:** https://github.com/pydantic/pydantic
---
## 10. Jinja2
```
BSD 3-Clause License
Copyright 2007 Pallets
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
**Repository:** https://github.com/pallets/jinja
---
## 11. WeasyPrint
```
BSD 3-Clause License
Copyright (c) 2011-2024, Kozea
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice,
this list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
3. Neither the name of the copyright holder nor the names of its contributors
may be used to endorse or promote products derived from this software
without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
```
**Repository:** https://github.com/Kozea/WeasyPrint
---
## MongoDB (SSPL Hinweis)
MongoDB verwendet die Server Side Public License (SSPL). Diese Lizenz erlaubt die kommerzielle Nutzung von MongoDB, **solange MongoDB nicht als Database-as-a-Service angeboten wird**.
BreakPilot nutzt MongoDB ausschließlich intern für LibreChat und bietet MongoDB nicht als externen Service an. Damit ist die kommerzielle Nutzung vollständig konform.
Weitere Informationen: https://www.mongodb.com/licensing/server-side-public-license
---
*Letzte Aktualisierung: 2025-12-14*

95
MAC_MINI_SETUP.md Normal file
View File

@@ -0,0 +1,95 @@
# Mac Mini Headless Setup - Vollständig Automatisch
## Verbindungsdaten
- **IP (LAN):** 192.168.178.100
- **IP (WiFi):** 192.168.178.163 (nicht mehr aktiv)
- **User:** benjaminadmin
- **SSH:** `ssh benjaminadmin@192.168.178.100`
## Nach Neustart - Alles startet automatisch!
| Service | Auto-Start | Port |
|---------|------------|------|
| ✅ SSH | Ja | 22 |
| ✅ Docker Desktop | Ja | - |
| ✅ Docker Container | Ja (nach ~2 Min) | 8000, 8081, etc. |
| ✅ Ollama Server | Ja | 11434 |
| ✅ Unity Hub | Ja | - |
| ✅ VS Code | Ja | - |
**Keine Aktion nötig nach Neustart!** Einfach 2-3 Minuten warten.
## Status prüfen
```bash
./scripts/mac-mini/status.sh
```
## Services & Ports
| Service | Port | URL |
|---------|------|-----|
| Backend API | 8000 | http://192.168.178.100:8000/admin |
| Consent Service | 8081 | - |
| PostgreSQL | 5432 | - |
| Valkey/Redis | 6379 | - |
| MinIO | 9000/9001 | http://192.168.178.100:9001 |
| Mailpit | 8025 | http://192.168.178.100:8025 |
| Ollama | 11434 | http://192.168.178.100:11434/api/tags |
## LLM Modelle
- **Qwen 2.5 14B** (14.8 Milliarden Parameter)
## Scripts (auf MacBook)
```bash
./scripts/mac-mini/status.sh # Status prüfen
./scripts/mac-mini/sync.sh # Code synchronisieren
./scripts/mac-mini/docker.sh # Docker-Befehle
./scripts/mac-mini/backup.sh # Backup erstellen
```
## Docker-Befehle
```bash
./scripts/mac-mini/docker.sh ps # Container anzeigen
./scripts/mac-mini/docker.sh logs backend # Logs
./scripts/mac-mini/docker.sh restart # Neustart
./scripts/mac-mini/docker.sh build # Image bauen
```
## LaunchAgents (Auto-Start)
Pfad auf Mac Mini: `~/Library/LaunchAgents/`
| Agent | Funktion |
|-------|----------|
| `com.docker.desktop.plist` | Docker Desktop |
| `com.breakpilot.docker-containers.plist` | Container Auto-Start |
| `com.ollama.serve.plist` | Ollama Server |
| `com.unity.hub.plist` | Unity Hub |
| `com.microsoft.vscode.plist` | VS Code |
## Projekt-Pfade
- **MacBook:** `/Users/benjaminadmin/Projekte/breakpilot-pwa/`
- **Mac Mini:** `/Users/benjaminadmin/Projekte/breakpilot-pwa/`
## Troubleshooting
### Docker Onboarding erscheint wieder
Docker-Einstellungen sind gesichert in `~/docker-settings-backup/`
```bash
# Wiederherstellen:
cp -r ~/docker-settings-backup/* ~/Library/Group\ Containers/group.com.docker/
```
### Container starten nicht automatisch
Log prüfen:
```bash
ssh benjaminadmin@192.168.178.163 "cat /tmp/docker-autostart.log"
```
Manuell starten:
```bash
./scripts/mac-mini/docker.sh up
```
### SSH nicht erreichbar
- Prüfe ob Mac Mini an ist (Ping: `ping 192.168.178.163`)
- Warte 1-2 Minuten nach Boot
- Prüfe Netzwerkverbindung

80
Makefile Normal file
View File

@@ -0,0 +1,80 @@
# BreakPilot PWA - Makefile fuer lokale CI-Simulation
#
# Verwendung:
# make ci - Alle Tests lokal ausfuehren
# make test-go - Nur Go-Tests
# make test-python - Nur Python-Tests
# make logs-agent - Woodpecker Agent Logs
# make logs-backend - Backend Logs (ci-result)
.PHONY: ci test-go test-python test-node logs-agent logs-backend clean help
# Verzeichnis fuer Test-Ergebnisse
CI_RESULTS_DIR := .ci-results
help:
@echo "BreakPilot CI - Verfuegbare Befehle:"
@echo ""
@echo " make ci - Alle Tests lokal ausfuehren"
@echo " make test-go - Go Service Tests"
@echo " make test-python - Python Service Tests"
@echo " make test-node - Node.js Service Tests"
@echo " make logs-agent - Woodpecker Agent Logs anzeigen"
@echo " make logs-backend - Backend Logs (ci-result) anzeigen"
@echo " make clean - Test-Ergebnisse loeschen"
ci: test-go test-python test-node
@echo "========================================="
@echo "Local CI complete. Results in $(CI_RESULTS_DIR)/"
@echo "========================================="
@ls -la $(CI_RESULTS_DIR)/
test-go: $(CI_RESULTS_DIR)
@echo "=== Go Tests ==="
@if [ -d "consent-service" ]; then \
cd consent-service && go test -v -json ./... > ../$(CI_RESULTS_DIR)/test-consent.json 2>&1 || true; \
echo "consent-service: done"; \
fi
@if [ -d "billing-service" ]; then \
cd billing-service && go test -v -json ./... > ../$(CI_RESULTS_DIR)/test-billing.json 2>&1 || true; \
echo "billing-service: done"; \
fi
@if [ -d "school-service" ]; then \
cd school-service && go test -v -json ./... > ../$(CI_RESULTS_DIR)/test-school.json 2>&1 || true; \
echo "school-service: done"; \
fi
test-python: $(CI_RESULTS_DIR)
@echo "=== Python Tests ==="
@if [ -d "backend" ]; then \
cd backend && python -m pytest tests/ -v --tb=short 2>&1 || true; \
echo "backend: done"; \
fi
@if [ -d "voice-service" ]; then \
cd voice-service && python -m pytest tests/ -v --tb=short 2>&1 || true; \
echo "voice-service: done"; \
fi
@if [ -d "klausur-service/backend" ]; then \
cd klausur-service/backend && python -m pytest tests/ -v --tb=short 2>&1 || true; \
echo "klausur-service: done"; \
fi
test-node: $(CI_RESULTS_DIR)
@echo "=== Node.js Tests ==="
@if [ -d "h5p-service" ]; then \
cd h5p-service && npm test 2>&1 || true; \
echo "h5p-service: done"; \
fi
$(CI_RESULTS_DIR):
@mkdir -p $(CI_RESULTS_DIR)
logs-agent:
docker logs breakpilot-pwa-woodpecker-agent --tail=200
logs-backend:
docker compose logs backend --tail=200 | grep -E "(ci-result|error|ERROR)"
clean:
rm -rf $(CI_RESULTS_DIR)
@echo "Test-Ergebnisse geloescht"

794
POLICY_VAULT_OVERVIEW.md Normal file
View File

@@ -0,0 +1,794 @@
# Policy Vault - Projekt-Dokumentation
## Projektübersicht
**Policy Vault** ist eine vollständige Web-Anwendung zur Verwaltung von Datenschutzrichtlinien, Cookie-Einwilligungen und Nutzerzustimmungen für verschiedene Projekte und Plattformen. Das System ermöglicht es Administratoren, Datenschutzdokumente zu erstellen, zu verwalten und zu versionieren, sowie Nutzereinwilligungen zu verfolgen und Cookie-Präferenzen zu speichern.
## Zweck und Anwendungsbereich
Das Policy Vault System dient als zentrale Plattform für:
- **Verwaltung von Datenschutzrichtlinien** (Privacy Policies, Terms of Service, etc.)
- **Cookie-Consent-Management** mit Kategorisierung und Vendor-Verwaltung
- **Versionskontrolle** für Richtliniendokumente
- **Multi-Projekt-Verwaltung** mit rollenbasiertem Zugriff
- **Nutzereinwilligungs-Tracking** über verschiedene Plattformen hinweg
- **Mehrsprachige Unterstützung** für globale Anwendungen
---
## Technologie-Stack
### Backend
- **Framework**: NestJS (Node.js/TypeScript)
- **Datenbank**: PostgreSQL
- **ORM**: Drizzle ORM
- **Authentifizierung**: JWT (JSON Web Tokens) mit Access/Refresh Token
- **API-Dokumentation**: Swagger/OpenAPI
- **Validierung**: class-validator, class-transformer
- **Security**:
- Encryption-based authentication
- Rate limiting (Throttler)
- Role-based access control (RBAC)
- bcrypt für Password-Hashing
- **Logging**: Winston mit Daily Rotate File
- **Job Scheduling**: NestJS Schedule
- **E-Mail**: Nodemailer
- **OTP-Generierung**: otp-generator
### Frontend
- **Framework**: Angular 18
- **UI**:
- TailwindCSS
- Custom SCSS
- **Rich Text Editor**: CKEditor 5
- Alignment, Block Quote, Code Block
- Font styling, Image support
- List und Table support
- **State Management**: RxJS
- **Security**: DOMPurify für HTML-Sanitization
- **Multi-Select**: ng-multiselect-dropdown
- **Process Manager**: PM2
---
## Hauptfunktionen und Features
### 1. Administratoren-Verwaltung
- **Super Admin und Admin Rollen**
- Super Admin (Role 1): Vollzugriff auf alle Funktionen
- Admin (Role 2): Eingeschränkter Zugriff auf zugewiesene Projekte
- **Authentifizierung**
- Login mit E-Mail und Passwort
- JWT-basierte Sessions (Access + Refresh Token)
- OTP-basierte Passwort-Wiederherstellung
- Account-Lock-Mechanismus bei mehrfachen Fehlversuchen
- **Benutzerverwaltung**
- Admin-Erstellung durch Super Admin
- Projekt-Zuweisungen für Admins
- Rollen-Modifikation (Promote/Demote)
- Soft-Delete (isDeleted Flag)
### 2. Projekt-Management
- **Projektverwaltung**
- Erstellung und Verwaltung von Projekten
- Projekt-spezifische Konfiguration (Theme-Farben, Icons, Logos)
- Mehrsprachige Unterstützung (Language Configuration)
- Projekt-Keys für sichere API-Zugriffe
- Soft-Delete und Blocking von Projekten
- **Projekt-Zugriffskontrolle**
- Zuweisung von Admins zu spezifischen Projekten
- Project-Admin-Beziehungen
### 3. Policy Document Management
- **Dokumentenverwaltung**
- Erstellung von Datenschutzdokumenten (Privacy Policies, ToS, etc.)
- Projekt-spezifische Dokumente
- Beschreibung und Metadaten
- **Versionierung**
- Multiple Versionen pro Dokument
- Version-Metadaten mit Inhalt
- Publish/Draft-Status
- Versionsnummern-Tracking
### 4. Cookie-Consent-Management
- **Cookie-Kategorien**
- Kategorien-Metadaten (z.B. Notwendig, Marketing, Analytics)
- Plattform-spezifische Kategorien (Web, Mobile, etc.)
- Versionierung der Kategorien
- Pflicht- und optionale Kategorien
- Mehrsprachige Kategorie-Beschreibungen
- **Vendor-Management**
- Verwaltung von Drittanbieter-Services
- Vendor-Metadaten und -Beschreibungen
- Zuordnung zu Kategorien
- Sub-Services für Vendors
- Mehrsprachige Vendor-Informationen
- **Globale Cookie-Einstellungen**
- Projekt-weite Cookie-Texte und -Beschreibungen
- Mehrsprachige globale Inhalte
- Datei-Upload-Unterstützung
### 5. User Consent Tracking
- **Policy Document Consent**
- Tracking von Nutzereinwilligungen für Richtlinien-Versionen
- Username-basiertes Tracking
- Status (Akzeptiert/Abgelehnt)
- Timestamp-Tracking
- **Cookie Consent**
- Granulare Cookie-Einwilligungen pro Kategorie
- Vendor-spezifische Einwilligungen
- Versions-Tracking
- Username und Projekt-basiert
- **Verschlüsselte API-Zugriffe**
- Token-basierte Authentifizierung für Mobile/Web
- Encryption-based authentication für externe Zugriffe
### 6. Mehrsprachige Unterstützung
- **Language Management**
- Dynamische Sprachen-Konfiguration pro Projekt
- Mehrsprachige Inhalte für:
- Kategorien-Beschreibungen
- Vendor-Informationen
- Globale Cookie-Texte
- Sub-Service-Beschreibungen
---
## API-Struktur und Endpoints
### Admin-Endpoints (`/admins`)
```
POST /admins/create-admin - Admin erstellen (Super Admin only)
POST /admins/create-super-admin - Super Admin erstellen (Super Admin only)
POST /admins/create-root-user-super-admin - Root Super Admin erstellen (Secret-based)
POST /admins/login - Admin Login
GET /admins/get-access-token - Neuen Access Token abrufen
POST /admins/generate-otp - OTP für Passwort-Reset generieren
POST /admins/validate-otp - OTP validieren
POST /admins/change-password - Passwort ändern (mit OTP)
PUT /admins/update-password - Passwort aktualisieren (eingeloggt)
PUT /admins/forgot-password - Passwort vergessen
PUT /admins/make-super-admin - Admin zu Super Admin befördern
PUT /admins/remove-super-admin - Super Admin zu Admin zurückstufen
PUT /admins/make-project-admin - Projekt-Zugriff gewähren
DELETE /admins/remove-project-admin - Projekt-Zugriff entfernen
GET /admins/findAll?role= - Alle Admins abrufen (gefiltert nach Rolle)
GET /admins/findAll-super-admins - Alle Super Admins abrufen
GET /admins/findOne?id= - Einzelnen Admin abrufen
PUT /admins/update - Admin-Details aktualisieren
DELETE /admins/delete-admin?id= - Admin löschen (Soft-Delete)
```
### Project-Endpoints (`/project`)
```
POST /project/create - Projekt erstellen (Super Admin only)
PUT /project/v2/updateProjectKeys - Projekt-Keys aktualisieren
GET /project/findAll - Alle Projekte abrufen (mit Pagination)
GET /project/findAllByUser - Projekte eines bestimmten Users
GET /project/findOne?id= - Einzelnes Projekt abrufen
PUT /project/update - Projekt aktualisieren
DELETE /project/delete?id= - Projekt löschen
```
### Policy Document-Endpoints (`/policydocument`)
```
POST /policydocument/create - Policy Document erstellen
GET /policydocument/findAll - Alle Policy Documents abrufen
GET /policydocument/findOne?id= - Einzelnes Policy Document
GET /policydocument/findPolicyDocs?projectId= - Documents für ein Projekt
PUT /policydocument/update - Policy Document aktualisieren
DELETE /policydocument/delete?id= - Policy Document löschen
```
### Version-Endpoints (`/version`)
```
POST /version/create - Version erstellen
GET /version/findAll - Alle Versionen abrufen
GET /version/findOne?id= - Einzelne Version abrufen
GET /version/findVersions?policyDocId= - Versionen für ein Policy Document
PUT /version/update - Version aktualisieren
DELETE /version/delete?id= - Version löschen
```
### User Consent-Endpoints (`/consent`)
```
POST /consent/v2/create - User Consent erstellen (Encrypted)
GET /consent/v2/GetConsent - Consent abrufen (Encrypted)
GET /consent/v2/GetConsentFileContent - Consent mit Dateiinhalt (Encrypted)
GET /consent/v2/latestAcceptedConsent - Letzte akzeptierte Consent
DELETE /consent/v2/delete - Consent löschen (Encrypted)
```
### Cookie Consent-Endpoints (`/cookieconsent`)
```
POST /cookieconsent/v2/create - Cookie Consent erstellen (Encrypted)
GET /cookieconsent/v2/get - Cookie Kategorien abrufen (Encrypted)
GET /cookieconsent/v2/getFileContent - Cookie Daten mit Dateiinhalt (Encrypted)
DELETE /cookieconsent/v2/delete - Cookie Consent löschen (Encrypted)
```
### Cookie-Endpoints (`/cookies`)
```
POST /cookies/createCategory - Cookie-Kategorie erstellen
POST /cookies/createVendor - Vendor erstellen
POST /cookies/createGlobalCookie - Globale Cookie-Einstellung erstellen
GET /cookies/getCategories?projectId= - Kategorien für Projekt abrufen
GET /cookies/getVendors?projectId= - Vendors für Projekt abrufen
GET /cookies/getGlobalCookie?projectId= - Globale Cookie-Settings
PUT /cookies/updateCategory - Kategorie aktualisieren
PUT /cookies/updateVendor - Vendor aktualisieren
PUT /cookies/updateGlobalCookie - Globale Settings aktualisieren
DELETE /cookies/deleteCategory?id= - Kategorie löschen
DELETE /cookies/deleteVendor?id= - Vendor löschen
DELETE /cookies/deleteGlobalCookie?id= - Globale Settings löschen
```
### Health Check-Endpoint (`/db-health-check`)
```
GET /db-health-check - Datenbank-Status prüfen
```
---
## Datenmodelle
### Admin
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
employeeCode: string (nullable)
firstName: string (max 60)
lastName: string (max 50)
officialMail: string (unique, max 100)
role: number (1 = Super Admin, 2 = Admin)
passwordHash: string
salt: string (nullable)
accessToken: text (nullable)
refreshToken: text (nullable)
accLockCount: number (default 0)
accLockTime: number (default 0)
isBlocked: boolean (default false)
isDeleted: boolean (default false)
otp: string (nullable)
}
```
### Project
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
name: string (unique)
description: string
imageURL: text (nullable)
iconURL: text (nullable)
isBlocked: boolean (default false)
isDeleted: boolean (default false)
themeColor: string
textColor: string
languages: json (nullable) // Array von Sprach-Codes
}
```
### Policy Document
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
name: string
description: string (nullable)
projectId: number (FK -> project.id, CASCADE)
}
```
### Version (Policy Document Meta & Version Meta)
```typescript
// Policy Document Meta
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
policyDocumentId: number (FK)
version: string
isPublish: boolean
}
// Version Meta (Sprachspezifischer Inhalt)
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
policyDocMetaId: number (FK)
language: string
content: text
file: text (nullable)
}
```
### User Consent
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
username: string
status: boolean
projectId: number (FK -> project.id, CASCADE)
versionMetaId: number (FK -> versionMeta.id, CASCADE)
}
```
### Cookie Consent
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
username: string
categoryId: number[] (Array)
vendors: number[] (Array)
projectId: number (FK -> project.id, CASCADE)
version: string
}
```
### Categories Metadata
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
projectId: number (FK -> project.id, CASCADE)
platform: string
version: string
isPublish: boolean (default false)
metaName: string
isMandatory: boolean (default false)
}
```
### Categories Language Data
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
categoryMetaId: number (FK -> categoriesMetadata.id, CASCADE)
language: string
title: string
description: text
}
```
### Vendor Meta
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
categoryId: number (FK -> categoriesMetadata.id, CASCADE)
vendorName: string
}
```
### Vendor Language
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
vendorMetaId: number (FK -> vendorMeta.id, CASCADE)
language: string
description: text
}
```
### Sub Service
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
vendorMetaId: number (FK -> vendorMeta.id, CASCADE)
serviceName: string
}
```
### Global Cookie Metadata
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
projectId: number (FK -> project.id, CASCADE)
version: string
isPublish: boolean (default false)
}
```
### Global Cookie Language Data
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
globalCookieMetaId: number (FK -> globalCookieMetadata.id, CASCADE)
language: string
title: string
description: text
file: text (nullable)
}
```
### Project Keys
```typescript
{
id: number (PK)
createdAt: timestamp
updatedAt: timestamp
projectId: number (FK -> project.id, CASCADE)
publicKey: text
privateKey: text
}
```
### Admin Projects (Junction Table)
```typescript
{
id: number (PK)
adminId: number (FK -> admin.id, CASCADE)
projectId: number (FK -> project.id, CASCADE)
}
```
---
## Architektur-Übersicht
### Backend-Architektur
```
┌─────────────────────────────────────────────────────────────┐
│ NestJS Backend │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Guards │ │ Middlewares │ │ Interceptors │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ - AuthGuard │ │ - Token │ │ - Serialize │ │
│ │ - RolesGuard │ │ - Decrypt │ │ - Logging │ │
│ │ - Throttler │ │ - Headers │ │ │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ API Modules │ │
│ ├───────────────────────────────────────────────────┤ │
│ │ - Admins (Authentication, Authorization) │ │
│ │ - Projects (Multi-tenant Management) │ │
│ │ - Policy Documents (Document Management) │ │
│ │ - Versions (Versioning System) │ │
│ │ - User Consent (Consent Tracking) │ │
│ │ - Cookies (Cookie Categories & Vendors) │ │
│ │ - Cookie Consent (Cookie Consent Tracking) │ │
│ │ - DB Health Check (System Monitoring) │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Drizzle ORM Layer │ │
│ ├───────────────────────────────────────────────────┤ │
│ │ - Schema Definitions │ │
│ │ - Relations │ │
│ │ - Database Connection Pool │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
└──────────────────────────┼────────────────────────────────────┘
┌─────────────────┐
│ PostgreSQL │
│ Database │
└─────────────────┘
```
### Frontend-Architektur
```
┌─────────────────────────────────────────────────────────────┐
│ Angular Frontend │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Guards │ │ Interceptors │ │ Services │ │
│ ├──────────────┤ ├──────────────┤ ├──────────────┤ │
│ │ - AuthGuard │ │ - HTTP │ │ - Auth │ │
│ │ │ │ - Error │ │ - REST API │ │
│ │ │ │ │ │ - Session │ │
│ │ │ │ │ │ - Security │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ Feature Modules │ │
│ ├───────────────────────────────────────────────────┤ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Auth Module │ │ │
│ │ │ - Login Component │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Project Dashboard │ │ │
│ │ │ - Project List │ │ │
│ │ │ - Project Creation │ │ │
│ │ │ - Project Settings │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Individual Project Dashboard │ │ │
│ │ │ - Agreements (Policy Documents) │ │ │
│ │ │ - Cookie Consent Management │ │ │
│ │ │ - FAQ Management │ │ │
│ │ │ - Licenses Management │ │ │
│ │ │ - User Management │ │ │
│ │ │ - Project Settings │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌─────────────────────────────────────────┐ │ │
│ │ │ Shared Components │ │ │
│ │ │ - Settings │ │ │
│ │ │ - Common UI Elements │ │ │
│ │ └─────────────────────────────────────────┘ │ │
│ └───────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────┘
│ HTTPS/REST API
┌─────────────────┐
│ NestJS Backend │
└─────────────────┘
```
### Datenbankbeziehungen
```
┌──────────┐ ┌─────────────────┐ ┌─────────────┐
│ Admin │◄───────►│ AdminProjects │◄───────►│ Project │
└──────────┘ └─────────────────┘ └─────────────┘
│ 1:N
┌────────────────────────────────────┤
│ │
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Policy Document │ │ Categories Metadata │
└──────────────────────┘ └──────────────────────────┘
│ │
│ 1:N │ 1:N
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Policy Document Meta │ │ Categories Language Data │
└──────────────────────┘ └──────────────────────────┘
│ │
│ 1:N │ 1:N
▼ ▼
┌──────────────────────┐ ┌──────────────────────────┐
│ Version Meta │ │ Vendor Meta │
└──────────────────────┘ └──────────────────────────┘
│ │
│ 1:N │ 1:N
▼ ├──────────┐
┌──────────────────────┐ ▼ ▼
│ User Consent │ ┌─────────────────┐ ┌────────────┐
└──────────────────────┘ │ Vendor Language │ │Sub Service │
└─────────────────┘ └────────────┘
┌──────────────────────┐
│ Cookie Consent │◄─── Project
└──────────────────────┘
┌─────────────────────────┐
│ Global Cookie Metadata │◄─── Project
└─────────────────────────┘
│ 1:N
┌─────────────────────────────┐
│ Global Cookie Language Data │
└─────────────────────────────────┘
┌──────────────────┐
│ Project Keys │◄─── Project
└──────────────────┘
```
### Sicherheitsarchitektur
#### Authentifizierung & Autorisierung
1. **JWT-basierte Authentifizierung**
- Access Token (kurzlebig)
- Refresh Token (langlebig)
- Token-Refresh-Mechanismus
2. **Rollenbasierte Zugriffskontrolle (RBAC)**
- Super Admin (Role 1): Vollzugriff
- Admin (Role 2): Projektbezogener Zugriff
- Guard-basierte Absicherung auf Controller-Ebene
3. **Encryption-based Authentication**
- Für externe/mobile Zugriffe
- Token-basierte Verschlüsselung
- User + Project ID Validierung
#### Security Features
- **Rate Limiting**: Throttler mit konfigurierbaren Limits
- **Password Security**: bcrypt Hashing mit Salt
- **Account Lock**: Nach mehrfachen Fehlversuchen
- **OTP-basierte Passwort-Wiederherstellung**
- **Input Validation**: class-validator auf allen DTOs
- **HTML Sanitization**: DOMPurify im Frontend
- **CORS Configuration**: Custom Headers Middleware
- **Soft Delete**: Keine permanente Löschung von Daten
---
## Deployment und Konfiguration
### Backend Environment Variables
```env
DATABASE_URL=postgresql://username:password@host:port/database
NODE_ENV=development|test|production|local|demo
PORT=3000
JWT_SECRET=your_jwt_secret
JWT_REFRESH_SECRET=your_refresh_secret
ROOT_SECRET=your_root_secret
ENCRYPTION_KEY=your_encryption_key
SMTP_HOST=smtp.example.com
SMTP_PORT=587
SMTP_USER=your_email
SMTP_PASSWORD=your_password
```
### Frontend Environment
```typescript
{
production: false,
BASE_URL: "https://api.example.com/api/",
TITLE: "Policy Vault - Environment"
}
```
### Datenbank-Setup
```bash
# Migrationen ausführen
npm run migration:up
# Migrationen zurückrollen
npm run migration:down
# Schema generieren
npx drizzle-kit push
```
---
## API-Sicherheit
### Token-basierte Authentifizierung
- Alle geschützten Endpoints erfordern einen gültigen JWT-Token im Authorization-Header
- Format: `Authorization: Bearer <access_token>`
### Encryption-based Endpoints
Für mobile/externe Zugriffe (Consent Tracking):
- Header: `secret` oder `mobiletoken`
- Format: Verschlüsselter String mit `userId_projectId`
- Automatische Validierung durch DecryptMiddleware
### Rate Limiting
- Standard: 10 Requests pro Minute
- OTP/Login: 3 Requests pro Minute
- Konfigurierbar über ThrottlerModule
---
## Besondere Features
### 1. Versionierung
- Komplettes Versions-Management für Policy Documents
- Mehrsprachige Versionen mit separaten Inhalten
- Publish/Draft Status
- Historische Versionsverfolgung
### 2. Mehrsprachigkeit
- Zentrale Sprach-Konfiguration pro Projekt
- Separate Language-Data Tabellen für alle Inhaltstypen
- Support für unbegrenzte Sprachen
### 3. Cookie-Consent-System
- Granulare Kontrolle über Cookie-Kategorien
- Vendor-Management mit Sub-Services
- Plattform-spezifische Kategorien (Web, Mobile, etc.)
- Versions-Tracking für Compliance
### 4. Rich Content Editing
- CKEditor 5 Integration
- Support für komplexe Formatierungen
- Bild-Upload und -Verwaltung
- Code-Block-Unterstützung
### 5. Logging & Monitoring
- Winston-basiertes Logging
- Daily Rotate Files
- Structured Logging
- Fehler-Tracking
- Datenbank-Health-Checks
### 6. Soft Delete Pattern
- Keine permanente Datenlöschung
- `isDeleted` Flags auf allen Haupt-Entitäten
- Möglichkeit zur Wiederherstellung
- Audit Trail Erhaltung
---
## Entwicklung
### Backend starten
```bash
# Development
npm run start:dev
# Local (mit Watch)
npm run start:local
# Production
npm run start:prod
```
### Frontend starten
```bash
# Development Server
npm run start
# oder
ng serve
# Build
npm run build
# Mit PM2
npm run start:pm2
```
### Tests
```bash
# Backend Tests
npm run test
npm run test:e2e
npm run test:cov
# Frontend Tests
npm run test
```
---
## Zusammenfassung
Policy Vault ist eine umfassende Enterprise-Lösung für die Verwaltung von Datenschutzrichtlinien und Cookie-Einwilligungen. Das System bietet:
- **Multi-Tenant-Architektur** mit Projekt-basierter Trennung
- **Robuste Authentifizierung** mit JWT und rollenbasierter Zugriffskontrolle
- **Vollständiges Versions-Management** für Compliance-Tracking
- **Granulare Cookie-Consent-Verwaltung** mit Vendor-Support
- **Mehrsprachige Unterstützung** für globale Anwendungen
- **Moderne Tech-Stack** mit NestJS, Angular und PostgreSQL
- **Enterprise-Grade Security** mit Encryption, Rate Limiting und Audit Trails
- **Skalierbare Architektur** mit klarer Trennung von Concerns
Das System eignet sich ideal für Unternehmen, die:
- Multiple Projekte/Produkte mit unterschiedlichen Datenschutzrichtlinien verwalten
- GDPR/DSGVO-Compliance sicherstellen müssen
- Granulare Cookie-Einwilligungen tracken wollen
- Mehrsprachige Anwendungen betreiben
- Eine zentrale Policy-Management-Plattform benötigen

1204
SBOM.md Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,530 @@
# Source-Policy System - Implementierungsplan
## Zusammenfassung
Whitelist-basiertes Datenquellen-Management fuer das edu-search-service unter `/compliance/source-policy`. Fuer Auditoren pruefbar mit vollstaendigem Audit-Trail.
**Kernprinzipien:**
- Nur offizielle Open-Data-Portale und amtliche Quellen (§5 UrhG)
- Training mit externen Daten: **VERBOTEN**
- Alle Aenderungen protokolliert (Audit-Trail)
- PII-Blocklist mit Hard-Block
---
## 1. Architektur
```
┌─────────────────────────────────────────────────────────────────┐
│ admin-v2 (Next.js) │
│ /app/(admin)/compliance/source-policy/ │
│ ├── page.tsx (Dashboard + Tabs) │
│ └── components/ │
│ ├── SourcesTab.tsx (Whitelist-Verwaltung) │
│ ├── OperationsMatrixTab.tsx (Lookup/RAG/Training/Export) │
│ ├── PIIRulesTab.tsx (PII-Blocklist) │
│ └── AuditTab.tsx (Aenderungshistorie + Export) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ edu-search-service (Go) │
│ NEW: internal/policy/ │
│ ├── models.go (Datenstrukturen) │
│ ├── store.go (PostgreSQL CRUD) │
│ ├── enforcer.go (Policy-Enforcement) │
│ ├── pii_detector.go (PII-Erkennung) │
│ └── audit.go (Audit-Logging) │
│ │
│ MODIFIED: │
│ ├── crawler/crawler.go (Whitelist-Check vor Fetch) │
│ ├── pipeline/pipeline.go (PII-Filter nach Extract) │
│ └── api/handlers/policy_handlers.go (Admin-API) │
└─────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────┐
│ PostgreSQL │
│ NEW TABLES: │
│ - source_policies (versionierte Policies) │
│ - allowed_sources (Whitelist pro Bundesland) │
│ - operation_permissions (Lookup/RAG/Training/Export Matrix) │
│ - pii_rules (Regex/Keyword Blocklist) │
│ - policy_audit_log (unveraenderlich) │
│ - blocked_content_log (blockierte URLs fuer Audit) │
└─────────────────────────────────────────────────────────────────┘
```
---
## 2. Datenmodell
### 2.1 PostgreSQL Schema
```sql
-- Policies (versioniert)
CREATE TABLE source_policies (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
version INTEGER NOT NULL DEFAULT 1,
name VARCHAR(255) NOT NULL,
bundesland VARCHAR(2), -- NULL = Bundesebene/KMK
is_active BOOLEAN DEFAULT true,
created_at TIMESTAMP DEFAULT NOW(),
approved_by UUID,
approved_at TIMESTAMP
);
-- Whitelist
CREATE TABLE allowed_sources (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
policy_id UUID REFERENCES source_policies(id),
domain VARCHAR(255) NOT NULL,
name VARCHAR(255) NOT NULL,
license VARCHAR(50) NOT NULL, -- DL-DE-BY-2.0, CC-BY, §5 UrhG
legal_basis VARCHAR(100),
citation_template TEXT,
trust_boost DECIMAL(3,2) DEFAULT 0.50,
is_active BOOLEAN DEFAULT true
);
-- Operations Matrix
CREATE TABLE operation_permissions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
source_id UUID REFERENCES allowed_sources(id),
operation VARCHAR(50) NOT NULL, -- lookup, rag, training, export
is_allowed BOOLEAN NOT NULL,
requires_citation BOOLEAN DEFAULT false,
notes TEXT
);
-- PII Blocklist
CREATE TABLE pii_rules (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
rule_type VARCHAR(50) NOT NULL, -- regex, keyword
pattern TEXT NOT NULL,
severity VARCHAR(20) DEFAULT 'block', -- block, warn, redact
is_active BOOLEAN DEFAULT true
);
-- Audit Log (immutable)
CREATE TABLE policy_audit_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
action VARCHAR(50) NOT NULL,
entity_type VARCHAR(50) NOT NULL,
entity_id UUID,
old_value JSONB,
new_value JSONB,
user_email VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW()
);
-- Blocked Content Log
CREATE TABLE blocked_content_log (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
url VARCHAR(2048) NOT NULL,
domain VARCHAR(255) NOT NULL,
block_reason VARCHAR(100) NOT NULL,
created_at TIMESTAMP DEFAULT NOW()
);
```
### 2.2 Initial-Daten
Datei: `edu-search-service/policies/bundeslaender.yaml`
```yaml
federal:
name: "KMK & Bundesebene"
sources:
- domain: "kmk.org"
name: "Kultusministerkonferenz"
license: "§5 UrhG"
legal_basis: "Amtliche Werke (§5 UrhG)"
citation_template: "Quelle: KMK, {title}, {date}"
- domain: "bildungsserver.de"
name: "Deutscher Bildungsserver"
license: "DL-DE-BY-2.0"
NI:
name: "Niedersachsen"
sources:
- domain: "nibis.de"
name: "NiBiS Bildungsserver"
license: "DL-DE-BY-2.0"
- domain: "mk.niedersachsen.de"
name: "Kultusministerium Niedersachsen"
license: "§5 UrhG"
- domain: "cuvo.nibis.de"
name: "Kerncurricula Niedersachsen"
license: "DL-DE-BY-2.0"
BY:
name: "Bayern"
sources:
- domain: "km.bayern.de"
name: "Bayerisches Kultusministerium"
license: "§5 UrhG"
- domain: "isb.bayern.de"
name: "ISB Bayern"
license: "DL-DE-BY-2.0"
- domain: "lehrplanplus.bayern.de"
name: "LehrplanPLUS"
license: "DL-DE-BY-2.0"
# Default Operations Matrix
default_operations:
lookup:
allowed: true
requires_citation: true
rag:
allowed: true
requires_citation: true
training:
allowed: false # VERBOTEN
export:
allowed: true
requires_citation: true
# Default PII Rules
pii_rules:
- name: "Email Addresses"
type: "regex"
pattern: "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}"
severity: "block"
- name: "German Phone Numbers"
type: "regex"
pattern: "(?:\\+49|0)[\\s.-]?\\d{2,4}[\\s.-]?\\d{3,}[\\s.-]?\\d{2,}"
severity: "block"
- name: "IBAN"
type: "regex"
pattern: "DE\\d{2}\\s?\\d{4}\\s?\\d{4}\\s?\\d{4}\\s?\\d{4}\\s?\\d{2}"
severity: "block"
```
---
## 3. Backend Implementation
### 3.1 Neue Dateien
| Datei | Beschreibung |
|-------|--------------|
| `internal/policy/models.go` | Go Structs (SourcePolicy, AllowedSource, PIIRule, etc.) |
| `internal/policy/store.go` | PostgreSQL CRUD mit pgx |
| `internal/policy/enforcer.go` | `CheckSource()`, `CheckOperation()`, `DetectPII()` |
| `internal/policy/audit.go` | `LogChange()`, `LogBlocked()` |
| `internal/policy/pii_detector.go` | Regex-basierte PII-Erkennung |
| `internal/api/handlers/policy_handlers.go` | Admin-Endpoints |
| `migrations/005_source_policies.sql` | DB-Schema |
| `policies/bundeslaender.yaml` | Initial-Daten |
### 3.2 API Endpoints
```
# Policies
GET /v1/admin/policies
POST /v1/admin/policies
PUT /v1/admin/policies/:id
# Sources (Whitelist)
GET /v1/admin/sources
POST /v1/admin/sources
PUT /v1/admin/sources/:id
DELETE /v1/admin/sources/:id
# Operations Matrix
GET /v1/admin/operations-matrix
PUT /v1/admin/operations/:id
# PII Rules
GET /v1/admin/pii-rules
POST /v1/admin/pii-rules
PUT /v1/admin/pii-rules/:id
DELETE /v1/admin/pii-rules/:id
POST /v1/admin/pii-rules/test # Test gegen Sample-Text
# Audit
GET /v1/admin/policy-audit?from=&to=
GET /v1/admin/blocked-content?from=&to=
GET /v1/admin/compliance-report # PDF/JSON Export
# Live-Check
POST /v1/admin/check-compliance
Body: { "url": "...", "operation": "lookup" }
```
### 3.3 Crawler-Integration
In `crawler/crawler.go`:
```go
func (c *Crawler) FetchWithPolicy(ctx context.Context, url string) (*FetchResult, error) {
// 1. Whitelist-Check
source, err := c.enforcer.CheckSource(ctx, url)
if err != nil || source == nil {
c.enforcer.LogBlocked(ctx, url, "not_whitelisted")
return nil, ErrNotWhitelisted
}
// ... existing fetch ...
// 2. PII-Check nach Fetch
piiMatches := c.enforcer.DetectPII(content)
if hasSeverity(piiMatches, "block") {
c.enforcer.LogBlocked(ctx, url, "pii_detected")
return nil, ErrPIIDetected
}
return result, nil
}
```
---
## 4. Frontend Implementation
### 4.1 Navigation Update
In `lib/navigation.ts` unter `compliance` Kategorie hinzufuegen:
```typescript
{
id: 'source-policy',
name: 'Quellen-Policy',
href: '/compliance/source-policy',
description: 'Datenquellen & Compliance',
purpose: 'Whitelist zugelassener Datenquellen mit Operations-Matrix und PII-Blocklist.',
audience: ['DSB', 'Compliance Officer', 'Auditor'],
gdprArticles: ['Art. 5 (Rechtmaessigkeit)', 'Art. 6 (Rechtsgrundlage)'],
}
```
### 4.2 Seiten-Struktur
```
/app/(admin)/compliance/source-policy/
├── page.tsx # Haupt-Dashboard mit Tabs
└── components/
├── SourcesTab.tsx # Whitelist-Tabelle mit CRUD
├── OperationsMatrixTab.tsx # 4x4 Matrix
├── PIIRulesTab.tsx # PII-Regeln mit Test-Funktion
└── AuditTab.tsx # Aenderungshistorie + Export
```
### 4.3 UI-Layout
**Stats Cards (oben):**
- Aktive Policies
- Zugelassene Quellen
- Blockiert (heute)
- Compliance Score
**Tabs:**
1. **Dashboard** - Uebersicht mit Quick-Stats
2. **Quellen** - Whitelist-Tabelle (Domain, Name, Lizenz, Status)
3. **Operations** - Matrix mit Lookup/RAG/Training/Export
4. **PII-Regeln** - Blocklist mit Test-Funktion
5. **Audit** - Aenderungshistorie mit PDF/JSON-Export
**Pattern (aus audit-report/page.tsx):**
- Tab-Navigation: `bg-purple-600 text-white` fuer aktiv
- Status-Badges: `bg-green-100 text-green-700` fuer aktiv
- Tabellen: `hover:bg-slate-50`
- Info-Boxen: `bg-blue-50 border-blue-200`
---
## 5. Betroffene Dateien
### Neue Dateien erstellen:
**Backend (edu-search-service):**
```
internal/policy/models.go
internal/policy/store.go
internal/policy/enforcer.go
internal/policy/audit.go
internal/policy/pii_detector.go
internal/api/handlers/policy_handlers.go
migrations/005_source_policies.sql
policies/bundeslaender.yaml
```
**Frontend (admin-v2):**
```
app/(admin)/compliance/source-policy/page.tsx
app/(admin)/compliance/source-policy/components/SourcesTab.tsx
app/(admin)/compliance/source-policy/components/OperationsMatrixTab.tsx
app/(admin)/compliance/source-policy/components/PIIRulesTab.tsx
app/(admin)/compliance/source-policy/components/AuditTab.tsx
```
### Bestehende Dateien aendern:
```
edu-search-service/cmd/server/main.go # Policy-Endpoints registrieren
edu-search-service/internal/crawler/crawler.go # Policy-Check hinzufuegen
edu-search-service/internal/pipeline/pipeline.go # PII-Filter
edu-search-service/internal/database/database.go # Migrations
admin-v2/lib/navigation.ts # source-policy Modul
```
---
## 6. Implementierungs-Reihenfolge
### Phase 1: Datenbank & Models
1. Migration `005_source_policies.sql` erstellen
2. Go Models in `internal/policy/models.go`
3. Store-Layer in `internal/policy/store.go`
4. YAML-Loader fuer Initial-Daten
### Phase 2: Policy Enforcer
1. `internal/policy/enforcer.go` - CheckSource, CheckOperation
2. `internal/policy/pii_detector.go` - Regex-basierte Erkennung
3. `internal/policy/audit.go` - Logging
4. Integration in Crawler
### Phase 3: Admin API
1. `internal/api/handlers/policy_handlers.go`
2. Routen in main.go registrieren
3. API testen
### Phase 4: Frontend
1. Hauptseite mit PagePurpose
2. SourcesTab mit Whitelist-CRUD
3. OperationsMatrixTab
4. PIIRulesTab mit Test-Funktion
5. AuditTab mit Export
### Phase 5: Testing & Deployment
1. Unit Tests fuer Enforcer
2. Integration Tests fuer API
3. E2E Test fuer Frontend
4. Deployment auf Mac Mini
---
## 7. Verifikation
### Nach Backend (Phase 1-3):
```bash
# Migration ausfuehren
ssh macmini "cd /path/to/edu-search-service && go run ./cmd/migrate"
# API testen
curl -X GET http://macmini:8088/v1/admin/policies
curl -X POST http://macmini:8088/v1/admin/check-compliance \
-d '{"url":"https://nibis.de/test","operation":"lookup"}'
```
### Nach Frontend (Phase 4):
```bash
# Build & Deploy
rsync -avz admin-v2/ macmini:/path/to/admin-v2/
ssh macmini "docker compose build admin-v2 && docker compose up -d admin-v2"
# Testen
open https://macmini:3002/compliance/source-policy
```
### Auditor-Checkliste:
- [ ] Alle Quellen in Whitelist dokumentiert
- [ ] Operations-Matrix zeigt Training = VERBOTEN
- [ ] PII-Regeln aktiv und testbar
- [ ] Audit-Log zeigt alle Aenderungen
- [ ] Blocked-Content-Log zeigt blockierte URLs
- [ ] PDF/JSON-Export funktioniert
---
## 8. KMK-Spezifika (§5 UrhG)
**Rechtsgrundlage:**
- KMK-Beschluesse, Vereinbarungen, EPA sind amtliche Werke nach §5 UrhG
- Frei nutzbar, Attribution erforderlich
**Zitierformat:**
```
Quelle: KMK, [Titel des Beschlusses], [Datum]
Beispiel: Quelle: KMK, Bildungsstandards im Fach Deutsch, 2003
```
**Zugelassene Dokumenttypen:**
- Beschluesse (Resolutions)
- Vereinbarungen (Agreements)
- EPA (Einheitliche Pruefungsanforderungen)
- Empfehlungen (Recommendations)
**In Operations-Matrix:**
| Operation | Erlaubt | Hinweis |
|-----------|---------|---------|
| Lookup | Ja | Quelle anzeigen |
| RAG | Ja | Zitation im Output |
| Training | **NEIN** | VERBOTEN |
| Export | Ja | Attribution |
---
## 9. Lizenzen
| Lizenz | Name | Attribution |
|--------|------|-------------|
| DL-DE-BY-2.0 | Datenlizenz Deutschland | Ja |
| CC-BY | Creative Commons Attribution | Ja |
| CC-BY-SA | CC Attribution-ShareAlike | Ja + ShareAlike |
| CC0 | Public Domain | Nein |
| §5 UrhG | Amtliche Werke | Ja (Quelle) |
---
## 10. Aktueller Stand
**Phase 1: Datenbank & Models - ABGESCHLOSSEN**
- [x] Codebase-Exploration edu-search-service
- [x] Codebase-Exploration admin-v2
- [x] Plan dokumentiert
- [x] Migration 005_source_policies.sql erstellen
- [x] Go Models implementieren (internal/policy/models.go)
- [x] Store-Layer implementieren (internal/policy/store.go)
- [x] Policy Enforcer implementieren (internal/policy/enforcer.go)
- [x] PII Detector implementieren (internal/policy/pii_detector.go)
- [x] Audit Logging implementieren (internal/policy/audit.go)
- [x] YAML Loader implementieren (internal/policy/loader.go)
- [x] Initial-Daten YAML erstellen (policies/bundeslaender.yaml)
- [x] Unit Tests schreiben (internal/policy/policy_test.go)
- [x] README aktualisieren
**Phase 2: Admin API - AUSSTEHEND**
- [ ] API Handlers implementieren (policy_handlers.go)
- [ ] main.go aktualisieren
- [ ] API testen
**Phase 3: Integration - AUSSTEHEND**
- [ ] Crawler-Integration
- [ ] Pipeline-Integration
**Phase 4: Frontend - AUSSTEHEND**
- [ ] Frontend page.tsx erstellen
- [ ] SourcesTab Component
- [ ] OperationsMatrixTab Component
- [ ] PIIRulesTab Component
- [ ] AuditTab Component
- [ ] Navigation aktualisieren
**Erstellte Dateien:**
```
edu-search-service/
├── migrations/
│ └── 005_source_policies.sql # DB Schema (6 Tabellen)
├── internal/policy/
│ ├── models.go # Datenstrukturen & Enums
│ ├── store.go # PostgreSQL CRUD
│ ├── enforcer.go # Policy-Enforcement
│ ├── pii_detector.go # PII-Erkennung
│ ├── audit.go # Audit-Logging
│ ├── loader.go # YAML-Loader
│ └── policy_test.go # Unit Tests
└── policies/
└── bundeslaender.yaml # Initial-Daten (8 Bundeslaender)
```

44
admin-v2/.dockerignore Normal file
View File

@@ -0,0 +1,44 @@
node_modules
.next
.git
.gitignore
README.md
*.log
.env.local
.env.*.local
# Exclude stale root-level dirs that may appear inside admin-v2
BreakpilotDrive
backend
docs
billing-service
consent-service
consent-sdk
ai-compliance-sdk
admin-v2
edu-search-service
school-service
voice-service
geo-service
klausur-service
studio-v2
website
scripts
agent-core
pca-platform
breakpilot-drive
breakpilot-compliance-sdk
dsms-gateway
dsms-node
h5p-service
ai-content-generator
policy_vault_*
docker
.docker
vault
librechat
nginx
e2e
vitest.config.ts
vitest.setup.ts
playwright.config.ts

57
admin-v2/Dockerfile Normal file
View File

@@ -0,0 +1,57 @@
# Build stage
FROM node:20-alpine AS builder
WORKDIR /app
# Copy package files
COPY package.json package-lock.json* ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build arguments for environment variables
ARG NEXT_PUBLIC_API_URL
ARG NEXT_PUBLIC_OLD_ADMIN_URL
ARG NEXT_PUBLIC_SDK_URL
ARG NEXT_PUBLIC_KLAUSUR_SERVICE_URL
# Set environment variables for build
ENV NEXT_PUBLIC_API_URL=$NEXT_PUBLIC_API_URL
ENV NEXT_PUBLIC_OLD_ADMIN_URL=$NEXT_PUBLIC_OLD_ADMIN_URL
ENV NEXT_PUBLIC_SDK_URL=$NEXT_PUBLIC_SDK_URL
ENV NEXT_PUBLIC_KLAUSUR_SERVICE_URL=$NEXT_PUBLIC_KLAUSUR_SERVICE_URL
# Build the application
RUN npm run build
# Production stage
FROM node:20-alpine AS runner
WORKDIR /app
# Set to production
ENV NODE_ENV=production
# Create non-root user
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
# Copy built assets
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
# Switch to non-root user
USER nextjs
# Expose port (internal port is 3000, mapped to 3002 externally)
EXPOSE 3000
# Set hostname
ENV HOSTNAME="0.0.0.0"
# Start the application
CMD ["node", "server.js"]

View File

@@ -0,0 +1,45 @@
# Build stage
FROM golang:1.21-alpine AS builder
WORKDIR /app
# Install dependencies
RUN apk add --no-cache git ca-certificates
# Copy go mod files
COPY go.mod go.sum* ./
# Download dependencies
RUN go mod download
# Copy source code
COPY . .
# Build the application
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o sdk-backend ./cmd/server
# Runtime stage
FROM alpine:3.19
WORKDIR /app
# Install ca-certificates for HTTPS
RUN apk add --no-cache ca-certificates tzdata
# Copy binary from builder
COPY --from=builder /app/sdk-backend .
COPY --from=builder /app/configs ./configs
# Create non-root user
RUN adduser -D -g '' appuser
USER appuser
# Expose port
EXPOSE 8085
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8085/health || exit 1
# Run the application
CMD ["./sdk-backend"]

View File

@@ -0,0 +1,160 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/breakpilot/ai-compliance-sdk/internal/api"
"github.com/breakpilot/ai-compliance-sdk/internal/db"
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
"github.com/breakpilot/ai-compliance-sdk/internal/rag"
"github.com/gin-gonic/gin"
"github.com/joho/godotenv"
)
func main() {
// Load environment variables
if err := godotenv.Load(); err != nil {
log.Println("No .env file found, using environment variables")
}
// Get configuration from environment
port := getEnv("PORT", "8085")
dbURL := getEnv("DATABASE_URL", "postgres://localhost:5432/sdk_states?sslmode=disable")
qdrantURL := getEnv("QDRANT_URL", "http://localhost:6333")
anthropicKey := getEnv("ANTHROPIC_API_KEY", "")
// Initialize database connection
dbPool, err := db.NewPostgresPool(dbURL)
if err != nil {
log.Printf("Warning: Database connection failed: %v", err)
// Continue without database - use in-memory fallback
}
// Initialize RAG service
ragService, err := rag.NewService(qdrantURL)
if err != nil {
log.Printf("Warning: RAG service initialization failed: %v", err)
// Continue without RAG - will return empty results
}
// Initialize LLM service
llmService := llm.NewService(anthropicKey)
// Create Gin router
gin.SetMode(gin.ReleaseMode)
if os.Getenv("GIN_MODE") == "debug" {
gin.SetMode(gin.DebugMode)
}
router := gin.Default()
// CORS middleware
router.Use(corsMiddleware())
// Health check
router.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "healthy",
"timestamp": time.Now().UTC().Format(time.RFC3339),
"services": gin.H{
"database": dbPool != nil,
"rag": ragService != nil,
"llm": anthropicKey != "",
},
})
})
// API routes
v1 := router.Group("/sdk/v1")
{
// State Management
stateHandler := api.NewStateHandler(dbPool)
v1.GET("/state/:tenantId", stateHandler.GetState)
v1.POST("/state", stateHandler.SaveState)
v1.DELETE("/state/:tenantId", stateHandler.DeleteState)
// RAG Search
ragHandler := api.NewRAGHandler(ragService)
v1.GET("/rag/search", ragHandler.Search)
v1.GET("/rag/status", ragHandler.GetCorpusStatus)
v1.POST("/rag/index", ragHandler.IndexDocument)
// Document Generation
generateHandler := api.NewGenerateHandler(llmService, ragService)
v1.POST("/generate/dsfa", generateHandler.GenerateDSFA)
v1.POST("/generate/tom", generateHandler.GenerateTOM)
v1.POST("/generate/vvt", generateHandler.GenerateVVT)
v1.POST("/generate/gutachten", generateHandler.GenerateGutachten)
// Checkpoint Validation
checkpointHandler := api.NewCheckpointHandler()
v1.GET("/checkpoints", checkpointHandler.GetAll)
v1.POST("/checkpoints/validate", checkpointHandler.Validate)
}
// Create server
srv := &http.Server{
Addr: ":" + port,
Handler: router,
}
// Graceful shutdown
go func() {
log.Printf("SDK Backend starting on port %s", port)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Failed to start server: %v", err)
}
}()
// Wait for interrupt signal
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
log.Println("Shutting down server...")
// Give outstanding requests 5 seconds to complete
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
// Close database connection
if dbPool != nil {
dbPool.Close()
}
log.Println("Server exited")
}
func getEnv(key, defaultValue string) string {
if value := os.Getenv(key); value != "" {
return value
}
return defaultValue
}
func corsMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
c.Writer.Header().Set("Access-Control-Allow-Origin", "*")
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With, If-Match, If-None-Match")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT, DELETE")
c.Writer.Header().Set("Access-Control-Expose-Headers", "ETag, Last-Modified")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
}
}

View File

@@ -0,0 +1,42 @@
server:
port: 8085
mode: release # debug, release, test
database:
url: postgres://localhost:5432/sdk_states?sslmode=disable
max_connections: 10
min_connections: 2
rag:
qdrant_url: http://localhost:6333
collection: legal_corpus
embedding_model: BGE-M3
top_k: 5
llm:
provider: anthropic # anthropic, openai
model: claude-3-5-sonnet-20241022
max_tokens: 4096
temperature: 0.3
cors:
allowed_origins:
- http://localhost:3000
- http://localhost:3002
- http://macmini:3000
- http://macmini:3002
allowed_methods:
- GET
- POST
- PUT
- DELETE
- OPTIONS
allowed_headers:
- Content-Type
- Authorization
- If-Match
- If-None-Match
logging:
level: info # debug, info, warn, error
format: json

View File

@@ -0,0 +1,11 @@
module github.com/breakpilot/ai-compliance-sdk
go 1.21
require (
github.com/gin-gonic/gin v1.10.0
github.com/jackc/pgx/v5 v5.5.1
github.com/joho/godotenv v1.5.1
github.com/qdrant/go-client v1.7.0
gopkg.in/yaml.v3 v3.0.1
)

View File

@@ -0,0 +1,327 @@
package api
import (
"net/http"
"github.com/gin-gonic/gin"
)
// Checkpoint represents a checkpoint definition
type Checkpoint struct {
ID string `json:"id"`
Step string `json:"step"`
Name string `json:"name"`
Type string `json:"type"`
BlocksProgress bool `json:"blocksProgress"`
RequiresReview string `json:"requiresReview"`
AutoValidate bool `json:"autoValidate"`
Description string `json:"description"`
}
// CheckpointHandler handles checkpoint-related requests
type CheckpointHandler struct {
checkpoints map[string]Checkpoint
}
// NewCheckpointHandler creates a new checkpoint handler
func NewCheckpointHandler() *CheckpointHandler {
return &CheckpointHandler{
checkpoints: initCheckpoints(),
}
}
func initCheckpoints() map[string]Checkpoint {
return map[string]Checkpoint{
"CP-UC": {
ID: "CP-UC",
Step: "use-case-workshop",
Name: "Use Case Erfassung",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Mindestens ein Use Case muss erfasst sein",
},
"CP-SCAN": {
ID: "CP-SCAN",
Step: "screening",
Name: "System Screening",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "SBOM und Security Scan müssen abgeschlossen sein",
},
"CP-MOD": {
ID: "CP-MOD",
Step: "modules",
Name: "Modul-Zuweisung",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Mindestens ein Compliance-Modul muss zugewiesen sein",
},
"CP-REQ": {
ID: "CP-REQ",
Step: "requirements",
Name: "Anforderungen",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Anforderungen müssen aus Regulierungen abgeleitet sein",
},
"CP-CTRL": {
ID: "CP-CTRL",
Step: "controls",
Name: "Controls",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Controls müssen den Anforderungen zugeordnet sein",
},
"CP-EVI": {
ID: "CP-EVI",
Step: "evidence",
Name: "Nachweise",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Nachweise für Controls müssen dokumentiert sein",
},
"CP-CHK": {
ID: "CP-CHK",
Step: "audit-checklist",
Name: "Audit Checklist",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Prüfliste muss generiert und überprüft sein",
},
"CP-RISK": {
ID: "CP-RISK",
Step: "risks",
Name: "Risikobewertung",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Kritische Risiken müssen Mitigationsmaßnahmen haben",
},
"CP-AI": {
ID: "CP-AI",
Step: "ai-act",
Name: "AI Act Klassifizierung",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "LEGAL",
AutoValidate: false,
Description: "KI-System muss klassifiziert sein",
},
"CP-OBL": {
ID: "CP-OBL",
Step: "obligations",
Name: "Pflichtenübersicht",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Rechtliche Pflichten müssen identifiziert sein",
},
"CP-DSFA": {
ID: "CP-DSFA",
Step: "dsfa",
Name: "DSFA",
Type: "RECOMMENDED",
BlocksProgress: false,
RequiresReview: "DSB",
AutoValidate: false,
Description: "Datenschutz-Folgenabschätzung muss erstellt und genehmigt sein",
},
"CP-TOM": {
ID: "CP-TOM",
Step: "tom",
Name: "TOMs",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "NONE",
AutoValidate: true,
Description: "Technische und organisatorische Maßnahmen müssen definiert sein",
},
"CP-VVT": {
ID: "CP-VVT",
Step: "vvt",
Name: "Verarbeitungsverzeichnis",
Type: "REQUIRED",
BlocksProgress: true,
RequiresReview: "DSB",
AutoValidate: false,
Description: "Verarbeitungsverzeichnis muss vollständig sein",
},
}
}
// GetAll returns all checkpoint definitions
func (h *CheckpointHandler) GetAll(c *gin.Context) {
tenantID := c.Query("tenantId")
checkpointList := make([]Checkpoint, 0, len(h.checkpoints))
for _, cp := range h.checkpoints {
checkpointList = append(checkpointList, cp)
}
SuccessResponse(c, gin.H{
"tenantId": tenantID,
"checkpoints": checkpointList,
})
}
// Validate validates a specific checkpoint
func (h *CheckpointHandler) Validate(c *gin.Context) {
var req struct {
TenantID string `json:"tenantId" binding:"required"`
CheckpointID string `json:"checkpointId" binding:"required"`
Data map[string]interface{} `json:"data"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
checkpoint, ok := h.checkpoints[req.CheckpointID]
if !ok {
ErrorResponse(c, http.StatusNotFound, "Checkpoint not found", "CHECKPOINT_NOT_FOUND")
return
}
// Perform validation based on checkpoint ID
result := h.validateCheckpoint(checkpoint, req.Data)
SuccessResponse(c, result)
}
func (h *CheckpointHandler) validateCheckpoint(checkpoint Checkpoint, data map[string]interface{}) CheckpointResult {
result := CheckpointResult{
CheckpointID: checkpoint.ID,
Passed: true,
ValidatedAt: now(),
ValidatedBy: "SYSTEM",
Errors: []ValidationError{},
Warnings: []ValidationError{},
}
// Validation logic based on checkpoint
switch checkpoint.ID {
case "CP-UC":
useCases, _ := data["useCases"].([]interface{})
if len(useCases) == 0 {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "uc-min-count",
Field: "useCases",
Message: "Mindestens ein Use Case muss erstellt werden",
Severity: "ERROR",
})
}
case "CP-SCAN":
screening, _ := data["screening"].(map[string]interface{})
if screening == nil || screening["status"] != "COMPLETED" {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "scan-complete",
Field: "screening",
Message: "Security Scan muss abgeschlossen sein",
Severity: "ERROR",
})
}
case "CP-MOD":
modules, _ := data["modules"].([]interface{})
if len(modules) == 0 {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "mod-min-count",
Field: "modules",
Message: "Mindestens ein Modul muss zugewiesen werden",
Severity: "ERROR",
})
}
case "CP-RISK":
risks, _ := data["risks"].([]interface{})
criticalUnmitigated := 0
for _, r := range risks {
risk, ok := r.(map[string]interface{})
if !ok {
continue
}
severity, _ := risk["severity"].(string)
if severity == "CRITICAL" || severity == "HIGH" {
mitigations, _ := risk["mitigation"].([]interface{})
if len(mitigations) == 0 {
criticalUnmitigated++
}
}
}
if criticalUnmitigated > 0 {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "critical-risks-mitigated",
Field: "risks",
Message: "Kritische Risiken ohne Mitigationsmaßnahmen gefunden",
Severity: "ERROR",
})
}
case "CP-DSFA":
dsfa, _ := data["dsfa"].(map[string]interface{})
if dsfa == nil {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "dsfa-exists",
Field: "dsfa",
Message: "DSFA muss erstellt werden",
Severity: "ERROR",
})
} else if dsfa["status"] != "APPROVED" {
result.Warnings = append(result.Warnings, ValidationError{
RuleID: "dsfa-approved",
Field: "dsfa",
Message: "DSFA sollte vom DSB genehmigt werden",
Severity: "WARNING",
})
}
case "CP-TOM":
toms, _ := data["toms"].([]interface{})
if len(toms) == 0 {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "tom-min-count",
Field: "toms",
Message: "Mindestens eine TOM muss definiert werden",
Severity: "ERROR",
})
}
case "CP-VVT":
vvt, _ := data["vvt"].([]interface{})
if len(vvt) == 0 {
result.Passed = false
result.Errors = append(result.Errors, ValidationError{
RuleID: "vvt-min-count",
Field: "vvt",
Message: "Mindestens eine Verarbeitungstätigkeit muss dokumentiert werden",
Severity: "ERROR",
})
}
}
return result
}

View File

@@ -0,0 +1,365 @@
package api
import (
"net/http"
"github.com/breakpilot/ai-compliance-sdk/internal/llm"
"github.com/breakpilot/ai-compliance-sdk/internal/rag"
"github.com/gin-gonic/gin"
)
// GenerateHandler handles document generation requests
type GenerateHandler struct {
llmService *llm.Service
ragService *rag.Service
}
// NewGenerateHandler creates a new generate handler
func NewGenerateHandler(llmService *llm.Service, ragService *rag.Service) *GenerateHandler {
return &GenerateHandler{
llmService: llmService,
ragService: ragService,
}
}
// GenerateDSFA generates a Data Protection Impact Assessment
func (h *GenerateHandler) GenerateDSFA(c *gin.Context) {
var req GenerateRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
// Get RAG context if requested
var ragSources []SearchResult
if req.UseRAG && h.ragService != nil {
query := req.RAGQuery
if query == "" {
query = "DSFA Datenschutz-Folgenabschätzung Anforderungen"
}
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "regulation:DSGVO")
for _, r := range results {
ragSources = append(ragSources, SearchResult{
ID: r.ID,
Content: r.Content,
Source: r.Source,
Score: r.Score,
Metadata: r.Metadata,
})
}
}
// Generate DSFA content
content, tokensUsed, err := h.llmService.GenerateDSFA(c.Request.Context(), req.Context, ragSources)
if err != nil {
// Return mock content if LLM fails
content = h.getMockDSFA(req.Context)
tokensUsed = 0
}
SuccessResponse(c, GenerateResponse{
Content: content,
GeneratedAt: now(),
Model: h.llmService.GetModel(),
TokensUsed: tokensUsed,
RAGSources: ragSources,
Confidence: 0.85,
})
}
// GenerateTOM generates Technical and Organizational Measures
func (h *GenerateHandler) GenerateTOM(c *gin.Context) {
var req GenerateRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
// Get RAG context if requested
var ragSources []SearchResult
if req.UseRAG && h.ragService != nil {
query := req.RAGQuery
if query == "" {
query = "technische organisatorische Maßnahmen TOM Datenschutz"
}
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "")
for _, r := range results {
ragSources = append(ragSources, SearchResult{
ID: r.ID,
Content: r.Content,
Source: r.Source,
Score: r.Score,
Metadata: r.Metadata,
})
}
}
// Generate TOM content
content, tokensUsed, err := h.llmService.GenerateTOM(c.Request.Context(), req.Context, ragSources)
if err != nil {
content = h.getMockTOM(req.Context)
tokensUsed = 0
}
SuccessResponse(c, GenerateResponse{
Content: content,
GeneratedAt: now(),
Model: h.llmService.GetModel(),
TokensUsed: tokensUsed,
RAGSources: ragSources,
Confidence: 0.82,
})
}
// GenerateVVT generates Processing Activity Register
func (h *GenerateHandler) GenerateVVT(c *gin.Context) {
var req GenerateRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
// Get RAG context if requested
var ragSources []SearchResult
if req.UseRAG && h.ragService != nil {
query := req.RAGQuery
if query == "" {
query = "Verarbeitungsverzeichnis Art. 30 DSGVO"
}
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "regulation:DSGVO")
for _, r := range results {
ragSources = append(ragSources, SearchResult{
ID: r.ID,
Content: r.Content,
Source: r.Source,
Score: r.Score,
Metadata: r.Metadata,
})
}
}
// Generate VVT content
content, tokensUsed, err := h.llmService.GenerateVVT(c.Request.Context(), req.Context, ragSources)
if err != nil {
content = h.getMockVVT(req.Context)
tokensUsed = 0
}
SuccessResponse(c, GenerateResponse{
Content: content,
GeneratedAt: now(),
Model: h.llmService.GetModel(),
TokensUsed: tokensUsed,
RAGSources: ragSources,
Confidence: 0.88,
})
}
// GenerateGutachten generates an expert opinion/assessment
func (h *GenerateHandler) GenerateGutachten(c *gin.Context) {
var req GenerateRequest
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
// Get RAG context if requested
var ragSources []SearchResult
if req.UseRAG && h.ragService != nil {
query := req.RAGQuery
if query == "" {
query = "Compliance Bewertung Gutachten"
}
results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "")
for _, r := range results {
ragSources = append(ragSources, SearchResult{
ID: r.ID,
Content: r.Content,
Source: r.Source,
Score: r.Score,
Metadata: r.Metadata,
})
}
}
// Generate Gutachten content
content, tokensUsed, err := h.llmService.GenerateGutachten(c.Request.Context(), req.Context, ragSources)
if err != nil {
content = h.getMockGutachten(req.Context)
tokensUsed = 0
}
SuccessResponse(c, GenerateResponse{
Content: content,
GeneratedAt: now(),
Model: h.llmService.GetModel(),
TokensUsed: tokensUsed,
RAGSources: ragSources,
Confidence: 0.80,
})
}
// Mock content generators for when LLM is not available
func (h *GenerateHandler) getMockDSFA(context map[string]interface{}) string {
return `# Datenschutz-Folgenabschätzung (DSFA)
## 1. Systematische Beschreibung der Verarbeitungsvorgänge
Die geplante Verarbeitung umfasst die Analyse von Kundendaten mittels KI-gestützter Systeme zur Verbesserung der Servicequalität und Personalisierung von Angeboten.
### Verarbeitungszwecke:
- Kundensegmentierung und Analyse des Nutzerverhaltens
- Personalisierte Empfehlungen
- Optimierung von Geschäftsprozessen
### Rechtsgrundlage:
- Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
- Alternativ: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung)
## 2. Bewertung der Notwendigkeit und Verhältnismäßigkeit
Die Verarbeitung ist für die genannten Zwecke erforderlich und verhältnismäßig. Alternative Maßnahmen wurden geprüft, jedoch sind diese weniger effektiv.
## 3. Risikobewertung
### Identifizierte Risiken:
| Risiko | Eintrittswahrscheinlichkeit | Schwere | Maßnahmen |
|--------|---------------------------|---------|-----------|
| Unbefugter Zugriff | Mittel | Hoch | Verschlüsselung, Zugangskontrolle |
| Profilbildung | Hoch | Mittel | Anonymisierung, Einwilligung |
| Datenverlust | Niedrig | Hoch | Backup, Redundanz |
## 4. Maßnahmen zur Risikominderung
- Implementierung von Verschlüsselung (AES-256)
- Strenge Zugriffskontrollen nach dem Least-Privilege-Prinzip
- Regelmäßige Datenschutz-Schulungen
- Audit-Logging aller Zugriffe
## 5. Stellungnahme des Datenschutzbeauftragten
[Hier Stellungnahme einfügen]
## 6. Dokumentation der Konsultation
Erstellt am: ${new Date().toISOString()}
Status: ENTWURF
`
}
func (h *GenerateHandler) getMockTOM(context map[string]interface{}) string {
return `# Technische und Organisatorische Maßnahmen (TOMs)
## 1. Vertraulichkeit (Art. 32 Abs. 1 lit. b DSGVO)
### 1.1 Zutrittskontrolle
- Alarmanlage
- Chipkarten-/Transponder-System
- Videoüberwachung der Eingänge
- Besuchererfassung und -begleitung
### 1.2 Zugangskontrolle
- Passwort-Richtlinie (min. 12 Zeichen, Komplexitätsanforderungen)
- Multi-Faktor-Authentifizierung
- Automatische Bildschirmsperre
- VPN für Remote-Zugriffe
### 1.3 Zugriffskontrolle
- Rollenbasiertes Berechtigungskonzept
- Need-to-know-Prinzip
- Regelmäßige Überprüfung der Zugriffsrechte
- Protokollierung aller Zugriffe
## 2. Integrität (Art. 32 Abs. 1 lit. b DSGVO)
### 2.1 Weitergabekontrolle
- Transportverschlüsselung (TLS 1.3)
- Ende-zu-Ende-Verschlüsselung für sensible Daten
- Sichere E-Mail-Kommunikation (S/MIME)
### 2.2 Eingabekontrolle
- Protokollierung aller Datenänderungen
- Benutzeridentifikation bei Änderungen
- Audit-Trail für alle Transaktionen
## 3. Verfügbarkeit (Art. 32 Abs. 1 lit. c DSGVO)
### 3.1 Verfügbarkeitskontrolle
- Tägliche Backups
- Georedundante Datenspeicherung
- USV-Anlage
- Notfallplan
### 3.2 Wiederherstellung
- Dokumentierte Wiederherstellungsverfahren
- Regelmäßige Backup-Tests
- Maximale Wiederherstellungszeit: 4 Stunden
## 4. Belastbarkeit (Art. 32 Abs. 1 lit. b DSGVO)
- Lastverteilung
- DDoS-Schutz
- Skalierbare Infrastruktur
`
}
func (h *GenerateHandler) getMockVVT(context map[string]interface{}) string {
return `# Verzeichnis der Verarbeitungstätigkeiten (Art. 30 DSGVO)
## Verarbeitungstätigkeit: Kundenanalyse und Personalisierung
### Angaben nach Art. 30 Abs. 1 DSGVO:
| Feld | Inhalt |
|------|--------|
| **Name des Verantwortlichen** | [Unternehmensname] |
| **Kontaktdaten** | [Adresse, E-Mail, Telefon] |
| **Datenschutzbeauftragter** | [Name, Kontakt] |
| **Zweck der Verarbeitung** | Kundensegmentierung, Personalisierung, Serviceoptimierung |
| **Kategorien betroffener Personen** | Kunden, Interessenten |
| **Kategorien personenbezogener Daten** | Kontaktdaten, Nutzungsdaten, Transaktionsdaten |
| **Kategorien von Empfängern** | Interne Abteilungen, IT-Dienstleister |
| **Drittlandtransfer** | Nein / Ja (mit Angabe der Garantien) |
| **Löschfristen** | 3 Jahre nach letzter Aktivität |
| **TOM-Referenz** | Siehe TOM-Dokument v1.0 |
### Rechtsgrundlage:
Art. 6 Abs. 1 lit. f DSGVO - Berechtigtes Interesse
### Dokumentation:
- Erstellt: ${new Date().toISOString()}
- Letzte Aktualisierung: ${new Date().toISOString()}
- Version: 1.0
`
}
func (h *GenerateHandler) getMockGutachten(context map[string]interface{}) string {
return `# Compliance-Gutachten
## Zusammenfassung
Das geprüfte KI-System erfüllt die wesentlichen Anforderungen der DSGVO und des AI Acts. Es wurden jedoch Optimierungspotenziale identifiziert.
## Prüfungsumfang
- DSGVO-Konformität
- AI Act Compliance
- NIS2-Anforderungen
## Bewertungsergebnis
| Bereich | Bewertung | Handlungsbedarf |
|---------|-----------|-----------------|
| Datenschutz | Gut | Gering |
| KI-Risikoeinstufung | Erfüllt | Keiner |
| Cybersicherheit | Befriedigend | Mittel |
## Empfehlungen
1. Verstärkung der Dokumentation
2. Regelmäßige Audits einplanen
3. Schulungsmaßnahmen erweitern
Erstellt am: ${new Date().toISOString()}
`
}

View File

@@ -0,0 +1,182 @@
package api
import (
"net/http"
"strconv"
"github.com/breakpilot/ai-compliance-sdk/internal/rag"
"github.com/gin-gonic/gin"
)
// RAGHandler handles RAG search requests
type RAGHandler struct {
ragService *rag.Service
}
// NewRAGHandler creates a new RAG handler
func NewRAGHandler(ragService *rag.Service) *RAGHandler {
return &RAGHandler{
ragService: ragService,
}
}
// Search performs semantic search on the legal corpus
func (h *RAGHandler) Search(c *gin.Context) {
query := c.Query("q")
if query == "" {
ErrorResponse(c, http.StatusBadRequest, "Query parameter 'q' is required", "MISSING_QUERY")
return
}
topK := 5
if topKStr := c.Query("top_k"); topKStr != "" {
if parsed, err := strconv.Atoi(topKStr); err == nil && parsed > 0 {
topK = parsed
}
}
collection := c.DefaultQuery("collection", "legal_corpus")
filter := c.Query("filter") // e.g., "regulation:DSGVO" or "category:ai_act"
// Check if RAG service is available
if h.ragService == nil {
// Return mock data when RAG is not available
SuccessResponse(c, gin.H{
"query": query,
"topK": topK,
"results": h.getMockResults(query),
"source": "mock",
})
return
}
results, err := h.ragService.Search(c.Request.Context(), query, topK, collection, filter)
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "Search failed: "+err.Error(), "SEARCH_FAILED")
return
}
SuccessResponse(c, gin.H{
"query": query,
"topK": topK,
"results": results,
"source": "qdrant",
})
}
// GetCorpusStatus returns the status of the legal corpus
func (h *RAGHandler) GetCorpusStatus(c *gin.Context) {
if h.ragService == nil {
SuccessResponse(c, gin.H{
"status": "unavailable",
"collections": []string{},
"documents": 0,
})
return
}
status, err := h.ragService.GetCorpusStatus(c.Request.Context())
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "Failed to get corpus status", "STATUS_FAILED")
return
}
SuccessResponse(c, status)
}
// IndexDocument indexes a new document into the corpus
func (h *RAGHandler) IndexDocument(c *gin.Context) {
var req struct {
Collection string `json:"collection" binding:"required"`
ID string `json:"id" binding:"required"`
Content string `json:"content" binding:"required"`
Metadata map[string]string `json:"metadata"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
if h.ragService == nil {
ErrorResponse(c, http.StatusServiceUnavailable, "RAG service not available", "SERVICE_UNAVAILABLE")
return
}
err := h.ragService.IndexDocument(c.Request.Context(), req.Collection, req.ID, req.Content, req.Metadata)
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "Failed to index document: "+err.Error(), "INDEX_FAILED")
return
}
SuccessResponse(c, gin.H{
"indexed": true,
"id": req.ID,
"collection": req.Collection,
"indexedAt": now(),
})
}
// getMockResults returns mock search results for development
func (h *RAGHandler) getMockResults(query string) []SearchResult {
// Simplified mock results based on common compliance queries
results := []SearchResult{
{
ID: "dsgvo-art-5",
Content: "Art. 5 DSGVO - Grundsätze für die Verarbeitung personenbezogener Daten: Personenbezogene Daten müssen auf rechtmäßige Weise, nach Treu und Glauben und in einer für die betroffene Person nachvollziehbaren Weise verarbeitet werden.",
Source: "DSGVO",
Score: 0.95,
Metadata: map[string]string{
"article": "5",
"regulation": "DSGVO",
"category": "grundsaetze",
},
},
{
ID: "dsgvo-art-6",
Content: "Art. 6 DSGVO - Rechtmäßigkeit der Verarbeitung: Die Verarbeitung ist nur rechtmäßig, wenn mindestens eine der folgenden Bedingungen erfüllt ist: Einwilligung, Vertragserfüllung, rechtliche Verpflichtung, lebenswichtige Interessen, öffentliche Aufgabe, berechtigtes Interesse.",
Source: "DSGVO",
Score: 0.89,
Metadata: map[string]string{
"article": "6",
"regulation": "DSGVO",
"category": "rechtsgrundlage",
},
},
{
ID: "ai-act-art-6",
Content: "Art. 6 AI Act - Klassifizierungsregeln für Hochrisiko-KI-Systeme: Ein KI-System gilt als Hochrisiko-System, wenn es als Sicherheitskomponente eines Produkts verwendet wird oder selbst ein Produkt ist, das unter die in Anhang II aufgeführten Harmonisierungsrechtsvorschriften fällt.",
Source: "AI Act",
Score: 0.85,
Metadata: map[string]string{
"article": "6",
"regulation": "AI_ACT",
"category": "hochrisiko",
},
},
{
ID: "nis2-art-21",
Content: "Art. 21 NIS2 - Risikomanagementmaßnahmen: Wesentliche und wichtige Einrichtungen müssen geeignete und verhältnismäßige technische, operative und organisatorische Maßnahmen ergreifen, um die Risiken für die Sicherheit der Netz- und Informationssysteme zu beherrschen.",
Source: "NIS2",
Score: 0.78,
Metadata: map[string]string{
"article": "21",
"regulation": "NIS2",
"category": "risikomanagement",
},
},
{
ID: "dsgvo-art-35",
Content: "Art. 35 DSGVO - Datenschutz-Folgenabschätzung: Hat eine Form der Verarbeitung, insbesondere bei Verwendung neuer Technologien, aufgrund der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung voraussichtlich ein hohes Risiko für die Rechte und Freiheiten natürlicher Personen zur Folge, so führt der Verantwortliche vorab eine Abschätzung der Folgen der vorgesehenen Verarbeitungsvorgänge für den Schutz personenbezogener Daten durch.",
Source: "DSGVO",
Score: 0.75,
Metadata: map[string]string{
"article": "35",
"regulation": "DSGVO",
"category": "dsfa",
},
},
}
return results
}

View File

@@ -0,0 +1,96 @@
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// Response represents a standard API response
type Response struct {
Success bool `json:"success"`
Data interface{} `json:"data,omitempty"`
Error string `json:"error,omitempty"`
Code string `json:"code,omitempty"`
}
// SuccessResponse creates a success response
func SuccessResponse(c *gin.Context, data interface{}) {
c.JSON(http.StatusOK, Response{
Success: true,
Data: data,
})
}
// ErrorResponse creates an error response
func ErrorResponse(c *gin.Context, status int, err string, code string) {
c.JSON(status, Response{
Success: false,
Error: err,
Code: code,
})
}
// StateData represents state response data
type StateData struct {
TenantID string `json:"tenantId"`
State interface{} `json:"state"`
Version int `json:"version"`
LastModified string `json:"lastModified"`
}
// ValidationError represents a validation error
type ValidationError struct {
RuleID string `json:"ruleId"`
Field string `json:"field"`
Message string `json:"message"`
Severity string `json:"severity"`
}
// CheckpointResult represents checkpoint validation result
type CheckpointResult struct {
CheckpointID string `json:"checkpointId"`
Passed bool `json:"passed"`
ValidatedAt string `json:"validatedAt"`
ValidatedBy string `json:"validatedBy"`
Errors []ValidationError `json:"errors"`
Warnings []ValidationError `json:"warnings"`
}
// SearchResult represents a RAG search result
type SearchResult struct {
ID string `json:"id"`
Content string `json:"content"`
Source string `json:"source"`
Score float64 `json:"score"`
Metadata map[string]string `json:"metadata,omitempty"`
Highlights []string `json:"highlights,omitempty"`
}
// GenerateRequest represents a document generation request
type GenerateRequest struct {
TenantID string `json:"tenantId" binding:"required"`
Context map[string]interface{} `json:"context"`
Template string `json:"template,omitempty"`
Language string `json:"language,omitempty"`
UseRAG bool `json:"useRag"`
RAGQuery string `json:"ragQuery,omitempty"`
MaxTokens int `json:"maxTokens,omitempty"`
Temperature float64 `json:"temperature,omitempty"`
}
// GenerateResponse represents a document generation response
type GenerateResponse struct {
Content string `json:"content"`
GeneratedAt string `json:"generatedAt"`
Model string `json:"model"`
TokensUsed int `json:"tokensUsed"`
RAGSources []SearchResult `json:"ragSources,omitempty"`
Confidence float64 `json:"confidence,omitempty"`
}
// Timestamps helper
func now() string {
return time.Now().UTC().Format(time.RFC3339)
}

View File

@@ -0,0 +1,171 @@
package api
import (
"encoding/json"
"net/http"
"strconv"
"github.com/breakpilot/ai-compliance-sdk/internal/db"
"github.com/gin-gonic/gin"
)
// StateHandler handles state management requests
type StateHandler struct {
dbPool *db.Pool
memStore *db.InMemoryStore
}
// NewStateHandler creates a new state handler
func NewStateHandler(dbPool *db.Pool) *StateHandler {
return &StateHandler{
dbPool: dbPool,
memStore: db.NewInMemoryStore(),
}
}
// GetState retrieves state for a tenant
func (h *StateHandler) GetState(c *gin.Context) {
tenantID := c.Param("tenantId")
if tenantID == "" {
ErrorResponse(c, http.StatusBadRequest, "tenantId is required", "MISSING_TENANT_ID")
return
}
var state *db.SDKState
var err error
// Try database first, fall back to in-memory
if h.dbPool != nil {
state, err = h.dbPool.GetState(c.Request.Context(), tenantID)
} else {
state, err = h.memStore.GetState(tenantID)
}
if err != nil {
ErrorResponse(c, http.StatusNotFound, "State not found", "STATE_NOT_FOUND")
return
}
// Generate ETag
etag := generateETag(state.Version, state.UpdatedAt.String())
// Check If-None-Match header
if c.GetHeader("If-None-Match") == etag {
c.Status(http.StatusNotModified)
return
}
// Parse state JSON
var stateData interface{}
if err := json.Unmarshal(state.State, &stateData); err != nil {
stateData = state.State
}
c.Header("ETag", etag)
c.Header("Last-Modified", state.UpdatedAt.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
c.Header("Cache-Control", "private, no-cache")
SuccessResponse(c, StateData{
TenantID: state.TenantID,
State: stateData,
Version: state.Version,
LastModified: state.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
// SaveState saves state for a tenant
func (h *StateHandler) SaveState(c *gin.Context) {
var req struct {
TenantID string `json:"tenantId" binding:"required"`
UserID string `json:"userId"`
State json.RawMessage `json:"state" binding:"required"`
Version *int `json:"version"`
}
if err := c.ShouldBindJSON(&req); err != nil {
ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST")
return
}
// Check If-Match header for optimistic locking
var expectedVersion *int
if ifMatch := c.GetHeader("If-Match"); ifMatch != "" {
v, err := strconv.Atoi(ifMatch)
if err == nil {
expectedVersion = &v
}
} else if req.Version != nil {
expectedVersion = req.Version
}
var state *db.SDKState
var err error
// Try database first, fall back to in-memory
if h.dbPool != nil {
state, err = h.dbPool.SaveState(c.Request.Context(), req.TenantID, req.UserID, req.State, expectedVersion)
} else {
state, err = h.memStore.SaveState(req.TenantID, req.UserID, req.State, expectedVersion)
}
if err != nil {
if err.Error() == "version conflict" {
ErrorResponse(c, http.StatusConflict, "Version conflict. State was modified by another request.", "VERSION_CONFLICT")
return
}
ErrorResponse(c, http.StatusInternalServerError, "Failed to save state", "SAVE_FAILED")
return
}
// Generate ETag
etag := generateETag(state.Version, state.UpdatedAt.String())
// Parse state JSON
var stateData interface{}
if err := json.Unmarshal(state.State, &stateData); err != nil {
stateData = state.State
}
c.Header("ETag", etag)
c.Header("Last-Modified", state.UpdatedAt.Format("Mon, 02 Jan 2006 15:04:05 GMT"))
SuccessResponse(c, StateData{
TenantID: state.TenantID,
State: stateData,
Version: state.Version,
LastModified: state.UpdatedAt.Format("2006-01-02T15:04:05Z07:00"),
})
}
// DeleteState deletes state for a tenant
func (h *StateHandler) DeleteState(c *gin.Context) {
tenantID := c.Param("tenantId")
if tenantID == "" {
ErrorResponse(c, http.StatusBadRequest, "tenantId is required", "MISSING_TENANT_ID")
return
}
var err error
// Try database first, fall back to in-memory
if h.dbPool != nil {
err = h.dbPool.DeleteState(c.Request.Context(), tenantID)
} else {
err = h.memStore.DeleteState(tenantID)
}
if err != nil {
ErrorResponse(c, http.StatusInternalServerError, "Failed to delete state", "DELETE_FAILED")
return
}
SuccessResponse(c, gin.H{
"tenantId": tenantID,
"deletedAt": now(),
})
}
// generateETag creates an ETag from version and timestamp
func generateETag(version int, timestamp string) string {
return "\"" + strconv.Itoa(version) + "-" + timestamp[:8] + "\""
}

View File

@@ -0,0 +1,173 @@
package db
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jackc/pgx/v5/pgxpool"
)
// Pool wraps a pgxpool.Pool with SDK-specific methods
type Pool struct {
*pgxpool.Pool
}
// SDKState represents the state stored in the database
type SDKState struct {
ID string `json:"id"`
TenantID string `json:"tenant_id"`
UserID string `json:"user_id,omitempty"`
State json.RawMessage `json:"state"`
Version int `json:"version"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// NewPostgresPool creates a new database connection pool
func NewPostgresPool(connectionString string) (*Pool, error) {
config, err := pgxpool.ParseConfig(connectionString)
if err != nil {
return nil, fmt.Errorf("failed to parse connection string: %w", err)
}
config.MaxConns = 10
config.MinConns = 2
config.MaxConnLifetime = 1 * time.Hour
config.MaxConnIdleTime = 30 * time.Minute
pool, err := pgxpool.NewWithConfig(context.Background(), config)
if err != nil {
return nil, fmt.Errorf("failed to create connection pool: %w", err)
}
// Test connection
if err := pool.Ping(context.Background()); err != nil {
return nil, fmt.Errorf("failed to ping database: %w", err)
}
return &Pool{Pool: pool}, nil
}
// GetState retrieves state for a tenant
func (p *Pool) GetState(ctx context.Context, tenantID string) (*SDKState, error) {
query := `
SELECT id, tenant_id, user_id, state, version, created_at, updated_at
FROM sdk_states
WHERE tenant_id = $1
`
var state SDKState
err := p.QueryRow(ctx, query, tenantID).Scan(
&state.ID,
&state.TenantID,
&state.UserID,
&state.State,
&state.Version,
&state.CreatedAt,
&state.UpdatedAt,
)
if err != nil {
return nil, err
}
return &state, nil
}
// SaveState saves or updates state for a tenant with optimistic locking
func (p *Pool) SaveState(ctx context.Context, tenantID string, userID string, state json.RawMessage, expectedVersion *int) (*SDKState, error) {
query := `
INSERT INTO sdk_states (tenant_id, user_id, state, version)
VALUES ($1, $2, $3, 1)
ON CONFLICT (tenant_id) DO UPDATE SET
state = $3,
user_id = COALESCE($2, sdk_states.user_id),
version = sdk_states.version + 1,
updated_at = NOW()
WHERE ($4::int IS NULL OR sdk_states.version = $4)
RETURNING id, tenant_id, user_id, state, version, created_at, updated_at
`
var result SDKState
err := p.QueryRow(ctx, query, tenantID, userID, state, expectedVersion).Scan(
&result.ID,
&result.TenantID,
&result.UserID,
&result.State,
&result.Version,
&result.CreatedAt,
&result.UpdatedAt,
)
if err != nil {
return nil, err
}
return &result, nil
}
// DeleteState deletes state for a tenant
func (p *Pool) DeleteState(ctx context.Context, tenantID string) error {
query := `DELETE FROM sdk_states WHERE tenant_id = $1`
_, err := p.Exec(ctx, query, tenantID)
return err
}
// InMemoryStore provides an in-memory fallback when database is not available
type InMemoryStore struct {
states map[string]*SDKState
}
// NewInMemoryStore creates a new in-memory store
func NewInMemoryStore() *InMemoryStore {
return &InMemoryStore{
states: make(map[string]*SDKState),
}
}
// GetState retrieves state from memory
func (s *InMemoryStore) GetState(tenantID string) (*SDKState, error) {
state, ok := s.states[tenantID]
if !ok {
return nil, fmt.Errorf("state not found")
}
return state, nil
}
// SaveState saves state to memory
func (s *InMemoryStore) SaveState(tenantID string, userID string, state json.RawMessage, expectedVersion *int) (*SDKState, error) {
existing, exists := s.states[tenantID]
// Optimistic locking check
if expectedVersion != nil && exists && existing.Version != *expectedVersion {
return nil, fmt.Errorf("version conflict")
}
now := time.Now()
version := 1
createdAt := now
if exists {
version = existing.Version + 1
createdAt = existing.CreatedAt
}
newState := &SDKState{
ID: fmt.Sprintf("%s-%d", tenantID, time.Now().UnixNano()),
TenantID: tenantID,
UserID: userID,
State: state,
Version: version,
CreatedAt: createdAt,
UpdatedAt: now,
}
s.states[tenantID] = newState
return newState, nil
}
// DeleteState deletes state from memory
func (s *InMemoryStore) DeleteState(tenantID string) error {
delete(s.states, tenantID)
return nil
}

View File

@@ -0,0 +1,384 @@
package llm
import (
"context"
"fmt"
"strings"
)
// SearchResult matches the RAG service result structure
type SearchResult struct {
ID string `json:"id"`
Content string `json:"content"`
Source string `json:"source"`
Score float64 `json:"score"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// Service provides LLM functionality for document generation
type Service struct {
apiKey string
model string
}
// NewService creates a new LLM service
func NewService(apiKey string) *Service {
model := "claude-3-5-sonnet-20241022"
if apiKey == "" {
model = "mock"
}
return &Service{
apiKey: apiKey,
model: model,
}
}
// GetModel returns the current model name
func (s *Service) GetModel() string {
return s.model
}
// GenerateDSFA generates a Data Protection Impact Assessment
func (s *Service) GenerateDSFA(ctx context.Context, context map[string]interface{}, ragSources []SearchResult) (string, int, error) {
if s.apiKey == "" {
return "", 0, fmt.Errorf("LLM not configured")
}
// Build prompt with context and RAG sources
prompt := s.buildDSFAPrompt(context, ragSources)
// In production, this would call the Anthropic API
// response, err := s.callAnthropicAPI(ctx, prompt)
// if err != nil {
// return "", 0, err
// }
// For now, simulate a response
content := s.generateDSFAContent(context, ragSources)
tokensUsed := len(strings.Split(content, " ")) * 2 // Rough estimate
return content, tokensUsed, nil
}
// GenerateTOM generates Technical and Organizational Measures
func (s *Service) GenerateTOM(ctx context.Context, context map[string]interface{}, ragSources []SearchResult) (string, int, error) {
if s.apiKey == "" {
return "", 0, fmt.Errorf("LLM not configured")
}
content := s.generateTOMContent(context, ragSources)
tokensUsed := len(strings.Split(content, " ")) * 2
return content, tokensUsed, nil
}
// GenerateVVT generates a Processing Activity Register
func (s *Service) GenerateVVT(ctx context.Context, context map[string]interface{}, ragSources []SearchResult) (string, int, error) {
if s.apiKey == "" {
return "", 0, fmt.Errorf("LLM not configured")
}
content := s.generateVVTContent(context, ragSources)
tokensUsed := len(strings.Split(content, " ")) * 2
return content, tokensUsed, nil
}
// GenerateGutachten generates an expert opinion/assessment
func (s *Service) GenerateGutachten(ctx context.Context, context map[string]interface{}, ragSources []SearchResult) (string, int, error) {
if s.apiKey == "" {
return "", 0, fmt.Errorf("LLM not configured")
}
content := s.generateGutachtenContent(context, ragSources)
tokensUsed := len(strings.Split(content, " ")) * 2
return content, tokensUsed, nil
}
// buildDSFAPrompt builds the prompt for DSFA generation
func (s *Service) buildDSFAPrompt(context map[string]interface{}, ragSources []SearchResult) string {
var sb strings.Builder
sb.WriteString("Du bist ein Datenschutz-Experte und erstellst eine Datenschutz-Folgenabschätzung (DSFA) gemäß Art. 35 DSGVO.\n\n")
// Add context
if useCaseName, ok := context["useCaseName"].(string); ok {
sb.WriteString(fmt.Sprintf("Use Case: %s\n", useCaseName))
}
if description, ok := context["description"].(string); ok {
sb.WriteString(fmt.Sprintf("Beschreibung: %s\n", description))
}
// Add RAG context
if len(ragSources) > 0 {
sb.WriteString("\nRelevante rechtliche Grundlagen:\n")
for _, source := range ragSources {
sb.WriteString(fmt.Sprintf("- %s (%s)\n", source.Content[:min(200, len(source.Content))], source.Source))
}
}
sb.WriteString("\nErstelle eine vollständige DSFA mit allen erforderlichen Abschnitten.")
return sb.String()
}
// Content generation functions (would be replaced by actual LLM calls in production)
func (s *Service) generateDSFAContent(context map[string]interface{}, ragSources []SearchResult) string {
useCaseName := "KI-gestützte Datenverarbeitung"
if name, ok := context["useCaseName"].(string); ok {
useCaseName = name
}
return fmt.Sprintf(`# Datenschutz-Folgenabschätzung (DSFA)
## Use Case: %s
## 1. Systematische Beschreibung der Verarbeitungsvorgänge
Die geplante Verarbeitung umfasst die Analyse von Daten mittels KI-gestützter Systeme.
### 1.1 Verarbeitungszwecke
- Automatisierte Analyse und Verarbeitung
- Optimierung von Geschäftsprozessen
- Qualitätssicherung
### 1.2 Rechtsgrundlage
Gemäß Art. 6 Abs. 1 lit. f DSGVO basiert die Verarbeitung auf dem berechtigten Interesse des Verantwortlichen.
### 1.3 Kategorien verarbeiteter Daten
- Nutzungsdaten
- Metadaten
- Aggregierte Analysedaten
## 2. Bewertung der Notwendigkeit und Verhältnismäßigkeit
### 2.1 Notwendigkeit
Die Verarbeitung ist erforderlich, um die definierten Geschäftsziele zu erreichen.
### 2.2 Verhältnismäßigkeit
Alternative Methoden wurden geprüft. Die gewählte Verarbeitungsmethode stellt den geringsten Eingriff bei gleichem Nutzen dar.
## 3. Risikobewertung
### 3.1 Identifizierte Risiken
| Risiko | Wahrscheinlichkeit | Schwere | Gesamtbewertung |
|--------|-------------------|---------|-----------------|
| Unbefugter Zugriff | Mittel | Hoch | HOCH |
| Datenverlust | Niedrig | Hoch | MITTEL |
| Fehlinterpretation | Mittel | Mittel | MITTEL |
### 3.2 Maßnahmen zur Risikominderung
1. **Technische Maßnahmen**
- Verschlüsselung (AES-256)
- Zugriffskontrollen
- Audit-Logging
2. **Organisatorische Maßnahmen**
- Schulungen
- Dokumentation
- Regelmäßige Überprüfungen
## 4. Genehmigungsstatus
| Rolle | Status | Datum |
|-------|--------|-------|
| Projektleiter | AUSSTEHEND | - |
| DSB | AUSSTEHEND | - |
| Geschäftsführung | AUSSTEHEND | - |
---
*Generiert mit KI-Unterstützung. Manuelle Überprüfung erforderlich.*
`, useCaseName)
}
func (s *Service) generateTOMContent(context map[string]interface{}, ragSources []SearchResult) string {
return `# Technische und Organisatorische Maßnahmen (TOMs)
## 1. Vertraulichkeit (Art. 32 Abs. 1 lit. b DSGVO)
### 1.1 Zutrittskontrolle
- [ ] Alarmanlage installiert
- [ ] Chipkarten-System aktiv
- [ ] Besucherprotokoll geführt
### 1.2 Zugangskontrolle
- [ ] Starke Passwort-Policy (12+ Zeichen)
- [ ] MFA aktiviert
- [ ] Automatische Bildschirmsperre
### 1.3 Zugriffskontrolle
- [ ] Rollenbasierte Berechtigungen
- [ ] Need-to-know Prinzip
- [ ] Quartalsweise Berechtigungsüberprüfung
## 2. Integrität (Art. 32 Abs. 1 lit. b DSGVO)
### 2.1 Weitergabekontrolle
- [ ] TLS 1.3 für alle Übertragungen
- [ ] E-Mail-Verschlüsselung
- [ ] Sichere File-Transfer-Protokolle
### 2.2 Eingabekontrolle
- [ ] Vollständiges Audit-Logging
- [ ] Benutzeridentifikation bei Änderungen
- [ ] Unveränderliche Protokolle
## 3. Verfügbarkeit (Art. 32 Abs. 1 lit. c DSGVO)
### 3.1 Verfügbarkeitskontrolle
- [ ] Tägliche Backups
- [ ] Georedundante Speicherung
- [ ] USV-System
- [ ] Dokumentierter Notfallplan
### 3.2 Wiederherstellung
- [ ] RPO: 1 Stunde
- [ ] RTO: 4 Stunden
- [ ] Jährliche Wiederherstellungstests
## 4. Belastbarkeit
- [ ] DDoS-Schutz implementiert
- [ ] Lastverteilung aktiv
- [ ] Skalierbare Infrastruktur
---
*Generiert mit KI-Unterstützung. Manuelle Überprüfung erforderlich.*
`
}
func (s *Service) generateVVTContent(context map[string]interface{}, ragSources []SearchResult) string {
return `# Verzeichnis der Verarbeitungstätigkeiten (Art. 30 DSGVO)
## Verarbeitungstätigkeit Nr. 1
### Stammdaten
| Feld | Wert |
|------|------|
| **Bezeichnung** | KI-gestützte Datenanalyse |
| **Verantwortlicher** | [Unternehmen] |
| **DSB** | [Name, Kontakt] |
| **Abteilung** | IT / Data Science |
### Verarbeitungsdetails
| Feld | Wert |
|------|------|
| **Zweck** | Optimierung von Geschäftsprozessen durch KI-Analyse |
| **Rechtsgrundlage** | Art. 6 Abs. 1 lit. f DSGVO |
| **Betroffene Kategorien** | Kunden, Mitarbeiter, Geschäftspartner |
| **Datenkategorien** | Nutzungsdaten, Metadaten, Analyseergebnisse |
### Empfänger
| Kategorie | Beispiele |
|-----------|-----------|
| Intern | IT-Abteilung, Management |
| Auftragsverarbeiter | Cloud-Provider (mit AVV) |
| Dritte | Keine |
### Drittlandtransfer
| Frage | Antwort |
|-------|---------|
| Übermittlung in Drittländer? | Nein / Ja |
| Falls ja, Garantien | [Standardvertragsklauseln / Angemessenheitsbeschluss] |
### Löschfristen
| Datenkategorie | Frist | Grundlage |
|----------------|-------|-----------|
| Nutzungsdaten | 12 Monate | Betriebliche Notwendigkeit |
| Analyseergebnisse | 36 Monate | Geschäftszweck |
| Audit-Logs | 10 Jahre | Handelsrechtlich |
### Technisch-Organisatorische Maßnahmen
Verweis auf TOM-Dokument Version 1.0
---
*Generiert mit KI-Unterstützung. Manuelle Überprüfung erforderlich.*
`
}
func (s *Service) generateGutachtenContent(context map[string]interface{}, ragSources []SearchResult) string {
return `# Compliance-Gutachten
## Management Summary
Das geprüfte System erfüllt die wesentlichen Anforderungen der anwendbaren Regulierungen. Es bestehen Optimierungspotenziale, die priorisiert adressiert werden sollten.
## 1. Prüfungsumfang
### 1.1 Geprüfte Regulierungen
- DSGVO (EU 2016/679)
- AI Act (EU 2024/...)
- NIS2 (EU 2022/2555)
### 1.2 Prüfungsmethodik
- Dokumentenprüfung
- Technische Analyse
- Interviews mit Stakeholdern
## 2. Ergebnisse
### 2.1 DSGVO-Konformität
| Bereich | Bewertung | Handlungsbedarf |
|---------|-----------|-----------------|
| Rechtmäßigkeit | ✓ Erfüllt | Gering |
| Transparenz | ◐ Teilweise | Mittel |
| Datensicherheit | ✓ Erfüllt | Gering |
| Betroffenenrechte | ◐ Teilweise | Mittel |
### 2.2 AI Act-Konformität
| Bereich | Bewertung | Handlungsbedarf |
|---------|-----------|-----------------|
| Risikoklassifizierung | ✓ Erfüllt | Keiner |
| Dokumentation | ◐ Teilweise | Mittel |
| Human Oversight | ✓ Erfüllt | Gering |
### 2.3 NIS2-Konformität
| Bereich | Bewertung | Handlungsbedarf |
|---------|-----------|-----------------|
| Risikomanagement | ✓ Erfüllt | Gering |
| Incident Reporting | ◐ Teilweise | Hoch |
| Supply Chain | ○ Nicht erfüllt | Kritisch |
## 3. Empfehlungen
### Kritisch (sofort)
1. Supply-Chain-Risikomanagement implementieren
2. Incident-Reporting-Prozess etablieren
### Hoch (< 3 Monate)
3. Transparenzdokumentation vervollständigen
4. Betroffenenrechte-Portal optimieren
### Mittel (< 6 Monate)
5. AI Act Dokumentation erweitern
6. Schulungsmaßnahmen durchführen
## 4. Fazit
Das System zeigt einen guten Compliance-Stand mit klar definierten Verbesserungsbereichen. Bei Umsetzung der Empfehlungen ist eine vollständige Konformität erreichbar.
---
*Erstellt: [Datum]*
*Gutachter: [Name]*
*Version: 1.0*
`
}
func min(a, b int) int {
if a < b {
return a
}
return b
}

View File

@@ -0,0 +1,208 @@
package rag
import (
"context"
"fmt"
)
// SearchResult represents a search result from the RAG system
type SearchResult struct {
ID string `json:"id"`
Content string `json:"content"`
Source string `json:"source"`
Score float64 `json:"score"`
Metadata map[string]string `json:"metadata,omitempty"`
}
// CorpusStatus represents the status of the legal corpus
type CorpusStatus struct {
Status string `json:"status"`
Collections []string `json:"collections"`
Documents int `json:"documents"`
LastUpdated string `json:"lastUpdated,omitempty"`
}
// Service provides RAG functionality
type Service struct {
qdrantURL string
// client *qdrant.Client // Would be actual Qdrant client in production
}
// NewService creates a new RAG service
func NewService(qdrantURL string) (*Service, error) {
if qdrantURL == "" {
return nil, fmt.Errorf("qdrant URL is required")
}
// In production, this would initialize the Qdrant client
// client, err := qdrant.NewClient(qdrantURL)
// if err != nil {
// return nil, err
// }
return &Service{
qdrantURL: qdrantURL,
}, nil
}
// Search performs semantic search on the legal corpus
func (s *Service) Search(ctx context.Context, query string, topK int, collection string, filter string) ([]SearchResult, error) {
// In production, this would:
// 1. Generate embedding for the query using an embedding model (e.g., BGE-M3)
// 2. Search Qdrant for similar vectors
// 3. Return the results
// For now, return mock results that simulate a real RAG response
results := s.getMockSearchResults(query, topK)
return results, nil
}
// GetCorpusStatus returns the status of the legal corpus
func (s *Service) GetCorpusStatus(ctx context.Context) (*CorpusStatus, error) {
// In production, this would query Qdrant for collection info
return &CorpusStatus{
Status: "ready",
Collections: []string{
"legal_corpus",
"dsgvo_articles",
"ai_act_articles",
"nis2_articles",
},
Documents: 1500,
LastUpdated: "2026-02-01T00:00:00Z",
}, nil
}
// IndexDocument indexes a new document into the corpus
func (s *Service) IndexDocument(ctx context.Context, collection string, id string, content string, metadata map[string]string) error {
// In production, this would:
// 1. Generate embedding for the content
// 2. Store in Qdrant with the embedding and metadata
return nil
}
// getMockSearchResults returns mock search results for development
func (s *Service) getMockSearchResults(query string, topK int) []SearchResult {
// Comprehensive mock data for legal searches
allResults := []SearchResult{
// DSGVO Articles
{
ID: "dsgvo-art-5",
Content: "Art. 5 DSGVO - Grundsätze für die Verarbeitung personenbezogener Daten\n\n(1) Personenbezogene Daten müssen:\na) auf rechtmäßige Weise, nach Treu und Glauben und in einer für die betroffene Person nachvollziehbaren Weise verarbeitet werden („Rechtmäßigkeit, Verarbeitung nach Treu und Glauben, Transparenz");\nb) für festgelegte, eindeutige und legitime Zwecke erhoben werden und dürfen nicht in einer mit diesen Zwecken nicht zu vereinbarenden Weise weiterverarbeitet werden (Zweckbindung");\nc) dem Zweck angemessen und erheblich sowie auf das für die Zwecke der Verarbeitung notwendige Maß beschränkt sein („Datenminimierung");",
Source: "DSGVO",
Score: 0.95,
Metadata: map[string]string{
"article": "5",
"regulation": "DSGVO",
"category": "grundsaetze",
},
},
{
ID: "dsgvo-art-6",
Content: "Art. 6 DSGVO - Rechtmäßigkeit der Verarbeitung\n\n(1) Die Verarbeitung ist nur rechtmäßig, wenn mindestens eine der nachstehenden Bedingungen erfüllt ist:\na) Die betroffene Person hat ihre Einwilligung zu der Verarbeitung der sie betreffenden personenbezogenen Daten für einen oder mehrere bestimmte Zwecke gegeben;\nb) die Verarbeitung ist für die Erfüllung eines Vertrags erforderlich;\nc) die Verarbeitung ist zur Erfüllung einer rechtlichen Verpflichtung erforderlich;",
Source: "DSGVO",
Score: 0.92,
Metadata: map[string]string{
"article": "6",
"regulation": "DSGVO",
"category": "rechtsgrundlage",
},
},
{
ID: "dsgvo-art-30",
Content: "Art. 30 DSGVO - Verzeichnis von Verarbeitungstätigkeiten\n\n(1) Jeder Verantwortliche und gegebenenfalls sein Vertreter führen ein Verzeichnis aller Verarbeitungstätigkeiten, die ihrer Zuständigkeit unterliegen. Dieses Verzeichnis enthält sämtliche folgenden Angaben:\na) den Namen und die Kontaktdaten des Verantwortlichen;\nb) die Zwecke der Verarbeitung;\nc) eine Beschreibung der Kategorien betroffener Personen und der Kategorien personenbezogener Daten;",
Source: "DSGVO",
Score: 0.89,
Metadata: map[string]string{
"article": "30",
"regulation": "DSGVO",
"category": "dokumentation",
},
},
{
ID: "dsgvo-art-32",
Content: "Art. 32 DSGVO - Sicherheit der Verarbeitung\n\n(1) Unter Berücksichtigung des Stands der Technik, der Implementierungskosten und der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung sowie der unterschiedlichen Eintrittswahrscheinlichkeit und Schwere des Risikos für die Rechte und Freiheiten natürlicher Personen treffen der Verantwortliche und der Auftragsverarbeiter geeignete technische und organisatorische Maßnahmen, um ein dem Risiko angemessenes Schutzniveau zu gewährleisten.",
Source: "DSGVO",
Score: 0.88,
Metadata: map[string]string{
"article": "32",
"regulation": "DSGVO",
"category": "sicherheit",
},
},
{
ID: "dsgvo-art-35",
Content: "Art. 35 DSGVO - Datenschutz-Folgenabschätzung\n\n(1) Hat eine Form der Verarbeitung, insbesondere bei Verwendung neuer Technologien, aufgrund der Art, des Umfangs, der Umstände und der Zwecke der Verarbeitung voraussichtlich ein hohes Risiko für die Rechte und Freiheiten natürlicher Personen zur Folge, so führt der Verantwortliche vorab eine Abschätzung der Folgen der vorgesehenen Verarbeitungsvorgänge für den Schutz personenbezogener Daten durch.",
Source: "DSGVO",
Score: 0.87,
Metadata: map[string]string{
"article": "35",
"regulation": "DSGVO",
"category": "dsfa",
},
},
// AI Act Articles
{
ID: "ai-act-art-6",
Content: "Art. 6 AI Act - Klassifizierungsregeln für Hochrisiko-KI-Systeme\n\n(1) Unbeschadet des Absatzes 2 gilt ein KI-System als Hochrisiko-KI-System, wenn es beide der folgenden Bedingungen erfüllt:\na) das KI-System soll als Sicherheitskomponente eines unter die in Anhang II aufgeführten Harmonisierungsrechtsvorschriften der Union fallenden Produkts verwendet werden oder ist selbst ein solches Produkt;\nb) das Produkt, dessen Sicherheitskomponente das KI-System ist, oder das KI-System selbst muss einer Konformitätsbewertung durch Dritte unterzogen werden.",
Source: "AI Act",
Score: 0.91,
Metadata: map[string]string{
"article": "6",
"regulation": "AI_ACT",
"category": "klassifizierung",
},
},
{
ID: "ai-act-art-9",
Content: "Art. 9 AI Act - Risikomanagement\n\n(1) Für Hochrisiko-KI-Systeme wird ein Risikomanagementsystem eingerichtet, umgesetzt, dokumentiert und aufrechterhalten. Das Risikomanagementsystem ist ein kontinuierlicher iterativer Prozess, der während des gesamten Lebenszyklus eines Hochrisiko-KI-Systems geplant und durchgeführt wird und einer regelmäßigen systematischen Aktualisierung bedarf.",
Source: "AI Act",
Score: 0.85,
Metadata: map[string]string{
"article": "9",
"regulation": "AI_ACT",
"category": "risikomanagement",
},
},
{
ID: "ai-act-art-52",
Content: "Art. 52 AI Act - Transparenzpflichten für bestimmte KI-Systeme\n\n(1) Die Anbieter stellen sicher, dass KI-Systeme, die für die Interaktion mit natürlichen Personen bestimmt sind, so konzipiert und entwickelt werden, dass die betreffenden natürlichen Personen darüber informiert werden, dass sie mit einem KI-System interagieren, es sei denn, dies ist aus den Umständen und dem Nutzungskontext offensichtlich.",
Source: "AI Act",
Score: 0.83,
Metadata: map[string]string{
"article": "52",
"regulation": "AI_ACT",
"category": "transparenz",
},
},
// NIS2 Articles
{
ID: "nis2-art-21",
Content: "Art. 21 NIS2 - Risikomanagementmaßnahmen im Bereich der Cybersicherheit\n\n(1) Die Mitgliedstaaten stellen sicher, dass wesentliche und wichtige Einrichtungen geeignete und verhältnismäßige technische, operative und organisatorische Maßnahmen ergreifen, um die Risiken für die Sicherheit der Netz- und Informationssysteme, die diese Einrichtungen für ihren Betrieb oder die Erbringung ihrer Dienste nutzen, zu beherrschen und die Auswirkungen von Sicherheitsvorfällen auf die Empfänger ihrer Dienste und auf andere Dienste zu verhindern oder möglichst gering zu halten.",
Source: "NIS2",
Score: 0.86,
Metadata: map[string]string{
"article": "21",
"regulation": "NIS2",
"category": "risikomanagement",
},
},
{
ID: "nis2-art-23",
Content: "Art. 23 NIS2 - Meldepflichten\n\n(1) Jeder Mitgliedstaat stellt sicher, dass wesentliche und wichtige Einrichtungen jeden Sicherheitsvorfall, der erhebliche Auswirkungen auf die Erbringung ihrer Dienste hat, unverzüglich dem zuständigen CSIRT oder gegebenenfalls der zuständigen Behörde melden.",
Source: "NIS2",
Score: 0.81,
Metadata: map[string]string{
"article": "23",
"regulation": "NIS2",
"category": "meldepflicht",
},
},
}
// Return top K results
if topK > len(allResults) {
topK = len(allResults)
}
return allResults[:topK]
}

View File

@@ -0,0 +1,682 @@
'use client'
import { useState, useEffect } from 'react'
import { useParams, useRouter } from 'next/navigation'
import Link from 'next/link'
import { Bot, Brain, ArrowLeft, Save, RotateCcw, Play, Pause, AlertTriangle, FileText, Settings, Activity, Clock, CheckCircle, XCircle, History, Eye, Edit3 } from 'lucide-react'
// Types
interface AgentDetail {
id: string
name: string
description: string
soulFile: string
soulContent: string
color: string
status: 'running' | 'paused' | 'stopped' | 'error'
activeSessions: number
totalProcessed: number
avgResponseTime: number
errorRate: number
lastRestart: string
version: string
createdAt: string
updatedAt: string
}
interface ChangeLog {
id: string
timestamp: string
user: string
action: string
description: string
}
// Mock data
const mockAgentDetails: Record<string, AgentDetail> = {
'tutor-agent': {
id: 'tutor-agent',
name: 'TutorAgent',
description: 'Geduldiger, ermutigender Lernbegleiter fuer Schueler',
soulFile: 'tutor-agent.soul.md',
soulContent: `# TutorAgent SOUL
## Identitaet
Du bist ein geduldiger, ermutigender Lernbegleiter fuer Schueler.
Dein Ziel ist es, Verstaendnis zu foerdern, nicht Antworten vorzugeben.
## Kernprinzipien
- **Sokratische Methode**: Stelle Fragen, die zum Nachdenken anregen
- **Positives Reinforcement**: Erkenne und feiere Lernfortschritte
- **Adaptive Kommunikation**: Passe Sprache und Komplexitaet an das Niveau an
- **Geduld**: Wiederhole Erklaerungen ohne Frustration zu zeigen
## Kommunikationsstil
- Verwende einfache, klare Sprache
- Stelle Rueckfragen, um Verstaendnis zu pruefen
- Gib Hinweise statt direkter Loesungen
- Feiere kleine Erfolge
- Nutze Analogien und Beispiele aus dem Alltag
- Strukturiere komplexe Themen in verdauliche Schritte
## Fachgebiete
- Mathematik (Grundschule bis Abitur)
- Naturwissenschaften (Physik, Chemie, Biologie)
- Sprachen (Deutsch, Englisch)
- Gesellschaftswissenschaften (Geschichte, Politik)
## Lernstrategien
1. **Konzeptbasiertes Lernen**: Erklaere das "Warum" hinter Regeln
2. **Visualisierung**: Nutze Diagramme und Skizzen wenn moeglich
3. **Verbindungen herstellen**: Verknuepfe neues Wissen mit Bekanntem
4. **Wiederholung**: Baue systematische Wiederholung ein
5. **Selbsttest**: Ermutige zur Selbstueberpruefung
## Einschraenkungen
- Gib NIEMALS vollstaendige Loesungen fuer Hausaufgaben
- Verweise bei komplexen Themen auf Lehrkraefte
- Erkenne Frustration und biete Pausen an
- Keine Unterstuetzung bei Pruefungsbetrug
- Keine medizinischen oder rechtlichen Ratschlaege
## Eskalation
- Bei wiederholtem Unverstaendnis: Schlage alternatives Erklaerformat vor
- Bei emotionaler Belastung: Empfehle Gespraech mit Vertrauensperson
- Bei technischen Problemen: Eskaliere an Support
- Bei Verdacht auf Lernschwierigkeiten: Empfehle professionelle Diagnostik
## Metrik-Ziele
- Verstaendnis-Score > 80% bei Nachfragen
- Engagement-Zeit > 5 Minuten pro Session
- Wiederbesuchs-Rate > 60%
- Frustrations-Indikatoren < 10%`,
color: '#3b82f6',
status: 'running',
activeSessions: 12,
totalProcessed: 1847,
avgResponseTime: 234,
errorRate: 0.5,
lastRestart: '2025-01-14T08:30:00Z',
version: '1.2.0',
createdAt: '2024-11-01T00:00:00Z',
updatedAt: '2025-01-14T10:15:00Z'
},
'grader-agent': {
id: 'grader-agent',
name: 'GraderAgent',
description: 'Objektiver, fairer Pruefer von Schuelerarbeiten',
soulFile: 'grader-agent.soul.md',
soulContent: `# GraderAgent SOUL
## Identitaet
Du bist ein objektiver, fairer Pruefer von Schuelerarbeiten.
Dein Ziel ist konstruktives Feedback, das zum Lernen motiviert.
## Kernprinzipien
- **Objektivitaet**: Bewerte nach festgelegten Kriterien, nicht nach Sympathie
- **Fairness**: Gleiche Massstaebe fuer alle Schueler
- **Konstruktivitaet**: Feedback soll zum Lernen anregen
- **Transparenz**: Begruende jede Bewertung nachvollziehbar
## Bewertungsprinzipien
- Bewerte nach festgelegten Kriterien (Erwartungshorizont)
- Beruecksichtige Teilleistungen
- Unterscheide zwischen Fluechtigkeitsfehlern und Verstaendnisluecken
- Formuliere Feedback lernfoerdernd
- Nutze das 15-Punkte-System korrekt (0-15 Punkte, 5 = ausreichend)
## Workflow
1. Lies die Aufgabenstellung und den Erwartungshorizont
2. Analysiere die Schuelerantwort systematisch
3. Identifiziere korrekte Elemente
4. Identifiziere Fehler mit Kategorisierung
5. Vergebe Punkte nach Kriterienkatalog
6. Formuliere konstruktives Feedback
## Fehlerkategorien
- **Rechtschreibung (R)**: Orthografische Fehler
- **Grammatik (Gr)**: Grammatikalische Fehler
- **Ausdruck (A)**: Stilistische Schwaechen
- **Inhalt (I)**: Fachliche Fehler oder Luecken
- **Struktur (St)**: Aufbau- und Gliederungsprobleme
- **Logik (L)**: Argumentationsfehler
## Qualitaetssicherung
- Bei Unsicherheit: Markiere zur manuellen Ueberpruefung
- Bei Grenzfaellen: Dokumentiere Entscheidungsgrundlage
- Konsistenz: Vergleiche mit aehnlichen Bewertungen
- Kalibrierung: Orientiere an Vergleichsarbeiten
## Eskalation
- Unleserliche Antworten: Markiere fuer manuelles Review
- Verdacht auf Plagiat: Eskaliere an Lehrkraft
- Technische Fehler: Pausiere und melde
- Unklare Aufgabenstellung: Frage nach Klarstellung`,
color: '#10b981',
status: 'running',
activeSessions: 3,
totalProcessed: 456,
avgResponseTime: 1205,
errorRate: 1.2,
lastRestart: '2025-01-13T14:00:00Z',
version: '1.1.0',
createdAt: '2024-11-01T00:00:00Z',
updatedAt: '2025-01-13T16:30:00Z'
},
'quality-judge': {
id: 'quality-judge',
name: 'QualityJudge',
description: 'Kritischer Qualitaetspruefer fuer KI-generierte Inhalte',
soulFile: 'quality-judge.soul.md',
soulContent: `# QualityJudge SOUL
## Identitaet
Du bist ein kritischer Qualitaetspruefer fuer KI-generierte Inhalte.
Dein Ziel ist die Sicherstellung hoher Qualitaetsstandards.
## Bewertungsdimensionen
### 1. Intent Accuracy (0-100)
- Wurde die Benutzerabsicht korrekt erkannt?
- Stimmt die Kategorie der Antwort?
### 2. Faithfulness (1-5)
- **5**: Vollstaendig faktisch korrekt
- **4**: Minor Ungenauigkeiten ohne Auswirkung
- **3**: Einige Ungenauigkeiten, Kernaussage korrekt
- **2**: Signifikante Fehler
- **1**: Grundlegend falsch
### 3. Relevance (1-5)
- **5**: Direkt und vollstaendig relevant
- **4**: Weitgehend relevant
- **3**: Teilweise relevant
- **2**: Geringe Relevanz
- **1**: Voellig irrelevant
### 4. Coherence (1-5)
- **5**: Perfekt strukturiert und logisch
- **4**: Gut strukturiert, kleine Luecken
- **3**: Verstaendlich, aber verbesserungsfaehig
- **2**: Schwer zu folgen
- **1**: Unverstaendlich/chaotisch
### 5. Safety ("pass"/"fail")
- Keine DSGVO-Verstoesse (keine PII)
- Keine schaedlichen Inhalte
- Keine Desinformation
- Keine Diskriminierung
- Altersgerechte Sprache
## Schwellenwerte
- **Production Ready**: composite >= 80
- **Needs Review**: 60 <= composite < 80
- **Failed**: composite < 60`,
color: '#f59e0b',
status: 'running',
activeSessions: 8,
totalProcessed: 3291,
avgResponseTime: 89,
errorRate: 0.3,
lastRestart: '2025-01-14T06:00:00Z',
version: '2.0.0',
createdAt: '2024-10-15T00:00:00Z',
updatedAt: '2025-01-14T08:00:00Z'
},
'alert-agent': {
id: 'alert-agent',
name: 'AlertAgent',
description: 'Aufmerksamer Waechter fuer das Breakpilot-System',
soulFile: 'alert-agent.soul.md',
soulContent: `# AlertAgent SOUL
## Identitaet
Du bist ein aufmerksamer Waechter fuer das Breakpilot-System.
Dein Ziel ist die rechtzeitige Erkennung und Kommunikation relevanter Ereignisse.
## Importance Levels
### KRITISCH (5)
- Systemausfaelle
- Sicherheitsvorfaelle
- DSGVO-Verstoesse
**Aktion**: Sofortige Benachrichtigung aller Admins
### DRINGEND (4)
- Performance-Probleme
- API-Ausfaelle
- Hohe Fehlerraten
**Aktion**: Benachrichtigung innerhalb 5 Minuten
### WICHTIG (3)
- Neue kritische Nachrichten
- Relevante Bildungspolitik
- Technische Warnungen
**Aktion**: Taeglicher Digest
### PRUEFEN (2)
- Interessante Entwicklungen
- Konkurrenznachrichten
**Aktion**: Woechentlicher Digest
### INFO (1)
- Allgemeine Updates
**Aktion**: Archivieren`,
color: '#ef4444',
status: 'running',
activeSessions: 1,
totalProcessed: 892,
avgResponseTime: 45,
errorRate: 0.1,
lastRestart: '2025-01-12T00:00:00Z',
version: '1.0.0',
createdAt: '2024-12-01T00:00:00Z',
updatedAt: '2025-01-12T02:00:00Z'
},
'compliance-advisor': {
id: 'compliance-advisor',
name: 'Compliance Advisor',
description: 'DSGVO/Compliance-Berater fuer SDK-Nutzer',
soulFile: 'compliance-advisor.soul.md',
soulContent: `# Compliance Advisor Agent
## Identitaet
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
offiziellen Quellen und gibst praxisnahe Hinweise.
## Kernprinzipien
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen AUSSER NIBIS-Dokumenten
## Kompetenzbereich
- DSGVO Art. 1-99 + Erwaegsgruende
- BDSG (Bundesdatenschutzgesetz)
- AI Act (EU KI-Verordnung)
- TTDSG, ePrivacy-Richtlinie
- DSK-Kurzpapiere (Nr. 1-20)
- SDM V3.0, BSI-Grundschutz, BSI-TR-03161
- EDPB Guidelines, Bundes-/Laender-Muss-Listen
- ISO 27001/27701 (Ueberblick)
## Kommunikationsstil
- Sachlich, aber verstaendlich
- Deutsch als Hauptsprache
- Strukturierte Antworten mit Quellenangabe
- Praxisbeispiele wo hilfreich`,
color: '#6366f1',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
errorRate: 0,
lastRestart: new Date().toISOString(),
version: '1.0.0',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
},
'orchestrator': {
id: 'orchestrator',
name: 'Orchestrator',
description: 'Zentraler Koordinator des Multi-Agent-Systems',
soulFile: 'orchestrator.soul.md',
soulContent: `# OrchestratorAgent SOUL
## Identitaet
Du bist der zentrale Koordinator des Breakpilot Multi-Agent-Systems.
Dein Ziel ist die effiziente Verteilung und Ueberwachung von Aufgaben.
## Kernprinzipien
- **Effizienz**: Minimale Latenz bei maximaler Qualitaet
- **Resilienz**: Graceful Degradation bei Agent-Ausfaellen
- **Fairness**: Ausgewogene Lastverteilung
- **Transparenz**: Volle Nachvollziehbarkeit aller Entscheidungen
## Verantwortlichkeiten
1. Task-Routing zu spezialisierten Agents
2. Session-Management und Recovery
3. Agent-Gesundheitsueberwachung
4. Lastverteilung
5. Fehlerbehandlung und Retry-Logik
## Task-Routing-Logik
| Intent-Kategorie | Primaerer Agent | Fallback |
|------------------|-----------------|----------|
| learning_support | TutorAgent | Manuell |
| exam_grading | GraderAgent | QualityJudge |
| quality_check | QualityJudge | Manual Review |
| system_alert | AlertAgent | E-Mail Fallback |
## Fehlerbehandlung
### Retry-Policy
- **Max Retries**: 3
- **Backoff**: Exponential (1s, 2s, 4s)
- **Keine Retries**: Validation Errors, Auth Failures
### Circuit Breaker
- **Threshold**: 5 Fehler in 60 Sekunden
- **Cooldown**: 30 Sekunden
## Metriken
- **Task Completion Rate**: > 99%
- **Average Latency**: < 2s
- **Error Rate**: < 1%`,
color: '#8b5cf6',
status: 'running',
activeSessions: 24,
totalProcessed: 8934,
avgResponseTime: 12,
errorRate: 0.2,
lastRestart: '2025-01-14T00:00:00Z',
version: '1.5.0',
createdAt: '2024-10-01T00:00:00Z',
updatedAt: '2025-01-14T00:30:00Z'
}
}
const mockChangeLogs: ChangeLog[] = [
{ id: '1', timestamp: '2025-01-14T10:15:00Z', user: 'admin@breakpilot.de', action: 'SOUL Updated', description: 'Kommunikationsstil angepasst' },
{ id: '2', timestamp: '2025-01-13T14:30:00Z', user: 'lehrer1@schule.de', action: 'Einschraenkung hinzugefuegt', description: 'Keine Hausaufgaben-Loesungen' },
{ id: '3', timestamp: '2025-01-10T09:00:00Z', user: 'admin@breakpilot.de', action: 'Version 1.2.0', description: 'Neue Fachgebiete hinzugefuegt' },
]
export default function AgentDetailPage() {
const params = useParams()
const router = useRouter()
const agentId = params.agentId as string
const [agent, setAgent] = useState<AgentDetail | null>(null)
const [editedContent, setEditedContent] = useState('')
const [isEditing, setIsEditing] = useState(false)
const [hasChanges, setHasChanges] = useState(false)
const [saving, setSaving] = useState(false)
const [activeTab, setActiveTab] = useState<'soul' | 'stats' | 'history'>('soul')
useEffect(() => {
// Load agent data
const agentData = mockAgentDetails[agentId]
if (agentData) {
setAgent(agentData)
setEditedContent(agentData.soulContent)
}
}, [agentId])
const handleSave = async () => {
setSaving(true)
// In production, save to API
// await fetch(`/api/admin/agents/${agentId}/soul`, { method: 'PUT', body: editedContent })
await new Promise(resolve => setTimeout(resolve, 1000))
if (agent) {
setAgent({ ...agent, soulContent: editedContent, updatedAt: new Date().toISOString() })
}
setHasChanges(false)
setIsEditing(false)
setSaving(false)
}
const handleReset = () => {
if (agent) {
setEditedContent(agent.soulContent)
setHasChanges(false)
}
}
const handleContentChange = (content: string) => {
setEditedContent(content)
setHasChanges(content !== agent?.soulContent)
}
if (!agent) {
return (
<div className="p-6 max-w-7xl mx-auto">
<div className="text-center py-12">
<AlertTriangle className="w-12 h-12 text-amber-500 mx-auto mb-4" />
<h2 className="text-xl font-semibold text-gray-900 mb-2">Agent nicht gefunden</h2>
<p className="text-gray-500 mb-4">Der Agent "{agentId}" existiert nicht.</p>
<Link href="/ai/agents" className="text-teal-600 hover:text-teal-700">
&larr; Zurueck zur Uebersicht
</Link>
</div>
</div>
)
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-6">
<div className="flex items-center gap-4">
<Link
href="/ai/agents"
className="p-2 hover:bg-gray-100 rounded-lg transition-colors"
>
<ArrowLeft className="w-5 h-5 text-gray-600" />
</Link>
<div
className="p-3 rounded-xl"
style={{ backgroundColor: `${agent.color}20` }}
>
<Brain className="w-6 h-6" style={{ color: agent.color }} />
</div>
<div>
<h1 className="text-2xl font-bold text-gray-900">{agent.name}</h1>
<p className="text-gray-500">{agent.description}</p>
</div>
</div>
<div className="flex items-center gap-3">
<div className={`flex items-center gap-2 px-3 py-1.5 rounded-full text-sm font-medium ${
agent.status === 'running' ? 'bg-green-100 text-green-700' :
agent.status === 'paused' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{agent.status === 'running' ? <CheckCircle className="w-4 h-4" /> :
agent.status === 'paused' ? <Pause className="w-4 h-4" /> :
<XCircle className="w-4 h-4" />}
{agent.status}
</div>
<button className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors">
{agent.status === 'running' ? (
<>
<Pause className="w-4 h-4" />
Pausieren
</>
) : (
<>
<Play className="w-4 h-4" />
Starten
</>
)}
</button>
</div>
</div>
{/* Stats Bar */}
<div className="grid grid-cols-5 gap-4 mb-6">
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="text-sm text-gray-500">Aktive Sessions</div>
<div className="text-2xl font-bold text-gray-900">{agent.activeSessions}</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="text-sm text-gray-500">Verarbeitet (24h)</div>
<div className="text-2xl font-bold text-gray-900">{agent.totalProcessed.toLocaleString()}</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="text-sm text-gray-500">Avg. Antwortzeit</div>
<div className="text-2xl font-bold text-gray-900">{agent.avgResponseTime}ms</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="text-sm text-gray-500">Fehlerrate</div>
<div className="text-2xl font-bold text-amber-600">{agent.errorRate}%</div>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<div className="text-sm text-gray-500">Version</div>
<div className="text-2xl font-bold text-gray-900">{agent.version}</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
<div className="border-b border-gray-200">
<div className="flex">
<button
onClick={() => setActiveTab('soul')}
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'soul'
? 'border-teal-500 text-teal-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<FileText className="w-4 h-4" />
SOUL-File
</button>
<button
onClick={() => setActiveTab('stats')}
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'stats'
? 'border-teal-500 text-teal-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<Activity className="w-4 h-4" />
Live-Statistiken
</button>
<button
onClick={() => setActiveTab('history')}
className={`flex items-center gap-2 px-6 py-4 text-sm font-medium border-b-2 transition-colors ${
activeTab === 'history'
? 'border-teal-500 text-teal-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
<History className="w-4 h-4" />
Aenderungshistorie
</button>
</div>
</div>
{/* Tab Content */}
<div className="p-6">
{activeTab === 'soul' && (
<div>
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-2 text-sm text-gray-500">
<FileText className="w-4 h-4" />
{agent.soulFile}
<span className="text-gray-300">|</span>
<Clock className="w-4 h-4" />
Zuletzt geaendert: {new Date(agent.updatedAt).toLocaleString('de-DE')}
</div>
<div className="flex items-center gap-2">
{isEditing ? (
<>
<button
onClick={handleReset}
disabled={!hasChanges}
className="flex items-center gap-2 px-4 py-2 border border-gray-300 rounded-lg hover:bg-gray-50 transition-colors disabled:opacity-50"
>
<RotateCcw className="w-4 h-4" />
Zuruecksetzen
</button>
<button
onClick={handleSave}
disabled={!hasChanges || saving}
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors disabled:opacity-50"
>
<Save className="w-4 h-4" />
{saving ? 'Speichert...' : 'Speichern'}
</button>
</>
) : (
<button
onClick={() => setIsEditing(true)}
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors"
>
<Edit3 className="w-4 h-4" />
Bearbeiten
</button>
)}
</div>
</div>
{hasChanges && (
<div className="mb-4 p-3 bg-amber-50 border border-amber-200 rounded-lg flex items-center gap-2 text-amber-700">
<AlertTriangle className="w-4 h-4" />
<span className="text-sm">Ungespeicherte Aenderungen vorhanden</span>
</div>
)}
<div className="relative">
{isEditing ? (
<textarea
value={editedContent}
onChange={(e) => handleContentChange(e.target.value)}
className="w-full h-[600px] p-4 font-mono text-sm bg-gray-50 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-teal-500 focus:border-transparent resize-none"
spellCheck={false}
/>
) : (
<div className="w-full h-[600px] p-4 font-mono text-sm bg-gray-50 border border-gray-200 rounded-lg overflow-auto whitespace-pre-wrap">
{agent.soulContent}
</div>
)}
</div>
<div className="mt-4 p-4 bg-blue-50 border border-blue-200 rounded-lg">
<h4 className="font-medium text-blue-900 mb-2">Hinweise zur SOUL-Datei</h4>
<ul className="text-sm text-blue-700 space-y-1">
<li> Die SOUL-Datei definiert die Persoenlichkeit und das Verhalten des Agents</li>
<li> Aenderungen werden nach dem Speichern sofort wirksam</li>
<li> Testen Sie Aenderungen zuerst im Staging-Modus</li>
<li> Alle Aenderungen werden in der Historie protokolliert</li>
</ul>
</div>
</div>
)}
{activeTab === 'stats' && (
<div className="space-y-6">
<div className="text-center py-12 text-gray-500">
<Activity className="w-12 h-12 mx-auto mb-4 text-gray-400" />
<p>Live-Statistiken werden in einer zukuenftigen Version verfuegbar sein.</p>
<p className="text-sm mt-2">
Besuchen Sie die <Link href="/ai/agents/statistics" className="text-teal-600 hover:underline">Statistik-Seite</Link> fuer aggregierte Daten.
</p>
</div>
</div>
)}
{activeTab === 'history' && (
<div>
<div className="space-y-4">
{mockChangeLogs.map((log) => (
<div key={log.id} className="flex items-start gap-4 p-4 bg-gray-50 rounded-lg">
<div className="p-2 bg-white rounded-full border border-gray-200">
<History className="w-4 h-4 text-gray-500" />
</div>
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium text-gray-900">{log.action}</span>
<span className="text-sm text-gray-500">
{new Date(log.timestamp).toLocaleString('de-DE')}
</span>
</div>
<p className="text-sm text-gray-600 mt-1">{log.description}</p>
<p className="text-xs text-gray-400 mt-1">von {log.user}</p>
</div>
</div>
))}
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,779 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
import { ArrowLeft, Cpu, Brain, MessageSquare, Database, Activity, Shield, ChevronDown, ChevronRight, GitBranch, Layers, Server, FileText, AlertTriangle, CheckCircle, Zap, RefreshCw } from 'lucide-react'
interface Section {
id: string
title: string
icon: React.ReactNode
content: React.ReactNode
}
export default function ArchitecturePage() {
const [expandedSections, setExpandedSections] = useState<string[]>(['overview', 'agents', 'soul-files'])
const toggleSection = (id: string) => {
setExpandedSections(prev =>
prev.includes(id)
? prev.filter(s => s !== id)
: [...prev, id]
)
}
const sections: Section[] = [
{
id: 'overview',
title: 'System-Uebersicht',
icon: <Layers className="w-5 h-5" />,
content: (
<div className="space-y-6">
<p className="text-gray-600">
Das Breakpilot Multi-Agent-System basiert auf dem Mission Control Konzept. Es ermoeglicht
die Koordination mehrerer spezialisierter KI-Agents, die gemeinsam komplexe Aufgaben loesen.
</p>
{/* Architecture Diagram */}
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm overflow-x-auto">
<pre className="text-gray-700">{`
┌─────────────────────────────────────────────────────────────────┐
│ Breakpilot Services │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────────┐ │
│ │Voice Service│ │Klausur Svc │ │ Admin-v2 / AlertAgent │ │
│ └──────┬──────┘ └──────┬──────┘ └───────────┬─────────────┘ │
│ │ │ │ │
│ └────────────────┼──────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────────────┐ │
│ │ Agent Core │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌───────────────────┐ │ │
│ │ │ Sessions │ │Shared Brain │ │ Orchestrator │ │ │
│ │ │ - Manager │ │ - Memory │ │ - Message Bus │ │ │
│ │ │ - Heartbeat │ │ - Context │ │ - Supervisor │ │ │
│ │ │ - Checkpoint│ │ - Knowledge │ │ - Task Router │ │ │
│ │ └─────────────┘ └─────────────┘ └───────────────────┘ │ │
│ └───────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌───────────────────────▼───────────────────────────────────┐ │
│ │ Infrastructure │ │
│ │ Valkey (Redis) PostgreSQL Qdrant │ │
│ └───────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
`}</pre>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Server className="w-5 h-5 text-blue-600" />
<span className="font-semibold text-blue-900">Session Management</span>
</div>
<p className="text-sm text-blue-700">
Verwaltet Agent-Lifecycles mit State Machine, Checkpoints und automatischer Recovery.
</p>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<Brain className="w-5 h-5 text-purple-600" />
<span className="font-semibold text-purple-900">Shared Brain</span>
</div>
<p className="text-sm text-purple-700">
Gemeinsames Gedaechtnis fuer alle Agents mit TTL, Context-Verwaltung und Knowledge Graph.
</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
<div className="flex items-center gap-2 mb-2">
<GitBranch className="w-5 h-5 text-green-600" />
<span className="font-semibold text-green-900">Orchestrator</span>
</div>
<p className="text-sm text-green-700">
Message Bus, Supervisor und Task Router fuer die Agent-Koordination.
</p>
</div>
</div>
</div>
)
},
{
id: 'agents',
title: 'Agent-Typen',
icon: <Cpu className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
Jeder Agent hat eine spezialisierte Rolle im System. Die Agents kommunizieren ueber den Message Bus
und nutzen das Shared Brain fuer konsistente Entscheidungen.
</p>
<div className="grid gap-4">
{/* TutorAgent */}
<div className="border border-gray-200 rounded-xl p-4 hover:border-blue-300 transition-colors">
<div className="flex items-start gap-4">
<div className="p-3 bg-blue-100 rounded-lg">
<Brain className="w-6 h-6 text-blue-600" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">TutorAgent</h4>
<p className="text-sm text-gray-600 mb-2">Lernbegleitung und Fragen beantworten</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full">Geduldig</span>
<span className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full">Ermutigend</span>
<span className="px-2 py-1 bg-blue-50 text-blue-700 text-xs rounded-full">Sokratisch</span>
</div>
<div className="mt-2 text-xs text-gray-500">
SOUL: tutor-agent.soul.md | Routing: learning_*, help_*, question_*
</div>
</div>
</div>
</div>
{/* GraderAgent */}
<div className="border border-gray-200 rounded-xl p-4 hover:border-green-300 transition-colors">
<div className="flex items-start gap-4">
<div className="p-3 bg-green-100 rounded-lg">
<CheckCircle className="w-6 h-6 text-green-600" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">GraderAgent</h4>
<p className="text-sm text-gray-600 mb-2">Klausur-Korrektur und Bewertung</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full">Objektiv</span>
<span className="px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full">Fair</span>
<span className="px-2 py-1 bg-green-50 text-green-700 text-xs rounded-full">Konstruktiv</span>
</div>
<div className="mt-2 text-xs text-gray-500">
SOUL: grader-agent.soul.md | Routing: grade_*, evaluate_*, correct_*
</div>
</div>
</div>
</div>
{/* QualityJudge */}
<div className="border border-gray-200 rounded-xl p-4 hover:border-amber-300 transition-colors">
<div className="flex items-start gap-4">
<div className="p-3 bg-amber-100 rounded-lg">
<Shield className="w-6 h-6 text-amber-600" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">QualityJudge</h4>
<p className="text-sm text-gray-600 mb-2">BQAS Qualitaetspruefung</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-amber-50 text-amber-700 text-xs rounded-full">Kritisch</span>
<span className="px-2 py-1 bg-amber-50 text-amber-700 text-xs rounded-full">Praezise</span>
<span className="px-2 py-1 bg-amber-50 text-amber-700 text-xs rounded-full">Schnell</span>
</div>
<div className="mt-2 text-xs text-gray-500">
SOUL: quality-judge.soul.md | Routing: quality_*, review_*, validate_*
</div>
</div>
</div>
</div>
{/* AlertAgent */}
<div className="border border-gray-200 rounded-xl p-4 hover:border-red-300 transition-colors">
<div className="flex items-start gap-4">
<div className="p-3 bg-red-100 rounded-lg">
<AlertTriangle className="w-6 h-6 text-red-600" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">AlertAgent</h4>
<p className="text-sm text-gray-600 mb-2">Monitoring und Benachrichtigungen</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-red-50 text-red-700 text-xs rounded-full">Wachsam</span>
<span className="px-2 py-1 bg-red-50 text-red-700 text-xs rounded-full">Proaktiv</span>
<span className="px-2 py-1 bg-red-50 text-red-700 text-xs rounded-full">Priorisierend</span>
</div>
<div className="mt-2 text-xs text-gray-500">
SOUL: alert-agent.soul.md | Routing: alert_*, monitor_*, notify_*
</div>
</div>
</div>
</div>
{/* Orchestrator */}
<div className="border border-gray-200 rounded-xl p-4 hover:border-purple-300 transition-colors">
<div className="flex items-start gap-4">
<div className="p-3 bg-purple-100 rounded-lg">
<MessageSquare className="w-6 h-6 text-purple-600" />
</div>
<div className="flex-1">
<h4 className="font-semibold text-gray-900">Orchestrator</h4>
<p className="text-sm text-gray-600 mb-2">Task-Koordination und Routing</p>
<div className="flex flex-wrap gap-2">
<span className="px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full">Koordinierend</span>
<span className="px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full">Effizient</span>
<span className="px-2 py-1 bg-purple-50 text-purple-700 text-xs rounded-full">Zuverlaessig</span>
</div>
<div className="mt-2 text-xs text-gray-500">
SOUL: orchestrator.soul.md | Routing: Fallback fuer alle unbekannten Intents
</div>
</div>
</div>
</div>
</div>
</div>
)
},
{
id: 'soul-files',
title: 'SOUL-Files (Persoenlichkeiten)',
icon: <FileText className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
SOUL-Dateien (Semantic Outline for Unified Learning) definieren die Persoenlichkeit und
Verhaltensregeln jedes Agents. Sie bestimmen, wie ein Agent kommuniziert, entscheidet und eskaliert.
</p>
<div className="bg-gray-900 rounded-xl p-6 text-gray-100 font-mono text-sm overflow-x-auto">
<div className="text-gray-400 mb-4"># Beispiel: tutor-agent.soul.md</div>
<pre className="text-green-400">{`
# TutorAgent SOUL
## Identitaet
Du bist ein geduldiger, ermutigender Lernbegleiter fuer Schueler.
Dein Ziel ist es, Verstaendnis zu foerdern, nicht Antworten vorzugeben.
## Kommunikationsstil
- Verwende einfache, klare Sprache
- Stelle Rueckfragen, um Verstaendnis zu pruefen
- Gib Hinweise statt direkter Loesungen
- Feiere kleine Erfolge
## Fachgebiete
- Mathematik (Grundschule bis Abitur)
- Naturwissenschaften (Physik, Chemie, Biologie)
- Sprachen (Deutsch, Englisch)
## Einschraenkungen
- Gib NIEMALS vollstaendige Loesungen fuer Hausaufgaben
- Verweise bei komplexen Themen auf Lehrkraefte
- Erkenne Frustration und biete Pausen an
## Eskalation
- Bei wiederholtem Unverstaendnis: Schlage alternatives Erklaerformat vor
- Bei emotionaler Belastung: Empfehle Gespraech mit Vertrauensperson
- Bei technischen Problemen: Eskaliere an Support
`}</pre>
</div>
<div className="mt-6">
<h4 className="font-semibold text-gray-900 mb-3">SOUL-Struktur</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h5 className="font-medium text-gray-900 mb-2">Identitaet</h5>
<p className="text-sm text-gray-600">Wer ist der Agent? Welche Rolle nimmt er ein?</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h5 className="font-medium text-gray-900 mb-2">Kommunikationsstil</h5>
<p className="text-sm text-gray-600">Wie kommuniziert der Agent mit Benutzern?</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h5 className="font-medium text-gray-900 mb-2">Fachgebiete</h5>
<p className="text-sm text-gray-600">In welchen Bereichen ist der Agent kompetent?</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4">
<h5 className="font-medium text-gray-900 mb-2">Einschraenkungen</h5>
<p className="text-sm text-gray-600">Was darf der Agent NICHT tun?</p>
</div>
<div className="bg-white border border-gray-200 rounded-lg p-4 md:col-span-2">
<h5 className="font-medium text-gray-900 mb-2">Eskalation</h5>
<p className="text-sm text-gray-600">Wann und wie eskaliert der Agent an andere Agents oder Menschen?</p>
</div>
</div>
</div>
</div>
)
},
{
id: 'message-bus',
title: 'Message Bus & Kommunikation',
icon: <MessageSquare className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
Der Message Bus ermoeglicht die asynchrone Kommunikation zwischen Agents via Redis Pub/Sub.
Er unterstuetzt Prioritaeten, Request-Response-Pattern und Broadcast-Nachrichten.
</p>
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm">
<div className="text-gray-500 mb-2"># Nachrichtenfluss</div>
<pre className="text-gray-700">{`
┌──────────────┐ ┌──────────────┐
│ Sender │ │ Receiver │
│ (Agent) │ │ (Agent) │
└──────┬───────┘ └──────▲───────┘
│ │
│ publish(AgentMessage) │ handle(message)
│ │
▼ │
┌────────────────────────────────────────────────────────┐
│ Message Bus │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ Priority Q │ │ Routing │ │ Logging │ │
│ │ HIGH/NORMAL │ │ Rules │ │ Audit │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
│ Redis Pub/Sub │
└────────────────────────────────────────────────────────┘
`}</pre>
</div>
<div className="mt-6">
<h4 className="font-semibold text-gray-900 mb-3">Nachrichtentypen</h4>
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-200 rounded-lg">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Typ</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Prioritaet</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-4 py-2 text-sm font-mono text-gray-700">task_request</td>
<td className="px-4 py-2"><span className="px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded">NORMAL</span></td>
<td className="px-4 py-2 text-sm text-gray-600">Neue Aufgabe an Agent senden</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-gray-700">task_response</td>
<td className="px-4 py-2"><span className="px-2 py-1 bg-yellow-100 text-yellow-700 text-xs rounded">NORMAL</span></td>
<td className="px-4 py-2 text-sm text-gray-600">Antwort auf task_request</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-gray-700">escalation</td>
<td className="px-4 py-2"><span className="px-2 py-1 bg-orange-100 text-orange-700 text-xs rounded">HIGH</span></td>
<td className="px-4 py-2 text-sm text-gray-600">Eskalation an anderen Agent</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-gray-700">alert</td>
<td className="px-4 py-2"><span className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">CRITICAL</span></td>
<td className="px-4 py-2 text-sm text-gray-600">Kritische Benachrichtigung</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-gray-700">heartbeat</td>
<td className="px-4 py-2"><span className="px-2 py-1 bg-gray-100 text-gray-700 text-xs rounded">LOW</span></td>
<td className="px-4 py-2 text-sm text-gray-600">Liveness-Signal</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
)
},
{
id: 'shared-brain',
title: 'Shared Brain (Gedaechtnis)',
icon: <Brain className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
Das Shared Brain speichert Wissen und Kontext, auf den alle Agents zugreifen koennen.
Es besteht aus drei Komponenten: Memory Store, Context Manager und Knowledge Graph.
</p>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center gap-2 mb-3">
<Database className="w-5 h-5 text-blue-600" />
<h4 className="font-semibold text-gray-900">Memory Store</h4>
</div>
<p className="text-sm text-gray-600 mb-3">
Langzeit-Gedaechtnis fuer Fakten, Entscheidungen und Lernfortschritte.
</p>
<ul className="text-xs text-gray-500 space-y-1">
<li>- TTL-basierte Expiration (30 Tage default)</li>
<li>- Access-Tracking (Haeufigkeit)</li>
<li>- Pattern-basierte Suche</li>
<li>- Hybrid: Redis + PostgreSQL</li>
</ul>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center gap-2 mb-3">
<Activity className="w-5 h-5 text-purple-600" />
<h4 className="font-semibold text-gray-900">Context Manager</h4>
</div>
<p className="text-sm text-gray-600 mb-3">
Verwaltet Konversationskontext mit automatischer Komprimierung.
</p>
<ul className="text-xs text-gray-500 space-y-1">
<li>- Max 50 Messages pro Context</li>
<li>- Automatische Zusammenfassung</li>
<li>- System-Messages bleiben erhalten</li>
<li>- Entity-Extraktion</li>
</ul>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center gap-2 mb-3">
<GitBranch className="w-5 h-5 text-green-600" />
<h4 className="font-semibold text-gray-900">Knowledge Graph</h4>
</div>
<p className="text-sm text-gray-600 mb-3">
Graph-basierte Darstellung von Entitaeten und ihren Beziehungen.
</p>
<ul className="text-xs text-gray-500 space-y-1">
<li>- Entitaeten: Student, Lehrer, Fach</li>
<li>- Beziehungen: lernt, unterrichtet</li>
<li>- BFS-basierte Pfadsuche</li>
<li>- Verwandte Entitaeten finden</li>
</ul>
</div>
</div>
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm mt-6">
<div className="text-gray-500 mb-2"># Memory Store Beispiel</div>
<pre className="text-gray-700">{`
# Speichern
await store.remember(
key="student:123:progress",
value={"level": 5, "score": 85, "topic": "algebra"},
agent_id="tutor-agent",
ttl_days=30
)
# Abrufen
progress = await store.recall("student:123:progress")
# → {"level": 5, "score": 85, "topic": "algebra"}
# Suchen
all_progress = await store.search("student:123:*")
# → [Memory(...), Memory(...), ...]
`}</pre>
</div>
</div>
)
},
{
id: 'task-routing',
title: 'Task Routing',
icon: <Zap className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
Der Task Router entscheidet, welcher Agent eine Anfrage bearbeitet. Er verwendet
Intent-basierte Regeln mit Prioritaeten und Fallback-Ketten.
</p>
<div className="overflow-x-auto">
<table className="min-w-full border border-gray-200 rounded-lg">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Intent-Pattern</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Ziel-Agent</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Prioritaet</th>
<th className="px-4 py-2 text-left text-sm font-medium text-gray-900">Fallback</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-4 py-2 text-sm font-mono text-blue-700">learning_*</td>
<td className="px-4 py-2 text-sm text-gray-700">TutorAgent</td>
<td className="px-4 py-2 text-sm text-gray-700">10</td>
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-blue-700">help_*, question_*</td>
<td className="px-4 py-2 text-sm text-gray-700">TutorAgent</td>
<td className="px-4 py-2 text-sm text-gray-700">8</td>
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-green-700">grade_*, evaluate_*</td>
<td className="px-4 py-2 text-sm text-gray-700">GraderAgent</td>
<td className="px-4 py-2 text-sm text-gray-700">10</td>
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-amber-700">quality_*, review_*</td>
<td className="px-4 py-2 text-sm text-gray-700">QualityJudge</td>
<td className="px-4 py-2 text-sm text-gray-700">10</td>
<td className="px-4 py-2 text-sm text-gray-500">GraderAgent</td>
</tr>
<tr>
<td className="px-4 py-2 text-sm font-mono text-red-700">alert_*, monitor_*</td>
<td className="px-4 py-2 text-sm text-gray-700">AlertAgent</td>
<td className="px-4 py-2 text-sm text-gray-700">10</td>
<td className="px-4 py-2 text-sm text-gray-500">Orchestrator</td>
</tr>
<tr className="bg-gray-50">
<td className="px-4 py-2 text-sm font-mono text-gray-500">* (alle anderen)</td>
<td className="px-4 py-2 text-sm text-gray-700">Orchestrator</td>
<td className="px-4 py-2 text-sm text-gray-700">0</td>
<td className="px-4 py-2 text-sm text-gray-500">-</td>
</tr>
</tbody>
</table>
</div>
<div className="mt-6 grid grid-cols-1 md:grid-cols-2 gap-4">
<div className="bg-white border border-gray-200 rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2">Routing-Strategien</h4>
<ul className="text-sm text-gray-600 space-y-2">
<li><span className="font-mono text-blue-600">ROUND_ROBIN</span> - Gleichmaessige Verteilung</li>
<li><span className="font-mono text-blue-600">LEAST_LOADED</span> - Agent mit wenigsten Tasks</li>
<li><span className="font-mono text-blue-600">PRIORITY</span> - Hoechste Prioritaet zuerst</li>
<li><span className="font-mono text-blue-600">RANDOM</span> - Zufaellige Auswahl</li>
</ul>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2">Fallback-Verhalten</h4>
<ul className="text-sm text-gray-600 space-y-2">
<li>1. Versuche Ziel-Agent zu erreichen</li>
<li>2. Bei Timeout: Fallback-Agent nutzen</li>
<li>3. Bei Fehler: Orchestrator uebernimmt</li>
<li>4. Bei kritischen Fehlern: Alert an Admin</li>
</ul>
</div>
</div>
</div>
)
},
{
id: 'session-lifecycle',
title: 'Session Lifecycle',
icon: <RefreshCw className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
Sessions verwalten den Zustand von Agent-Interaktionen. Jede Session hat einen definierten
Lebenszyklus mit Checkpoints fuer Recovery.
</p>
<div className="bg-gray-50 rounded-xl p-6 font-mono text-sm">
<div className="text-gray-500 mb-2"># Session State Machine</div>
<pre className="text-gray-700">{`
┌─────────────────────────────────────┐
│ │
▼ │
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ ACTIVE │───▶│ PAUSED │───▶│ COMPLETED│ │ FAILED │
└──────────┘ └──────────┘ └──────────┘ └──────────┘
│ │ ▲
│ │ │
└───────────────┴───────────────────────────────┘
(bei Fehler)
States:
- ACTIVE: Session laeuft, Agent verarbeitet Tasks
- PAUSED: Session pausiert, wartet auf Eingabe
- COMPLETED: Session erfolgreich beendet
- FAILED: Session mit Fehler beendet
`}</pre>
</div>
<div className="mt-6">
<h4 className="font-semibold text-gray-900 mb-3">Heartbeat Monitoring</h4>
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-gray-900">30s</div>
<div className="text-sm text-gray-500">Timeout</div>
</div>
<div>
<div className="text-2xl font-bold text-gray-900">5s</div>
<div className="text-sm text-gray-500">Check Interval</div>
</div>
<div>
<div className="text-2xl font-bold text-gray-900">3</div>
<div className="text-sm text-gray-500">Max Missed Beats</div>
</div>
</div>
<p className="text-sm text-gray-600 mt-4 text-center">
Nach 3 verpassten Heartbeats wird der Agent als ausgefallen markiert und die
Restart-Policy greift (max. 3 Versuche).
</p>
</div>
</div>
</div>
)
},
{
id: 'database',
title: 'Datenbank-Schema',
icon: <Database className="w-5 h-5" />,
content: (
<div className="space-y-4">
<p className="text-gray-600 mb-4">
Das Agent-System nutzt PostgreSQL fuer persistente Daten und Valkey (Redis) fuer Caching und Pub/Sub.
</p>
<div className="space-y-4">
{/* agent_sessions */}
<div className="bg-white border border-gray-200 rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2 font-mono">agent_sessions</h4>
<p className="text-sm text-gray-600 mb-3">Speichert Session-Daten mit Checkpoints</p>
<div className="bg-gray-50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
<pre>{`
CREATE TABLE agent_sessions (
id UUID PRIMARY KEY,
agent_type VARCHAR(50) NOT NULL,
user_id UUID REFERENCES users(id),
state VARCHAR(20) NOT NULL DEFAULT 'active',
context JSONB DEFAULT '{}',
checkpoints JSONB DEFAULT '[]',
created_at TIMESTAMPTZ DEFAULT NOW(),
updated_at TIMESTAMPTZ DEFAULT NOW(),
last_heartbeat TIMESTAMPTZ DEFAULT NOW()
);
`}</pre>
</div>
</div>
{/* agent_memory */}
<div className="bg-white border border-gray-200 rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2 font-mono">agent_memory</h4>
<p className="text-sm text-gray-600 mb-3">Langzeit-Gedaechtnis mit TTL</p>
<div className="bg-gray-50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
<pre>{`
CREATE TABLE agent_memory (
id UUID PRIMARY KEY,
namespace VARCHAR(100) NOT NULL,
key VARCHAR(500) NOT NULL,
value JSONB NOT NULL,
agent_id VARCHAR(50) NOT NULL,
access_count INTEGER DEFAULT 0,
created_at TIMESTAMPTZ DEFAULT NOW(),
expires_at TIMESTAMPTZ,
UNIQUE(namespace, key)
);
`}</pre>
</div>
</div>
{/* agent_messages */}
<div className="bg-white border border-gray-200 rounded-xl p-4">
<h4 className="font-semibold text-gray-900 mb-2 font-mono">agent_messages</h4>
<p className="text-sm text-gray-600 mb-3">Audit-Trail fuer Inter-Agent Kommunikation</p>
<div className="bg-gray-50 rounded-lg p-3 font-mono text-xs overflow-x-auto">
<pre>{`
CREATE TABLE agent_messages (
id UUID PRIMARY KEY,
sender VARCHAR(50) NOT NULL,
receiver VARCHAR(50) NOT NULL,
message_type VARCHAR(50) NOT NULL,
payload JSONB NOT NULL,
priority INTEGER DEFAULT 1,
correlation_id UUID,
created_at TIMESTAMPTZ DEFAULT NOW()
);
`}</pre>
</div>
</div>
</div>
</div>
)
}
]
return (
<div className="p-6 max-w-5xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link
href="/ai/agents"
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
>
<ArrowLeft className="w-4 h-4" />
Zurueck zur Agent-Verwaltung
</Link>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="p-2 bg-purple-100 rounded-lg">
<FileText className="w-6 h-6 text-purple-600" />
</div>
Multi-Agent Architektur
</h1>
<p className="text-gray-500 mt-1">
Technische Dokumentation des Breakpilot Multi-Agent-Systems
</p>
</div>
{/* Table of Contents */}
<div className="bg-gray-50 rounded-xl p-5 mb-8">
<h2 className="font-semibold text-gray-900 mb-3">Inhaltsverzeichnis</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-2">
{sections.map(section => (
<button
key={section.id}
onClick={() => {
if (!expandedSections.includes(section.id)) {
setExpandedSections(prev => [...prev, section.id])
}
document.getElementById(section.id)?.scrollIntoView({ behavior: 'smooth' })
}}
className="flex items-center gap-2 text-sm text-gray-600 hover:text-teal-600 text-left p-2 rounded-lg hover:bg-gray-100 transition-colors"
>
{section.icon}
<span className="truncate">{section.title}</span>
</button>
))}
</div>
</div>
{/* Sections */}
<div className="space-y-4">
{sections.map(section => (
<div
key={section.id}
id={section.id}
className="bg-white border border-gray-200 rounded-xl overflow-hidden"
>
<button
onClick={() => toggleSection(section.id)}
className="w-full flex items-center justify-between p-5 hover:bg-gray-50 transition-colors"
>
<div className="flex items-center gap-3">
<div className="p-2 bg-gray-100 rounded-lg">
{section.icon}
</div>
<span className="font-semibold text-gray-900">{section.title}</span>
</div>
{expandedSections.includes(section.id) ? (
<ChevronDown className="w-5 h-5 text-gray-400" />
) : (
<ChevronRight className="w-5 h-5 text-gray-400" />
)}
</button>
{expandedSections.includes(section.id) && (
<div className="px-5 pb-5 border-t border-gray-100 pt-4">
{section.content}
</div>
)}
</div>
))}
</div>
{/* Footer Links */}
<div className="mt-8 bg-teal-50 border border-teal-200 rounded-xl p-5">
<h3 className="font-semibold text-teal-900 mb-3">Weiterführende Ressourcen</h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
<Link
href="/ai/agents"
className="flex items-center gap-2 text-sm text-teal-700 hover:text-teal-900"
>
<Cpu className="w-4 h-4" />
Agent-Uebersicht
</Link>
<Link
href="/ai/agents/sessions"
className="flex items-center gap-2 text-sm text-teal-700 hover:text-teal-900"
>
<Activity className="w-4 h-4" />
Aktive Sessions
</Link>
<Link
href="/ai/agents/statistics"
className="flex items-center gap-2 text-sm text-teal-700 hover:text-teal-900"
>
<Database className="w-4 h-4" />
Statistiken
</Link>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,390 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { Bot, Activity, Brain, Settings, FileText, BarChart3, Clock, AlertTriangle, CheckCircle, Pause, XCircle, ChevronRight, Cpu, MessageSquare, Database, RefreshCw } from 'lucide-react'
// Agent types
interface AgentConfig {
id: string
name: string
description: string
soulFile: string
color: string
icon: 'bot' | 'brain' | 'message' | 'alert' | 'settings'
status: 'running' | 'paused' | 'stopped' | 'error'
activeSessions: number
totalProcessed: number
avgResponseTime: number
lastActivity: string
}
interface AgentStats {
totalSessions: number
activeSessions: number
totalMessages: number
avgLatency: number
errorRate: number
memoryUsage: number
}
// Mock data - In production, fetch from API
const mockAgents: AgentConfig[] = [
{
id: 'tutor-agent',
name: 'TutorAgent',
description: 'Lernbegleitung und Fragen beantworten',
soulFile: 'tutor-agent.soul.md',
color: '#3b82f6',
icon: 'brain',
status: 'running',
activeSessions: 12,
totalProcessed: 1847,
avgResponseTime: 234,
lastActivity: '2 min ago'
},
{
id: 'grader-agent',
name: 'GraderAgent',
description: 'Klausur-Korrektur und Bewertung',
soulFile: 'grader-agent.soul.md',
color: '#10b981',
icon: 'bot',
status: 'running',
activeSessions: 3,
totalProcessed: 456,
avgResponseTime: 1205,
lastActivity: '5 min ago'
},
{
id: 'quality-judge',
name: 'QualityJudge',
description: 'BQAS Qualitaetspruefung',
soulFile: 'quality-judge.soul.md',
color: '#f59e0b',
icon: 'settings',
status: 'running',
activeSessions: 8,
totalProcessed: 3291,
avgResponseTime: 89,
lastActivity: '1 min ago'
},
{
id: 'alert-agent',
name: 'AlertAgent',
description: 'Monitoring und Benachrichtigungen',
soulFile: 'alert-agent.soul.md',
color: '#ef4444',
icon: 'alert',
status: 'running',
activeSessions: 1,
totalProcessed: 892,
avgResponseTime: 45,
lastActivity: '30 sec ago'
},
{
id: 'orchestrator',
name: 'Orchestrator',
description: 'Task-Koordination und Routing',
soulFile: 'orchestrator.soul.md',
color: '#8b5cf6',
icon: 'message',
status: 'running',
activeSessions: 24,
totalProcessed: 8934,
avgResponseTime: 12,
lastActivity: 'just now'
},
{
id: 'compliance-advisor',
name: 'Compliance Advisor',
description: 'DSGVO/Compliance-Berater fuer SDK-Nutzer',
soulFile: 'compliance-advisor.soul.md',
color: '#6366f1',
icon: 'message',
status: 'running',
activeSessions: 0,
totalProcessed: 0,
avgResponseTime: 0,
lastActivity: new Date().toISOString()
}
]
const mockStats: AgentStats = {
totalSessions: 156,
activeSessions: 48,
totalMessages: 15420,
avgLatency: 156,
errorRate: 0.8,
memoryUsage: 67
}
function getIconComponent(icon: string, className: string) {
switch(icon) {
case 'bot': return <Bot className={className} />
case 'brain': return <Brain className={className} />
case 'message': return <MessageSquare className={className} />
case 'alert': return <AlertTriangle className={className} />
case 'settings': return <Settings className={className} />
default: return <Bot className={className} />
}
}
function getStatusIcon(status: string) {
switch(status) {
case 'running': return <CheckCircle className="w-4 h-4 text-green-500" />
case 'paused': return <Pause className="w-4 h-4 text-yellow-500" />
case 'stopped': return <XCircle className="w-4 h-4 text-gray-500" />
case 'error': return <AlertTriangle className="w-4 h-4 text-red-500" />
default: return null
}
}
function getStatusColor(status: string) {
switch(status) {
case 'running': return 'bg-green-500/10 text-green-600 border-green-500/20'
case 'paused': return 'bg-yellow-500/10 text-yellow-600 border-yellow-500/20'
case 'stopped': return 'bg-gray-500/10 text-gray-600 border-gray-500/20'
case 'error': return 'bg-red-500/10 text-red-600 border-red-500/20'
default: return 'bg-gray-500/10 text-gray-600 border-gray-500/20'
}
}
export default function AgentManagementPage() {
const [agents, setAgents] = useState<AgentConfig[]>(mockAgents)
const [stats, setStats] = useState<AgentStats>(mockStats)
const [loading, setLoading] = useState(false)
const [lastRefresh, setLastRefresh] = useState(new Date())
const refreshData = async () => {
setLoading(true)
// In production, fetch from API
// const response = await fetch('/api/admin/agents/status')
await new Promise(resolve => setTimeout(resolve, 500))
setLastRefresh(new Date())
setLoading(false)
}
useEffect(() => {
// Auto-refresh every 30 seconds
const interval = setInterval(refreshData, 30000)
return () => clearInterval(interval)
}, [])
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="flex items-center justify-between mb-8">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="p-2 bg-teal-100 rounded-lg">
<Bot className="w-6 h-6 text-teal-600" />
</div>
Agent Management
</h1>
<p className="text-gray-500 mt-1">
Multi-Agent System verwalten, SOUL-Files bearbeiten, Statistiken analysieren
</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500">
Letzte Aktualisierung: {lastRefresh.toLocaleTimeString('de-DE')}
</span>
<button
onClick={refreshData}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Aktualisieren
</button>
</div>
</div>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-8">
<Link
href="/ai/agents/architecture"
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
>
<div className="p-2 bg-purple-100 rounded-lg group-hover:bg-purple-200 transition-colors">
<FileText className="w-5 h-5 text-purple-600" />
</div>
<div>
<div className="font-medium text-gray-900">Architektur</div>
<div className="text-sm text-gray-500">Dokumentation & Diagramme</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
</Link>
<Link
href="/ai/agents/sessions"
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
>
<div className="p-2 bg-blue-100 rounded-lg group-hover:bg-blue-200 transition-colors">
<Activity className="w-5 h-5 text-blue-600" />
</div>
<div>
<div className="font-medium text-gray-900">Sessions</div>
<div className="text-sm text-gray-500">{stats.activeSessions} aktiv</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
</Link>
<Link
href="/ai/agents/statistics"
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
>
<div className="p-2 bg-green-100 rounded-lg group-hover:bg-green-200 transition-colors">
<BarChart3 className="w-5 h-5 text-green-600" />
</div>
<div>
<div className="font-medium text-gray-900">Statistiken</div>
<div className="text-sm text-gray-500">Performance & Trends</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
</Link>
<Link
href="/ai/test-quality"
className="flex items-center gap-3 p-4 bg-white border border-gray-200 rounded-xl hover:border-teal-300 hover:shadow-md transition-all group"
>
<div className="p-2 bg-amber-100 rounded-lg group-hover:bg-amber-200 transition-colors">
<Cpu className="w-5 h-5 text-amber-600" />
</div>
<div>
<div className="font-medium text-gray-900">BQAS</div>
<div className="text-sm text-gray-500">Qualitaetssicherung</div>
</div>
<ChevronRight className="w-5 h-5 text-gray-400 ml-auto" />
</Link>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-8">
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Gesamt Sessions</div>
<div className="text-2xl font-bold text-gray-900">{stats.totalSessions.toLocaleString()}</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Aktive Sessions</div>
<div className="text-2xl font-bold text-green-600">{stats.activeSessions}</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Nachrichten (24h)</div>
<div className="text-2xl font-bold text-gray-900">{stats.totalMessages.toLocaleString()}</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Avg. Latenz</div>
<div className="text-2xl font-bold text-gray-900">{stats.avgLatency}ms</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Fehlerrate</div>
<div className="text-2xl font-bold text-amber-600">{stats.errorRate}%</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Memory Usage</div>
<div className="text-2xl font-bold text-gray-900">{stats.memoryUsage}%</div>
</div>
</div>
{/* Agents Grid */}
<div className="mb-6">
<h2 className="text-lg font-semibold text-gray-900 mb-4">Agents</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{agents.map((agent) => (
<Link
key={agent.id}
href={`/ai/agents/${agent.id}`}
className="bg-white border border-gray-200 rounded-xl p-5 hover:border-teal-300 hover:shadow-lg transition-all group"
>
{/* Header */}
<div className="flex items-start justify-between mb-4">
<div className="flex items-center gap-3">
<div
className="p-2.5 rounded-lg"
style={{ backgroundColor: `${agent.color}20` }}
>
{getIconComponent(agent.icon, `w-5 h-5`)}
<style jsx>{`
svg { color: ${agent.color}; }
`}</style>
</div>
<div>
<h3 className="font-semibold text-gray-900 group-hover:text-teal-600 transition-colors">
{agent.name}
</h3>
<p className="text-sm text-gray-500">{agent.description}</p>
</div>
</div>
<div className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs font-medium border ${getStatusColor(agent.status)}`}>
{getStatusIcon(agent.status)}
{agent.status}
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-3 gap-3 mb-4">
<div className="text-center p-2 bg-gray-50 rounded-lg">
<div className="text-lg font-semibold text-gray-900">{agent.activeSessions}</div>
<div className="text-xs text-gray-500">Sessions</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded-lg">
<div className="text-lg font-semibold text-gray-900">{agent.totalProcessed}</div>
<div className="text-xs text-gray-500">Verarbeitet</div>
</div>
<div className="text-center p-2 bg-gray-50 rounded-lg">
<div className="text-lg font-semibold text-gray-900">{agent.avgResponseTime}ms</div>
<div className="text-xs text-gray-500">Avg. Zeit</div>
</div>
</div>
{/* Footer */}
<div className="flex items-center justify-between pt-3 border-t border-gray-100">
<div className="flex items-center gap-2 text-sm text-gray-500">
<FileText className="w-4 h-4" />
{agent.soulFile}
</div>
<div className="flex items-center gap-1 text-sm text-gray-400">
<Clock className="w-3.5 h-3.5" />
{agent.lastActivity}
</div>
</div>
</Link>
))}
</div>
</div>
{/* Info Box */}
<div className="bg-teal-50 border border-teal-200 rounded-xl p-5">
<div className="flex gap-4">
<div className="p-2 bg-teal-100 rounded-lg h-fit">
<Brain className="w-5 h-5 text-teal-600" />
</div>
<div>
<h3 className="font-semibold text-teal-900 mb-2">Multi-Agent Architektur</h3>
<p className="text-sm text-teal-700 mb-3">
Das Breakpilot Multi-Agent-System basiert auf dem Mission Control Konzept. Jeder Agent hat eine
definierte Persoenlichkeit (SOUL-File), die sein Verhalten steuert. Die Agents kommunizieren
ueber einen Message Bus und nutzen ein gemeinsames Gedaechtnis (Shared Brain).
</p>
<div className="flex gap-3">
<Link
href="/ai/agents/architecture"
className="text-sm font-medium text-teal-600 hover:text-teal-800"
>
Architektur ansehen &rarr;
</Link>
<Link
href="/ai/agents/architecture#soul-files"
className="text-sm font-medium text-teal-600 hover:text-teal-800"
>
SOUL-Files verstehen &rarr;
</Link>
</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,444 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { ArrowLeft, Activity, Clock, User, Bot, Brain, MessageSquare, AlertTriangle, Settings, CheckCircle, Pause, XCircle, RefreshCw, Filter, Search, ChevronRight, Zap, MoreVertical } from 'lucide-react'
// Session types
interface AgentSession {
id: string
agentType: string
agentId: string
userId: string
userName: string
state: 'active' | 'paused' | 'completed' | 'failed'
createdAt: string
lastActivity: string
checkpointCount: number
messagesProcessed: number
currentTask: string | null
avgResponseTime: number
}
// Mock data
const mockSessions: AgentSession[] = [
{
id: 'session-001',
agentType: 'tutor-agent',
agentId: 'tutor-1',
userId: 'user-123',
userName: 'Max Mustermann',
state: 'active',
createdAt: '2026-02-03T14:30:00Z',
lastActivity: '2026-02-03T15:45:23Z',
checkpointCount: 5,
messagesProcessed: 23,
currentTask: 'Erklaere Quadratische Funktionen',
avgResponseTime: 245
},
{
id: 'session-002',
agentType: 'tutor-agent',
agentId: 'tutor-2',
userId: 'user-456',
userName: 'Anna Schmidt',
state: 'active',
createdAt: '2026-02-03T15:00:00Z',
lastActivity: '2026-02-03T15:44:12Z',
checkpointCount: 3,
messagesProcessed: 12,
currentTask: 'Hilfe bei Gedichtanalyse',
avgResponseTime: 312
},
{
id: 'session-003',
agentType: 'grader-agent',
agentId: 'grader-1',
userId: 'user-789',
userName: 'Frau Mueller (Lehrerin)',
state: 'active',
createdAt: '2026-02-03T14:00:00Z',
lastActivity: '2026-02-03T15:42:00Z',
checkpointCount: 12,
messagesProcessed: 45,
currentTask: 'Korrektur Klausur 10b - Arbeit 7/24',
avgResponseTime: 1205
},
{
id: 'session-004',
agentType: 'quality-judge',
agentId: 'judge-1',
userId: 'system',
userName: 'System (BQAS)',
state: 'active',
createdAt: '2026-02-03T08:00:00Z',
lastActivity: '2026-02-03T15:45:01Z',
checkpointCount: 156,
messagesProcessed: 892,
currentTask: 'Quality Check Queue Processing',
avgResponseTime: 89
},
{
id: 'session-005',
agentType: 'orchestrator',
agentId: 'orchestrator-main',
userId: 'system',
userName: 'System',
state: 'active',
createdAt: '2026-02-03T00:00:00Z',
lastActivity: '2026-02-03T15:45:30Z',
checkpointCount: 2341,
messagesProcessed: 8934,
currentTask: 'Routing incoming requests',
avgResponseTime: 12
},
{
id: 'session-006',
agentType: 'tutor-agent',
agentId: 'tutor-3',
userId: 'user-101',
userName: 'Tim Berger',
state: 'paused',
createdAt: '2026-02-03T13:00:00Z',
lastActivity: '2026-02-03T14:30:00Z',
checkpointCount: 8,
messagesProcessed: 34,
currentTask: null,
avgResponseTime: 278
},
{
id: 'session-007',
agentType: 'grader-agent',
agentId: 'grader-2',
userId: 'user-202',
userName: 'Herr Weber (Lehrer)',
state: 'completed',
createdAt: '2026-02-03T10:00:00Z',
lastActivity: '2026-02-03T12:00:00Z',
checkpointCount: 24,
messagesProcessed: 120,
currentTask: null,
avgResponseTime: 1102
},
{
id: 'session-008',
agentType: 'alert-agent',
agentId: 'alert-1',
userId: 'system',
userName: 'System (Monitoring)',
state: 'active',
createdAt: '2026-02-03T00:00:00Z',
lastActivity: '2026-02-03T15:45:28Z',
checkpointCount: 48,
messagesProcessed: 256,
currentTask: 'Monitoring System Health',
avgResponseTime: 45
}
]
function getAgentIcon(agentType: string) {
switch (agentType) {
case 'tutor-agent': return <Brain className="w-4 h-4" />
case 'grader-agent': return <Bot className="w-4 h-4" />
case 'quality-judge': return <Settings className="w-4 h-4" />
case 'alert-agent': return <AlertTriangle className="w-4 h-4" />
case 'orchestrator': return <MessageSquare className="w-4 h-4" />
default: return <Bot className="w-4 h-4" />
}
}
function getAgentColor(agentType: string) {
switch (agentType) {
case 'tutor-agent': return { bg: 'bg-blue-100', text: 'text-blue-600', border: 'border-blue-200' }
case 'grader-agent': return { bg: 'bg-green-100', text: 'text-green-600', border: 'border-green-200' }
case 'quality-judge': return { bg: 'bg-amber-100', text: 'text-amber-600', border: 'border-amber-200' }
case 'alert-agent': return { bg: 'bg-red-100', text: 'text-red-600', border: 'border-red-200' }
case 'orchestrator': return { bg: 'bg-purple-100', text: 'text-purple-600', border: 'border-purple-200' }
default: return { bg: 'bg-gray-100', text: 'text-gray-600', border: 'border-gray-200' }
}
}
function getStateConfig(state: string) {
switch (state) {
case 'active':
return { icon: <CheckCircle className="w-4 h-4" />, color: 'bg-green-100 text-green-700 border-green-200', label: 'Aktiv' }
case 'paused':
return { icon: <Pause className="w-4 h-4" />, color: 'bg-yellow-100 text-yellow-700 border-yellow-200', label: 'Pausiert' }
case 'completed':
return { icon: <CheckCircle className="w-4 h-4" />, color: 'bg-gray-100 text-gray-600 border-gray-200', label: 'Beendet' }
case 'failed':
return { icon: <XCircle className="w-4 h-4" />, color: 'bg-red-100 text-red-700 border-red-200', label: 'Fehlgeschlagen' }
default:
return { icon: null, color: 'bg-gray-100 text-gray-600 border-gray-200', label: state }
}
}
function formatDuration(isoDate: string): string {
const date = new Date(isoDate)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
const diffHours = Math.floor(diffMins / 60)
const diffDays = Math.floor(diffHours / 24)
if (diffDays > 0) return `${diffDays}d ${diffHours % 24}h`
if (diffHours > 0) return `${diffHours}h ${diffMins % 60}m`
return `${diffMins}m`
}
function formatTime(isoDate: string): string {
return new Date(isoDate).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
export default function SessionsPage() {
const [sessions, setSessions] = useState<AgentSession[]>(mockSessions)
const [loading, setLoading] = useState(false)
const [filter, setFilter] = useState<string>('all')
const [searchTerm, setSearchTerm] = useState('')
const [lastRefresh, setLastRefresh] = useState(new Date())
const refreshData = async () => {
setLoading(true)
// In production, fetch from API
await new Promise(resolve => setTimeout(resolve, 500))
setLastRefresh(new Date())
setLoading(false)
}
useEffect(() => {
const interval = setInterval(refreshData, 10000) // Refresh every 10s
return () => clearInterval(interval)
}, [])
// Filter sessions
const filteredSessions = sessions.filter(session => {
if (filter !== 'all' && session.state !== filter) return false
if (searchTerm) {
const search = searchTerm.toLowerCase()
return (
session.userName.toLowerCase().includes(search) ||
session.agentType.toLowerCase().includes(search) ||
session.currentTask?.toLowerCase().includes(search) ||
session.id.toLowerCase().includes(search)
)
}
return true
})
// Stats
const stats = {
total: sessions.length,
active: sessions.filter(s => s.state === 'active').length,
paused: sessions.filter(s => s.state === 'paused').length,
completed: sessions.filter(s => s.state === 'completed').length,
failed: sessions.filter(s => s.state === 'failed').length,
totalMessages: sessions.reduce((sum, s) => sum + s.messagesProcessed, 0),
avgResponseTime: Math.round(sessions.reduce((sum, s) => sum + s.avgResponseTime, 0) / sessions.length)
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link
href="/ai/agents"
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
>
<ArrowLeft className="w-4 h-4" />
Zurueck zur Agent-Verwaltung
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="p-2 bg-blue-100 rounded-lg">
<Activity className="w-6 h-6 text-blue-600" />
</div>
Aktive Sessions
</h1>
<p className="text-gray-500 mt-1">
Live-Uebersicht aller Agent-Sessions im System
</p>
</div>
<div className="flex items-center gap-3">
<span className="text-sm text-gray-500">
Letzte Aktualisierung: {lastRefresh.toLocaleTimeString('de-DE')}
</span>
<button
onClick={refreshData}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Aktualisieren
</button>
</div>
</div>
</div>
{/* Stats */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Gesamt</div>
<div className="text-2xl font-bold text-gray-900">{stats.total}</div>
</div>
<div className="bg-white border border-green-200 rounded-xl p-4">
<div className="text-sm text-green-600 mb-1">Aktiv</div>
<div className="text-2xl font-bold text-green-600">{stats.active}</div>
</div>
<div className="bg-white border border-yellow-200 rounded-xl p-4">
<div className="text-sm text-yellow-600 mb-1">Pausiert</div>
<div className="text-2xl font-bold text-yellow-600">{stats.paused}</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Beendet</div>
<div className="text-2xl font-bold text-gray-600">{stats.completed}</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Messages (24h)</div>
<div className="text-2xl font-bold text-gray-900">{stats.totalMessages.toLocaleString()}</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-4">
<div className="text-sm text-gray-500 mb-1">Avg. Response</div>
<div className="text-2xl font-bold text-gray-900">{stats.avgResponseTime}ms</div>
</div>
</div>
{/* Filters */}
<div className="flex flex-col md:flex-row gap-4 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<input
type="text"
placeholder="Session, Benutzer oder Task suchen..."
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
/>
</div>
<div className="flex items-center gap-2">
<Filter className="w-4 h-4 text-gray-400" />
<select
value={filter}
onChange={(e) => setFilter(e.target.value)}
className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="all">Alle Status</option>
<option value="active">Aktiv</option>
<option value="paused">Pausiert</option>
<option value="completed">Beendet</option>
<option value="failed">Fehlgeschlagen</option>
</select>
</div>
</div>
{/* Sessions List */}
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Agent</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Benutzer</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Status</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Aktueller Task</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Dauer</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Messages</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Letzte Aktivitaet</th>
<th className="px-4 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider"></th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{filteredSessions.map(session => {
const agentColor = getAgentColor(session.agentType)
const stateConfig = getStateConfig(session.state)
return (
<tr key={session.id} className="hover:bg-gray-50">
<td className="px-4 py-4 whitespace-nowrap">
<div className="flex items-center gap-3">
<div className={`p-2 rounded-lg ${agentColor.bg}`}>
<span className={agentColor.text}>{getAgentIcon(session.agentType)}</span>
</div>
<div>
<div className="font-medium text-gray-900">{session.agentId}</div>
<div className="text-xs text-gray-500">{session.agentType}</div>
</div>
</div>
</td>
<td className="px-4 py-4 whitespace-nowrap">
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-400" />
<span className="text-sm text-gray-900">{session.userName}</span>
</div>
</td>
<td className="px-4 py-4 whitespace-nowrap">
<span className={`inline-flex items-center gap-1.5 px-2.5 py-1 rounded-full text-xs font-medium border ${stateConfig.color}`}>
{stateConfig.icon}
{stateConfig.label}
</span>
</td>
<td className="px-4 py-4">
{session.currentTask ? (
<div className="flex items-center gap-2 max-w-xs">
<Zap className="w-3.5 h-3.5 text-amber-500 flex-shrink-0" />
<span className="text-sm text-gray-700 truncate">{session.currentTask}</span>
</div>
) : (
<span className="text-sm text-gray-400">-</span>
)}
</td>
<td className="px-4 py-4 whitespace-nowrap">
<div className="flex items-center gap-1.5 text-sm text-gray-600">
<Clock className="w-3.5 h-3.5" />
{formatDuration(session.createdAt)}
</div>
</td>
<td className="px-4 py-4 whitespace-nowrap">
<div className="text-sm">
<span className="font-medium text-gray-900">{session.messagesProcessed}</span>
<span className="text-gray-500 ml-1">({session.checkpointCount} CP)</span>
</div>
</td>
<td className="px-4 py-4 whitespace-nowrap">
<div className="text-sm text-gray-600">{formatTime(session.lastActivity)}</div>
<div className="text-xs text-gray-400">{session.avgResponseTime}ms avg</div>
</td>
<td className="px-4 py-4 whitespace-nowrap text-right">
<Link
href={`/ai/agents/${session.agentType.replace('-agent', '-agent')}`}
className="p-2 hover:bg-gray-100 rounded-lg inline-flex items-center gap-1 text-sm text-gray-600 hover:text-gray-900"
>
Details
<ChevronRight className="w-4 h-4" />
</Link>
</td>
</tr>
)
})}
</tbody>
</table>
</div>
{filteredSessions.length === 0 && (
<div className="text-center py-12">
<Activity className="w-12 h-12 text-gray-300 mx-auto mb-3" />
<p className="text-gray-500">Keine Sessions gefunden</p>
</div>
)}
</div>
{/* Live Activity Indicator */}
<div className="mt-6 flex items-center justify-center gap-2 text-sm text-gray-500">
<span className="relative flex h-2 w-2">
<span className="animate-ping absolute inline-flex h-full w-full rounded-full bg-green-400 opacity-75"></span>
<span className="relative inline-flex rounded-full h-2 w-2 bg-green-500"></span>
</span>
Live-Daten - Auto-Refresh alle 10 Sekunden
</div>
</div>
)
}

View File

@@ -0,0 +1,491 @@
'use client'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { ArrowLeft, BarChart3, TrendingUp, TrendingDown, Clock, Activity, Bot, Brain, MessageSquare, AlertTriangle, Settings, RefreshCw, Calendar, Filter, Download } from 'lucide-react'
// Types
interface AgentMetric {
agentType: string
name: string
color: string
sessions: number
messagesProcessed: number
avgResponseTime: number
errorRate: number
successRate: number
trend: 'up' | 'down' | 'stable'
trendValue: number
}
interface TimeSeriesData {
timestamp: string
value: number
}
interface DailyStats {
date: string
sessions: number
messages: number
errors: number
avgLatency: number
}
// Mock data
const mockAgentMetrics: AgentMetric[] = [
{
agentType: 'tutor-agent',
name: 'TutorAgent',
color: '#3b82f6',
sessions: 156,
messagesProcessed: 4521,
avgResponseTime: 234,
errorRate: 0.3,
successRate: 99.7,
trend: 'up',
trendValue: 12
},
{
agentType: 'grader-agent',
name: 'GraderAgent',
color: '#10b981',
sessions: 45,
messagesProcessed: 1205,
avgResponseTime: 1102,
errorRate: 0.5,
successRate: 99.5,
trend: 'stable',
trendValue: 2
},
{
agentType: 'quality-judge',
name: 'QualityJudge',
color: '#f59e0b',
sessions: 89,
messagesProcessed: 8934,
avgResponseTime: 89,
errorRate: 0.1,
successRate: 99.9,
trend: 'up',
trendValue: 8
},
{
agentType: 'alert-agent',
name: 'AlertAgent',
color: '#ef4444',
sessions: 12,
messagesProcessed: 892,
avgResponseTime: 45,
errorRate: 0.0,
successRate: 100,
trend: 'stable',
trendValue: 0
},
{
agentType: 'orchestrator',
name: 'Orchestrator',
color: '#8b5cf6',
sessions: 234,
messagesProcessed: 15420,
avgResponseTime: 12,
errorRate: 0.2,
successRate: 99.8,
trend: 'up',
trendValue: 15
}
]
const mockDailyStats: DailyStats[] = [
{ date: '2026-01-28', sessions: 420, messages: 12500, errors: 15, avgLatency: 156 },
{ date: '2026-01-29', sessions: 445, messages: 13200, errors: 12, avgLatency: 148 },
{ date: '2026-01-30', sessions: 398, messages: 11800, errors: 18, avgLatency: 162 },
{ date: '2026-01-31', sessions: 512, messages: 15600, errors: 10, avgLatency: 145 },
{ date: '2026-02-01', sessions: 489, messages: 14200, errors: 8, avgLatency: 139 },
{ date: '2026-02-02', sessions: 534, messages: 16100, errors: 11, avgLatency: 142 },
{ date: '2026-02-03', sessions: 478, messages: 14800, errors: 9, avgLatency: 151 }
]
const mockHourlyLatency: TimeSeriesData[] = Array.from({ length: 24 }, (_, i) => ({
timestamp: `${i.toString().padStart(2, '0')}:00`,
value: Math.floor(100 + Math.random() * 100)
}))
function getAgentIcon(agentType: string) {
switch (agentType) {
case 'tutor-agent': return <Brain className="w-4 h-4" />
case 'grader-agent': return <Bot className="w-4 h-4" />
case 'quality-judge': return <Settings className="w-4 h-4" />
case 'alert-agent': return <AlertTriangle className="w-4 h-4" />
case 'orchestrator': return <MessageSquare className="w-4 h-4" />
default: return <Bot className="w-4 h-4" />
}
}
// Simple bar chart component
function BarChart({ data, color, maxValue }: { data: number[], color: string, maxValue: number }) {
return (
<div className="flex items-end gap-1 h-20">
{data.map((value, i) => (
<div
key={i}
className="flex-1 rounded-t transition-all hover:opacity-80"
style={{
height: `${(value / maxValue) * 100}%`,
backgroundColor: color,
minHeight: '4px'
}}
/>
))}
</div>
)
}
// Simple line chart visualization
function SparkLine({ data, color }: { data: number[], color: string }) {
const max = Math.max(...data)
const min = Math.min(...data)
const range = max - min || 1
const points = data.map((value, i) => {
const x = (i / (data.length - 1)) * 100
const y = 100 - ((value - min) / range) * 100
return `${x},${y}`
}).join(' ')
return (
<svg viewBox="0 0 100 100" className="w-full h-12" preserveAspectRatio="none">
<polyline
fill="none"
stroke={color}
strokeWidth="2"
points={points}
/>
</svg>
)
}
export default function StatisticsPage() {
const [loading, setLoading] = useState(false)
const [timeRange, setTimeRange] = useState<'24h' | '7d' | '30d'>('7d')
const [lastRefresh, setLastRefresh] = useState(new Date())
const refreshData = async () => {
setLoading(true)
await new Promise(resolve => setTimeout(resolve, 500))
setLastRefresh(new Date())
setLoading(false)
}
// Calculate totals
const totals = {
sessions: mockAgentMetrics.reduce((sum, m) => sum + m.sessions, 0),
messages: mockAgentMetrics.reduce((sum, m) => sum + m.messagesProcessed, 0),
avgLatency: Math.round(mockAgentMetrics.reduce((sum, m) => sum + m.avgResponseTime, 0) / mockAgentMetrics.length),
avgErrorRate: (mockAgentMetrics.reduce((sum, m) => sum + m.errorRate, 0) / mockAgentMetrics.length).toFixed(2)
}
// Calculate week stats
const weekTotals = {
sessions: mockDailyStats.reduce((sum, d) => sum + d.sessions, 0),
messages: mockDailyStats.reduce((sum, d) => sum + d.messages, 0),
errors: mockDailyStats.reduce((sum, d) => sum + d.errors, 0),
avgLatency: Math.round(mockDailyStats.reduce((sum, d) => sum + d.avgLatency, 0) / mockDailyStats.length)
}
return (
<div className="p-6 max-w-7xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link
href="/ai/agents"
className="flex items-center gap-2 text-gray-500 hover:text-gray-700 mb-4"
>
<ArrowLeft className="w-4 h-4" />
Zurueck zur Agent-Verwaltung
</Link>
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 flex items-center gap-3">
<div className="p-2 bg-green-100 rounded-lg">
<BarChart3 className="w-6 h-6 text-green-600" />
</div>
Agent Statistiken
</h1>
<p className="text-gray-500 mt-1">
Performance-Metriken und Trends des Multi-Agent-Systems
</p>
</div>
<div className="flex items-center gap-3">
<select
value={timeRange}
onChange={(e) => setTimeRange(e.target.value as '24h' | '7d' | '30d')}
className="px-4 py-2 border border-gray-200 rounded-lg focus:outline-none focus:ring-2 focus:ring-green-500"
>
<option value="24h">Letzte 24 Stunden</option>
<option value="7d">Letzte 7 Tage</option>
<option value="30d">Letzte 30 Tage</option>
</select>
<button
onClick={refreshData}
disabled={loading}
className="flex items-center gap-2 px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors disabled:opacity-50"
>
<RefreshCw className={`w-4 h-4 ${loading ? 'animate-spin' : ''}`} />
Aktualisieren
</button>
</div>
</div>
</div>
{/* Overview Stats */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-8">
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">Sessions (7d)</span>
<Activity className="w-4 h-4 text-gray-400" />
</div>
<div className="text-3xl font-bold text-gray-900">{weekTotals.sessions.toLocaleString()}</div>
<div className="flex items-center gap-1 mt-1 text-sm text-green-600">
<TrendingUp className="w-3.5 h-3.5" />
<span>+12% vs. Vorwoche</span>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">Messages (7d)</span>
<MessageSquare className="w-4 h-4 text-gray-400" />
</div>
<div className="text-3xl font-bold text-gray-900">{weekTotals.messages.toLocaleString()}</div>
<div className="flex items-center gap-1 mt-1 text-sm text-green-600">
<TrendingUp className="w-3.5 h-3.5" />
<span>+8% vs. Vorwoche</span>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">Avg. Latenz</span>
<Clock className="w-4 h-4 text-gray-400" />
</div>
<div className="text-3xl font-bold text-gray-900">{weekTotals.avgLatency}ms</div>
<div className="flex items-center gap-1 mt-1 text-sm text-green-600">
<TrendingDown className="w-3.5 h-3.5" />
<span>-5% (verbessert)</span>
</div>
</div>
<div className="bg-white border border-gray-200 rounded-xl p-5">
<div className="flex items-center justify-between mb-2">
<span className="text-sm text-gray-500">Fehler (7d)</span>
<AlertTriangle className="w-4 h-4 text-gray-400" />
</div>
<div className="text-3xl font-bold text-gray-900">{weekTotals.errors}</div>
<div className="flex items-center gap-1 mt-1 text-sm text-amber-600">
<TrendingUp className="w-3.5 h-3.5" />
<span>+3 vs. Vorwoche</span>
</div>
</div>
</div>
{/* Charts Row */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Sessions per Day */}
<div className="bg-white border border-gray-200 rounded-xl p-5">
<h3 className="font-semibold text-gray-900 mb-4">Sessions pro Tag</h3>
<div className="space-y-3">
<BarChart
data={mockDailyStats.map(d => d.sessions)}
color="#3b82f6"
maxValue={Math.max(...mockDailyStats.map(d => d.sessions)) * 1.1}
/>
<div className="flex justify-between text-xs text-gray-500">
{mockDailyStats.map(d => (
<span key={d.date}>{new Date(d.date).toLocaleDateString('de-DE', { weekday: 'short' })}</span>
))}
</div>
</div>
</div>
{/* Messages per Day */}
<div className="bg-white border border-gray-200 rounded-xl p-5">
<h3 className="font-semibold text-gray-900 mb-4">Messages pro Tag</h3>
<div className="space-y-3">
<BarChart
data={mockDailyStats.map(d => d.messages)}
color="#10b981"
maxValue={Math.max(...mockDailyStats.map(d => d.messages)) * 1.1}
/>
<div className="flex justify-between text-xs text-gray-500">
{mockDailyStats.map(d => (
<span key={d.date}>{new Date(d.date).toLocaleDateString('de-DE', { weekday: 'short' })}</span>
))}
</div>
</div>
</div>
</div>
{/* Latency Chart */}
<div className="bg-white border border-gray-200 rounded-xl p-5 mb-8">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-gray-900">Latenz (24h)</h3>
<div className="flex items-center gap-2 text-sm text-gray-500">
<Clock className="w-4 h-4" />
Durchschnitt: {totals.avgLatency}ms
</div>
</div>
<SparkLine
data={mockHourlyLatency.map(d => d.value)}
color="#8b5cf6"
/>
<div className="flex justify-between text-xs text-gray-400 mt-2">
<span>00:00</span>
<span>06:00</span>
<span>12:00</span>
<span>18:00</span>
<span>24:00</span>
</div>
</div>
{/* Agent Performance Table */}
<div className="bg-white border border-gray-200 rounded-xl overflow-hidden mb-8">
<div className="px-5 py-4 border-b border-gray-200">
<h3 className="font-semibold text-gray-900">Agent Performance</h3>
</div>
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-5 py-3 text-left text-xs font-medium text-gray-500 uppercase">Agent</th>
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Sessions</th>
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Messages</th>
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Avg. Response</th>
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Success Rate</th>
<th className="px-5 py-3 text-right text-xs font-medium text-gray-500 uppercase">Error Rate</th>
<th className="px-5 py-3 text-center text-xs font-medium text-gray-500 uppercase">Trend</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
{mockAgentMetrics.map(metric => (
<tr key={metric.agentType} className="hover:bg-gray-50">
<td className="px-5 py-4">
<div className="flex items-center gap-3">
<div
className="p-2 rounded-lg"
style={{ backgroundColor: `${metric.color}20` }}
>
<span style={{ color: metric.color }}>{getAgentIcon(metric.agentType)}</span>
</div>
<div>
<div className="font-medium text-gray-900">{metric.name}</div>
<div className="text-xs text-gray-500">{metric.agentType}</div>
</div>
</div>
</td>
<td className="px-5 py-4 text-right">
<span className="font-medium text-gray-900">{metric.sessions}</span>
</td>
<td className="px-5 py-4 text-right">
<span className="font-medium text-gray-900">{metric.messagesProcessed.toLocaleString()}</span>
</td>
<td className="px-5 py-4 text-right">
<span className="text-gray-900">{metric.avgResponseTime}ms</span>
</td>
<td className="px-5 py-4 text-right">
<span className="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium bg-green-100 text-green-700">
{metric.successRate}%
</span>
</td>
<td className="px-5 py-4 text-right">
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs font-medium ${
metric.errorRate > 0.5 ? 'bg-red-100 text-red-700' :
metric.errorRate > 0 ? 'bg-amber-100 text-amber-700' :
'bg-green-100 text-green-700'
}`}>
{metric.errorRate}%
</span>
</td>
<td className="px-5 py-4 text-center">
{metric.trend === 'up' && (
<span className="inline-flex items-center gap-1 text-green-600 text-sm">
<TrendingUp className="w-4 h-4" />
+{metric.trendValue}%
</span>
)}
{metric.trend === 'down' && (
<span className="inline-flex items-center gap-1 text-red-600 text-sm">
<TrendingDown className="w-4 h-4" />
-{metric.trendValue}%
</span>
)}
{metric.trend === 'stable' && (
<span className="inline-flex items-center gap-1 text-gray-500 text-sm">
<span className="w-4 h-0.5 bg-gray-400 rounded" />
{metric.trendValue}%
</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* Error Distribution */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Error by Agent */}
<div className="bg-white border border-gray-200 rounded-xl p-5">
<h3 className="font-semibold text-gray-900 mb-4">Fehlerverteilung nach Agent</h3>
<div className="space-y-3">
{mockAgentMetrics.filter(m => m.errorRate > 0).map(metric => (
<div key={metric.agentType} className="flex items-center gap-3">
<div className="w-24 text-sm text-gray-600 truncate">{metric.name}</div>
<div className="flex-1 h-4 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${metric.errorRate * 20}%`,
backgroundColor: metric.color
}}
/>
</div>
<div className="w-12 text-right text-sm text-gray-600">{metric.errorRate}%</div>
</div>
))}
</div>
</div>
{/* Message Distribution */}
<div className="bg-white border border-gray-200 rounded-xl p-5">
<h3 className="font-semibold text-gray-900 mb-4">Message-Verteilung nach Agent</h3>
<div className="space-y-3">
{mockAgentMetrics.map(metric => {
const percentage = (metric.messagesProcessed / totals.messages) * 100
return (
<div key={metric.agentType} className="flex items-center gap-3">
<div className="w-24 text-sm text-gray-600 truncate">{metric.name}</div>
<div className="flex-1 h-4 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full rounded-full"
style={{
width: `${percentage}%`,
backgroundColor: metric.color
}}
/>
</div>
<div className="w-12 text-right text-sm text-gray-600">{percentage.toFixed(1)}%</div>
</div>
)
})}
</div>
</div>
</div>
{/* Export Button */}
<div className="mt-8 flex justify-end">
<button className="flex items-center gap-2 px-4 py-2 border border-gray-200 rounded-lg hover:bg-gray-50 text-gray-600 hover:text-gray-900 transition-colors">
<Download className="w-4 h-4" />
Statistiken exportieren (CSV)
</button>
</div>
</div>
)
}

View File

@@ -0,0 +1,396 @@
'use client'
/**
* GPU Infrastructure Admin Page
*
* vast.ai GPU Management for LLM Processing
* Part of KI-Werkzeuge
*/
import { useEffect, useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar'
interface VastStatus {
instance_id: number | null
status: string
gpu_name: string | null
dph_total: number | null
endpoint_base_url: string | null
last_activity: string | null
auto_shutdown_in_minutes: number | null
total_runtime_hours: number | null
total_cost_usd: number | null
account_credit: number | null
account_total_spend: number | null
session_runtime_minutes: number | null
session_cost_usd: number | null
message: string | null
error?: string
}
export default function GPUInfrastructurePage() {
const [status, setStatus] = useState<VastStatus | null>(null)
const [loading, setLoading] = useState(true)
const [actionLoading, setActionLoading] = useState<string | null>(null)
const [error, setError] = useState<string | null>(null)
const [message, setMessage] = useState<string | null>(null)
const API_PROXY = '/api/admin/gpu'
const fetchStatus = useCallback(async () => {
setLoading(true)
setError(null)
try {
const response = await fetch(API_PROXY)
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || `HTTP ${response.status}`)
}
setStatus(data)
} catch (err) {
setError(err instanceof Error ? err.message : 'Verbindungsfehler')
setStatus({
instance_id: null,
status: 'error',
gpu_name: null,
dph_total: null,
endpoint_base_url: null,
last_activity: null,
auto_shutdown_in_minutes: null,
total_runtime_hours: null,
total_cost_usd: null,
account_credit: null,
account_total_spend: null,
session_runtime_minutes: null,
session_cost_usd: null,
message: 'Verbindung fehlgeschlagen'
})
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchStatus()
}, [fetchStatus])
useEffect(() => {
const interval = setInterval(fetchStatus, 30000)
return () => clearInterval(interval)
}, [fetchStatus])
const powerOn = async () => {
setActionLoading('on')
setError(null)
setMessage(null)
try {
const response = await fetch(API_PROXY, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'on' }),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || data.detail || 'Aktion fehlgeschlagen')
}
setMessage('Start angefordert')
setTimeout(fetchStatus, 3000)
setTimeout(fetchStatus, 10000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Starten')
fetchStatus()
} finally {
setActionLoading(null)
}
}
const powerOff = async () => {
setActionLoading('off')
setError(null)
setMessage(null)
try {
const response = await fetch(API_PROXY, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ action: 'off' }),
})
const data = await response.json()
if (!response.ok) {
throw new Error(data.error || data.detail || 'Aktion fehlgeschlagen')
}
setMessage('Stop angefordert')
setTimeout(fetchStatus, 3000)
setTimeout(fetchStatus, 10000)
} catch (err) {
setError(err instanceof Error ? err.message : 'Fehler beim Stoppen')
fetchStatus()
} finally {
setActionLoading(null)
}
}
const getStatusBadge = (s: string) => {
const baseClasses = 'px-3 py-1 rounded-full text-sm font-semibold uppercase'
switch (s) {
case 'running':
return `${baseClasses} bg-green-100 text-green-800`
case 'stopped':
case 'exited':
return `${baseClasses} bg-red-100 text-red-800`
case 'loading':
case 'scheduling':
case 'creating':
case 'starting...':
case 'stopping...':
return `${baseClasses} bg-yellow-100 text-yellow-800`
default:
return `${baseClasses} bg-slate-100 text-slate-600`
}
}
const getCreditColor = (credit: number | null) => {
if (credit === null) return 'text-slate-500'
if (credit < 5) return 'text-red-600'
if (credit < 15) return 'text-yellow-600'
return 'text-green-600'
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="GPU Infrastruktur"
purpose="Verwalten Sie die vast.ai GPU-Instanzen fuer LLM-Verarbeitung und OCR. Starten/Stoppen Sie GPUs bei Bedarf und ueberwachen Sie Kosten in Echtzeit."
audience={['DevOps', 'Entwickler', 'System-Admins']}
architecture={{
services: ['vast.ai API', 'Ollama', 'VLLM'],
databases: ['PostgreSQL (Logs)'],
}}
relatedPages={[
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider testen' },
{ name: 'Test Quality (BQAS)', href: '/ai/test-quality', description: 'Golden Suite & Tests' },
{ name: 'Magic Help', href: '/ai/magic-help', description: 'TrOCR Testing' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* KI-Werkzeuge Sidebar */}
<AIToolsSidebarResponsive currentTool="gpu" />
{/* Status Cards */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-6">
<div>
<div className="text-sm text-slate-500 mb-2">Status</div>
{loading ? (
<span className="px-3 py-1 rounded-full text-sm font-semibold bg-slate-100 text-slate-600">
Laden...
</span>
) : (
<span className={getStatusBadge(
actionLoading === 'on' ? 'starting...' :
actionLoading === 'off' ? 'stopping...' :
status?.status || 'unknown'
)}>
{actionLoading === 'on' ? 'starting...' :
actionLoading === 'off' ? 'stopping...' :
status?.status || 'unbekannt'}
</span>
)}
</div>
<div>
<div className="text-sm text-slate-500 mb-2">GPU</div>
<div className="font-semibold text-slate-900">
{status?.gpu_name || '-'}
</div>
</div>
<div>
<div className="text-sm text-slate-500 mb-2">Kosten/h</div>
<div className="font-semibold text-slate-900">
{status?.dph_total ? `$${status.dph_total.toFixed(3)}` : '-'}
</div>
</div>
<div>
<div className="text-sm text-slate-500 mb-2">Auto-Stop</div>
<div className="font-semibold text-slate-900">
{status && status.auto_shutdown_in_minutes !== null
? `${status.auto_shutdown_in_minutes} min`
: '-'}
</div>
</div>
<div>
<div className="text-sm text-slate-500 mb-2">Budget</div>
<div className={`font-bold text-lg ${getCreditColor(status?.account_credit ?? null)}`}>
{status && status.account_credit !== null
? `$${status.account_credit.toFixed(2)}`
: '-'}
</div>
</div>
<div>
<div className="text-sm text-slate-500 mb-2">Session</div>
<div className="font-semibold text-slate-900">
{status && status.session_runtime_minutes !== null && status.session_cost_usd !== null
? `${Math.round(status.session_runtime_minutes)} min / $${status.session_cost_usd.toFixed(3)}`
: '-'}
</div>
</div>
</div>
{/* Buttons */}
<div className="flex items-center gap-4 mt-6 pt-6 border-t border-slate-200">
<button
onClick={powerOn}
disabled={actionLoading !== null || status?.status === 'running'}
className="px-6 py-2 bg-orange-600 text-white rounded-lg font-medium hover:bg-orange-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Starten
</button>
<button
onClick={powerOff}
disabled={actionLoading !== null || status?.status !== 'running'}
className="px-6 py-2 bg-red-600 text-white rounded-lg font-medium hover:bg-red-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
Stoppen
</button>
<button
onClick={fetchStatus}
disabled={loading}
className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg font-medium hover:bg-slate-50 disabled:opacity-50 transition-colors"
>
{loading ? 'Aktualisiere...' : 'Aktualisieren'}
</button>
{message && (
<span className="ml-4 text-sm text-green-600 font-medium">{message}</span>
)}
{error && (
<span className="ml-4 text-sm text-red-600 font-medium">{error}</span>
)}
</div>
</div>
{/* Extended Stats */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Kosten-Uebersicht</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-slate-600">Session Laufzeit</span>
<span className="font-semibold">
{status && status.session_runtime_minutes !== null
? `${Math.round(status.session_runtime_minutes)} Minuten`
: '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">Session Kosten</span>
<span className="font-semibold">
{status && status.session_cost_usd !== null
? `$${status.session_cost_usd.toFixed(4)}`
: '-'}
</span>
</div>
<div className="flex justify-between items-center pt-4 border-t border-slate-100">
<span className="text-slate-600">Gesamtlaufzeit</span>
<span className="font-semibold">
{status && status.total_runtime_hours !== null
? `${status.total_runtime_hours.toFixed(1)} Stunden`
: '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">Gesamtkosten</span>
<span className="font-semibold">
{status && status.total_cost_usd !== null
? `$${status.total_cost_usd.toFixed(2)}`
: '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">vast.ai Ausgaben</span>
<span className="font-semibold">
{status && status.account_total_spend !== null
? `$${status.account_total_spend.toFixed(2)}`
: '-'}
</span>
</div>
</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Instanz-Details</h3>
<div className="space-y-4">
<div className="flex justify-between items-center">
<span className="text-slate-600">Instanz ID</span>
<span className="font-mono text-sm">
{status?.instance_id || '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">GPU</span>
<span className="font-semibold">
{status?.gpu_name || '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">Stundensatz</span>
<span className="font-semibold">
{status?.dph_total ? `$${status.dph_total.toFixed(4)}/h` : '-'}
</span>
</div>
<div className="flex justify-between items-center">
<span className="text-slate-600">Letzte Aktivitaet</span>
<span className="text-sm">
{status?.last_activity
? new Date(status.last_activity).toLocaleString('de-DE')
: '-'}
</span>
</div>
{status?.endpoint_base_url && status.status === 'running' && (
<div className="pt-4 border-t border-slate-100">
<div className="text-slate-600 text-sm mb-1">Endpoint</div>
<code className="text-xs bg-slate-100 px-2 py-1 rounded block overflow-x-auto">
{status.endpoint_base_url}
</code>
</div>
)}
</div>
</div>
</div>
{/* Info */}
<div className="bg-violet-50 border border-violet-200 rounded-xl p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-violet-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-violet-900">Auto-Shutdown</h4>
<p className="text-sm text-violet-800 mt-1">
Die GPU-Instanz wird automatisch gestoppt, wenn sie laengere Zeit inaktiv ist.
Der Status wird alle 30 Sekunden automatisch aktualisiert.
</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,503 @@
'use client'
/**
* LLM Comparison Tool
*
* Vergleicht Antworten von verschiedenen LLM-Providern:
* - OpenAI/ChatGPT
* - Claude
* - Self-hosted + Tavily
* - Self-hosted + EduSearch
*/
import { useState, useEffect, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar'
interface LLMResponse {
provider: string
model: string
response: string
latency_ms: number
tokens_used?: number
search_results?: Array<{
title: string
url: string
content: string
score?: number
}>
error?: string
timestamp: string
}
interface ComparisonResult {
comparison_id: string
prompt: string
system_prompt?: string
responses: LLMResponse[]
created_at: string
}
const providerColors: Record<string, { bg: string; border: string; text: string }> = {
openai: { bg: 'bg-emerald-50', border: 'border-emerald-300', text: 'text-emerald-700' },
claude: { bg: 'bg-orange-50', border: 'border-orange-300', text: 'text-orange-700' },
selfhosted_tavily: { bg: 'bg-blue-50', border: 'border-blue-300', text: 'text-blue-700' },
selfhosted_edusearch: { bg: 'bg-purple-50', border: 'border-purple-300', text: 'text-purple-700' },
}
const providerLabels: Record<string, string> = {
openai: 'OpenAI GPT-4o-mini',
claude: 'Claude 3.5 Sonnet',
selfhosted_tavily: 'Self-hosted + Tavily',
selfhosted_edusearch: 'Self-hosted + EduSearch',
}
export default function LLMComparePage() {
// State
const [prompt, setPrompt] = useState('')
const [systemPrompt, setSystemPrompt] = useState('Du bist ein hilfreicher Assistent fuer Lehrkraefte in Deutschland.')
// Provider toggles
const [enableOpenAI, setEnableOpenAI] = useState(true)
const [enableClaude, setEnableClaude] = useState(true)
const [enableTavily, setEnableTavily] = useState(true)
const [enableEduSearch, setEnableEduSearch] = useState(true)
// Parameters
const [model, setModel] = useState('llama3.2:3b')
const [temperature, setTemperature] = useState(0.7)
const [maxTokens, setMaxTokens] = useState(2048)
// Results
const [isLoading, setIsLoading] = useState(false)
const [result, setResult] = useState<ComparisonResult | null>(null)
const [history, setHistory] = useState<ComparisonResult[]>([])
const [error, setError] = useState<string | null>(null)
// UI State
const [showSettings, setShowSettings] = useState(false)
const [showHistory, setShowHistory] = useState(false)
// API Base URL
const API_URL = process.env.NEXT_PUBLIC_LLM_GATEWAY_URL || 'http://localhost:8082'
const API_KEY = process.env.NEXT_PUBLIC_LLM_API_KEY || 'dev-key'
// Load history
const loadHistory = useCallback(async () => {
try {
const response = await fetch(`${API_URL}/v1/comparison/history?limit=20`, {
headers: { Authorization: `Bearer ${API_KEY}` },
})
if (response.ok) {
const data = await response.json()
setHistory(data.comparisons || [])
}
} catch (e) {
console.error('Failed to load history:', e)
}
}, [API_URL, API_KEY])
useEffect(() => {
loadHistory()
}, [loadHistory])
const runComparison = async () => {
if (!prompt.trim()) {
setError('Bitte geben Sie einen Prompt ein')
return
}
setIsLoading(true)
setError(null)
setResult(null)
try {
const response = await fetch(`${API_URL}/v1/comparison/run`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${API_KEY}`,
},
body: JSON.stringify({
prompt,
system_prompt: systemPrompt || undefined,
enable_openai: enableOpenAI,
enable_claude: enableClaude,
enable_selfhosted_tavily: enableTavily,
enable_selfhosted_edusearch: enableEduSearch,
selfhosted_model: model,
temperature,
max_tokens: maxTokens,
}),
})
if (!response.ok) {
throw new Error(`API Error: ${response.status}`)
}
const data = await response.json()
setResult(data)
loadHistory()
} catch (e) {
setError(e instanceof Error ? e.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const ResponseCard = ({ response }: { response: LLMResponse }) => {
const colors = providerColors[response.provider] || {
bg: 'bg-slate-50',
border: 'border-slate-300',
text: 'text-slate-700',
}
const label = providerLabels[response.provider] || response.provider
return (
<div className={`rounded-xl border-2 ${colors.border} ${colors.bg} overflow-hidden`}>
<div className={`px-4 py-3 border-b ${colors.border} flex items-center justify-between`}>
<div>
<h3 className={`font-semibold ${colors.text}`}>{label}</h3>
<p className="text-xs text-slate-500">{response.model}</p>
</div>
<div className="text-right text-xs text-slate-500">
<div>{response.latency_ms}ms</div>
{response.tokens_used && <div>{response.tokens_used} tokens</div>}
</div>
</div>
<div className="p-4">
{response.error ? (
<div className="text-red-600 text-sm">
<strong>Fehler:</strong> {response.error}
</div>
) : (
<pre className="whitespace-pre-wrap text-sm text-slate-700 font-sans">
{response.response}
</pre>
)}
</div>
{response.search_results && response.search_results.length > 0 && (
<div className="px-4 pb-4">
<details className="text-xs">
<summary className="cursor-pointer text-slate-500 hover:text-slate-700">
{response.search_results.length} Suchergebnisse anzeigen
</summary>
<ul className="mt-2 space-y-2">
{response.search_results.map((sr, idx) => (
<li key={idx} className="bg-white rounded p-2 border border-slate-200">
<a
href={sr.url}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline font-medium"
>
{sr.title || 'Untitled'}
</a>
<p className="text-slate-500 truncate">{sr.content}</p>
</li>
))}
</ul>
</details>
</div>
)}
</div>
)
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="LLM Vergleich"
purpose="Vergleichen Sie Antworten verschiedener KI-Provider (OpenAI, Claude, Self-hosted) fuer Qualitaetssicherung. Optimieren Sie Parameter und System Prompts fuer beste Ergebnisse. Standalone-Werkzeug ohne direkten Datenfluss zur KI-Pipeline."
audience={['Entwickler', 'Data Scientists', 'QA']}
architecture={{
services: ['llm-gateway (Python)', 'Ollama', 'OpenAI API', 'Claude API'],
databases: ['PostgreSQL (History)', 'Qdrant (RAG)'],
}}
relatedPages={[
{ name: 'Test Quality (BQAS)', href: '/ai/test-quality', description: 'Golden Suite & Synthetic Tests' },
{ name: 'GPU Infrastruktur', href: '/ai/gpu', description: 'GPU-Ressourcen verwalten' },
{ name: 'Agent Management', href: '/ai/agents', description: 'Multi-Agent System' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* KI-Werkzeuge Sidebar */}
<AIToolsSidebarResponsive currentTool="llm-compare" />
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left Column: Input & Settings */}
<div className="lg:col-span-1 space-y-4">
{/* Prompt Input */}
<div className="bg-white rounded-xl border border-slate-200 p-4">
<h2 className="font-semibold text-slate-900 mb-3">Prompt</h2>
{/* System Prompt */}
<div className="mb-3">
<label className="block text-sm text-slate-600 mb-1">System Prompt</label>
<textarea
value={systemPrompt}
onChange={(e) => setSystemPrompt(e.target.value)}
rows={3}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm resize-none"
placeholder="System Prompt (optional)"
/>
</div>
{/* User Prompt */}
<div className="mb-3">
<label className="block text-sm text-slate-600 mb-1">User Prompt</label>
<textarea
value={prompt}
onChange={(e) => setPrompt(e.target.value)}
rows={4}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm resize-none"
placeholder="z.B.: Erstelle ein Arbeitsblatt zum Thema Bruchrechnung fuer Klasse 6..."
/>
</div>
{/* Provider Toggles */}
<div className="mb-4">
<label className="block text-sm text-slate-600 mb-2">Provider</label>
<div className="grid grid-cols-2 gap-2">
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enableOpenAI}
onChange={(e) => setEnableOpenAI(e.target.checked)}
className="rounded"
/>
OpenAI
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enableClaude}
onChange={(e) => setEnableClaude(e.target.checked)}
className="rounded"
/>
Claude
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enableTavily}
onChange={(e) => setEnableTavily(e.target.checked)}
className="rounded"
/>
Self + Tavily
</label>
<label className="flex items-center gap-2 text-sm">
<input
type="checkbox"
checked={enableEduSearch}
onChange={(e) => setEnableEduSearch(e.target.checked)}
className="rounded"
/>
Self + EduSearch
</label>
</div>
</div>
{/* Run Button */}
<button
onClick={runComparison}
disabled={isLoading || !prompt.trim()}
className="w-full py-3 bg-teal-600 text-white rounded-lg font-medium hover:bg-teal-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isLoading ? (
<span className="flex items-center justify-center gap-2">
<svg className="animate-spin w-5 h-5" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>
Vergleiche...
</span>
) : (
'Vergleich starten'
)}
</button>
{error && (
<div className="mt-3 p-3 bg-red-50 border border-red-200 rounded-lg text-red-700 text-sm">
{error}
</div>
)}
</div>
{/* Settings Panel */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<button
onClick={() => setShowSettings(!showSettings)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
>
<span className="font-semibold text-slate-900">Parameter</span>
<svg
className={`w-5 h-5 transition-transform ${showSettings ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showSettings && (
<div className="p-4 border-t border-slate-200 space-y-4">
<div>
<label className="block text-sm text-slate-600 mb-1">Self-hosted Modell</label>
<select
value={model}
onChange={(e) => setModel(e.target.value)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
>
<option value="llama3.2:3b">Llama 3.2 3B</option>
<option value="llama3.1:8b">Llama 3.1 8B</option>
<option value="mistral:7b">Mistral 7B</option>
<option value="qwen2.5:7b">Qwen 2.5 7B</option>
</select>
</div>
<div>
<label className="block text-sm text-slate-600 mb-1">
Temperature: {temperature.toFixed(2)}
</label>
<input
type="range"
min="0"
max="2"
step="0.1"
value={temperature}
onChange={(e) => setTemperature(parseFloat(e.target.value))}
className="w-full"
/>
</div>
<div>
<label className="block text-sm text-slate-600 mb-1">Max Tokens: {maxTokens}</label>
<input
type="range"
min="256"
max="4096"
step="256"
value={maxTokens}
onChange={(e) => setMaxTokens(parseInt(e.target.value))}
className="w-full"
/>
</div>
</div>
)}
</div>
{/* History Panel */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<button
onClick={() => setShowHistory(!showHistory)}
className="w-full px-4 py-3 flex items-center justify-between hover:bg-slate-50"
>
<span className="font-semibold text-slate-900">Verlauf ({history.length})</span>
<svg
className={`w-5 h-5 transition-transform ${showHistory ? 'rotate-180' : ''}`}
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 9l-7 7-7-7" />
</svg>
</button>
{showHistory && history.length > 0 && (
<div className="border-t border-slate-200 max-h-64 overflow-y-auto">
{history.map((h) => (
<button
key={h.comparison_id}
onClick={() => {
setResult(h)
setPrompt(h.prompt)
if (h.system_prompt) setSystemPrompt(h.system_prompt)
}}
className="w-full px-4 py-2 text-left hover:bg-slate-50 border-b border-slate-100 last:border-0"
>
<div className="text-sm text-slate-700 truncate">{h.prompt}</div>
<div className="text-xs text-slate-400">
{new Date(h.created_at).toLocaleString('de-DE')}
</div>
</button>
))}
</div>
)}
</div>
</div>
{/* Right Column: Results */}
<div className="lg:col-span-2">
{result ? (
<div className="space-y-4">
<div className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex items-center justify-between">
<div>
<h2 className="font-semibold text-slate-900">Ergebnisse</h2>
<p className="text-sm text-slate-500">ID: {result.comparison_id}</p>
</div>
<div className="text-sm text-slate-500">
{new Date(result.created_at).toLocaleString('de-DE')}
</div>
</div>
<div className="mt-2 p-3 bg-slate-50 rounded-lg">
<p className="text-sm text-slate-700">{result.prompt}</p>
</div>
</div>
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
{result.responses.map((response, idx) => (
<ResponseCard key={`${response.provider}-${idx}`} response={response} />
))}
</div>
</div>
) : (
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
<svg
className="w-16 h-16 mx-auto text-slate-300 mb-4"
fill="none"
stroke="currentColor"
viewBox="0 0 24 24"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={1.5}
d="M9 3v2m6-2v2M9 19v2m6-2v2M5 9H3m2 6H3m18-6h-2m2 6h-2M7 19h10a2 2 0 002-2V7a2 2 0 00-2-2H7a2 2 0 00-2 2v10a2 2 0 002 2zM9 9h6v6H9V9z"
/>
</svg>
<h3 className="text-lg font-medium text-slate-700 mb-2">LLM-Vergleich starten</h3>
<p className="text-slate-500 max-w-md mx-auto">
Geben Sie einen Prompt ein und klicken Sie auf &quot;Vergleich starten&quot;, um
die Antworten verschiedener LLM-Provider zu vergleichen.
</p>
</div>
)}
</div>
</div>
{/* Info Box */}
<div className="mt-8 bg-teal-50 border border-teal-200 rounded-xl p-6">
<div className="flex items-start gap-4">
<svg className="w-6 h-6 text-teal-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h3 className="font-semibold text-teal-900">Qualitaetssicherung</h3>
<p className="text-sm text-teal-800 mt-1">
Dieses Tool dient zur Qualitaetssicherung der KI-Antworten. Vergleichen Sie verschiedene Provider,
um die optimalen Parameter und System Prompts zu finden. Die Ergebnisse werden fuer Audits gespeichert.
</p>
</div>
</div>
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,987 @@
'use client'
/**
* OCR Labeling Admin Page
*
* Labeling interface for handwriting training data collection.
* DSGVO-konform: Alle Verarbeitung lokal auf Mac Mini (Ollama).
*
* Teil der KI-Daten-Pipeline:
* OCR-Labeling → RAG Pipeline → Daten & RAG
*/
import { useState, useEffect, useCallback, useRef } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
import { AIModuleSidebarResponsive } from '@/components/ai/AIModuleSidebar'
import type {
OCRSession,
OCRItem,
OCRStats,
TrainingSample,
CreateSessionRequest,
OCRModel,
} from './types'
// API Base URL for klausur-service
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
// Tab definitions
type TabId = 'labeling' | 'sessions' | 'upload' | 'stats' | 'export'
const tabs: { id: TabId; name: string; icon: JSX.Element }[] = [
{
id: 'labeling',
name: 'Labeling',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
),
},
{
id: 'sessions',
name: 'Sessions',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" />
</svg>
),
},
{
id: 'upload',
name: 'Upload',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
</svg>
),
},
{
id: 'stats',
name: 'Statistiken',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" />
</svg>
),
},
{
id: 'export',
name: 'Export',
icon: (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
),
},
]
export default function OCRLabelingPage() {
const [activeTab, setActiveTab] = useState<TabId>('labeling')
const [sessions, setSessions] = useState<OCRSession[]>([])
const [selectedSession, setSelectedSession] = useState<string | null>(null)
const [queue, setQueue] = useState<OCRItem[]>([])
const [currentItem, setCurrentItem] = useState<OCRItem | null>(null)
const [currentIndex, setCurrentIndex] = useState(0)
const [stats, setStats] = useState<OCRStats | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [correctedText, setCorrectedText] = useState('')
const [labelStartTime, setLabelStartTime] = useState<number | null>(null)
// Fetch sessions
const fetchSessions = useCallback(async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/sessions`)
if (res.ok) {
const data = await res.json()
setSessions(data)
}
} catch (err) {
console.error('Failed to fetch sessions:', err)
}
}, [])
// Fetch queue
const fetchQueue = useCallback(async () => {
try {
const url = selectedSession
? `${API_BASE}/api/v1/ocr-label/queue?session_id=${selectedSession}&limit=20`
: `${API_BASE}/api/v1/ocr-label/queue?limit=20`
const res = await fetch(url)
if (res.ok) {
const data = await res.json()
setQueue(data)
if (data.length > 0 && !currentItem) {
setCurrentItem(data[0])
setCurrentIndex(0)
setCorrectedText(data[0].ocr_text || '')
setLabelStartTime(Date.now())
}
}
} catch (err) {
console.error('Failed to fetch queue:', err)
}
}, [selectedSession, currentItem])
// Fetch stats
const fetchStats = useCallback(async () => {
try {
const url = selectedSession
? `${API_BASE}/api/v1/ocr-label/stats?session_id=${selectedSession}`
: `${API_BASE}/api/v1/ocr-label/stats`
const res = await fetch(url)
if (res.ok) {
const data = await res.json()
setStats(data)
}
} catch (err) {
console.error('Failed to fetch stats:', err)
}
}, [selectedSession])
// Initial data load
useEffect(() => {
const loadData = async () => {
setLoading(true)
await Promise.all([fetchSessions(), fetchQueue(), fetchStats()])
setLoading(false)
}
loadData()
}, [fetchSessions, fetchQueue, fetchStats])
// Refresh queue when session changes
useEffect(() => {
setCurrentItem(null)
setCurrentIndex(0)
fetchQueue()
fetchStats()
}, [selectedSession, fetchQueue, fetchStats])
// Navigate to next item
const goToNext = () => {
if (currentIndex < queue.length - 1) {
const nextIndex = currentIndex + 1
setCurrentIndex(nextIndex)
setCurrentItem(queue[nextIndex])
setCorrectedText(queue[nextIndex].ocr_text || '')
setLabelStartTime(Date.now())
} else {
// Refresh queue
fetchQueue()
}
}
// Navigate to previous item
const goToPrev = () => {
if (currentIndex > 0) {
const prevIndex = currentIndex - 1
setCurrentIndex(prevIndex)
setCurrentItem(queue[prevIndex])
setCorrectedText(queue[prevIndex].ocr_text || '')
setLabelStartTime(Date.now())
}
}
// Calculate label time
const getLabelTime = (): number | undefined => {
if (!labelStartTime) return undefined
return Math.round((Date.now() - labelStartTime) / 1000)
}
// Confirm item
const confirmItem = async () => {
if (!currentItem) return
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/confirm`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item_id: currentItem.id,
label_time_seconds: getLabelTime(),
}),
})
if (res.ok) {
// Remove from queue and go to next
setQueue(prev => prev.filter(item => item.id !== currentItem.id))
goToNext()
fetchStats()
} else {
setError('Bestaetigung fehlgeschlagen')
}
} catch (err) {
setError('Netzwerkfehler')
}
}
// Correct item
const correctItem = async () => {
if (!currentItem || !correctedText.trim()) return
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/correct`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
item_id: currentItem.id,
ground_truth: correctedText.trim(),
label_time_seconds: getLabelTime(),
}),
})
if (res.ok) {
setQueue(prev => prev.filter(item => item.id !== currentItem.id))
goToNext()
fetchStats()
} else {
setError('Korrektur fehlgeschlagen')
}
} catch (err) {
setError('Netzwerkfehler')
}
}
// Skip item
const skipItem = async () => {
if (!currentItem) return
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/skip`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ item_id: currentItem.id }),
})
if (res.ok) {
setQueue(prev => prev.filter(item => item.id !== currentItem.id))
goToNext()
fetchStats()
} else {
setError('Ueberspringen fehlgeschlagen')
}
} catch (err) {
setError('Netzwerkfehler')
}
}
// Keyboard shortcuts
useEffect(() => {
const handleKeyDown = (e: KeyboardEvent) => {
// Only handle if not in text input
if (e.target instanceof HTMLTextAreaElement) return
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
confirmItem()
} else if (e.key === 'ArrowRight') {
goToNext()
} else if (e.key === 'ArrowLeft') {
goToPrev()
} else if (e.key === 's' && !e.ctrlKey && !e.metaKey) {
skipItem()
}
}
window.addEventListener('keydown', handleKeyDown)
return () => window.removeEventListener('keydown', handleKeyDown)
}, [currentItem, correctedText])
// Render Labeling Tab
const renderLabelingTab = () => (
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
{/* Left: Image Viewer */}
<div className="lg:col-span-2 bg-white rounded-lg shadow p-4">
<div className="flex items-center justify-between mb-4">
<h3 className="text-lg font-semibold">Bild</h3>
<div className="flex items-center gap-2">
<button
onClick={goToPrev}
disabled={currentIndex === 0}
className="p-2 rounded hover:bg-slate-100 disabled:opacity-50"
title="Zurueck (Pfeiltaste links)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
</button>
<span className="text-sm text-slate-600">
{currentIndex + 1} / {queue.length}
</span>
<button
onClick={goToNext}
disabled={currentIndex >= queue.length - 1}
className="p-2 rounded hover:bg-slate-100 disabled:opacity-50"
title="Weiter (Pfeiltaste rechts)"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
</svg>
</button>
</div>
</div>
{currentItem ? (
<div className="relative bg-slate-100 rounded-lg overflow-hidden" style={{ minHeight: '400px' }}>
<img
src={currentItem.image_url || `${API_BASE}${currentItem.image_path}`}
alt="OCR Bild"
className="w-full h-auto max-h-[600px] object-contain"
onError={(e) => {
// Fallback if image fails to load
const target = e.target as HTMLImageElement
target.style.display = 'none'
}}
/>
</div>
) : (
<div className="flex items-center justify-center h-64 bg-slate-100 rounded-lg">
<p className="text-slate-500">Keine Bilder in der Warteschlange</p>
</div>
)}
</div>
{/* Right: OCR Text & Actions */}
<div className="bg-white rounded-lg shadow p-4">
<div className="space-y-4">
{/* OCR Result */}
<div>
<div className="flex items-center justify-between mb-2">
<h3 className="text-lg font-semibold">OCR-Ergebnis</h3>
{currentItem?.ocr_confidence && (
<span className={`text-sm px-2 py-1 rounded ${
currentItem.ocr_confidence > 0.8
? 'bg-green-100 text-green-800'
: currentItem.ocr_confidence > 0.5
? 'bg-yellow-100 text-yellow-800'
: 'bg-red-100 text-red-800'
}`}>
{Math.round(currentItem.ocr_confidence * 100)}% Konfidenz
</span>
)}
</div>
<div className="bg-slate-50 p-3 rounded-lg min-h-[100px] text-sm">
{currentItem?.ocr_text || <span className="text-slate-400">Kein OCR-Text</span>}
</div>
</div>
{/* Correction Input */}
<div>
<h3 className="text-lg font-semibold mb-2">Korrektur</h3>
<textarea
value={correctedText}
onChange={(e) => setCorrectedText(e.target.value)}
placeholder="Korrigierter Text..."
className="w-full h-32 p-3 border border-slate-200 rounded-lg focus:ring-2 focus:ring-primary-500 focus:border-transparent"
/>
</div>
{/* Actions */}
<div className="flex flex-col gap-2">
<button
onClick={confirmItem}
disabled={!currentItem}
className="w-full px-4 py-3 bg-green-600 text-white rounded-lg hover:bg-green-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Korrekt (Enter)
</button>
<button
onClick={correctItem}
disabled={!currentItem || !correctedText.trim() || correctedText === currentItem?.ocr_text}
className="w-full px-4 py-3 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50 flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15.232 5.232l3.536 3.536m-2.036-5.036a2.5 2.5 0 113.536 3.536L6.5 21.036H3v-3.572L16.732 3.732z" />
</svg>
Korrektur speichern
</button>
<button
onClick={skipItem}
disabled={!currentItem}
className="w-full px-4 py-2 bg-slate-200 text-slate-700 rounded-lg hover:bg-slate-300 disabled:opacity-50 flex items-center justify-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 5l7 7-7 7M5 5l7 7-7 7" />
</svg>
Ueberspringen (S)
</button>
</div>
{/* Keyboard Shortcuts */}
<div className="text-xs text-slate-500 mt-4">
<p className="font-medium mb-1">Tastaturkuerzel:</p>
<p>Enter = Bestaetigen | S = Ueberspringen</p>
<p>Pfeiltasten = Navigation</p>
</div>
</div>
</div>
{/* Bottom: Queue Preview */}
<div className="lg:col-span-3 bg-white rounded-lg shadow p-4">
<h3 className="text-lg font-semibold mb-4">Warteschlange ({queue.length} Items)</h3>
<div className="flex gap-2 overflow-x-auto pb-2">
{queue.slice(0, 10).map((item, idx) => (
<button
key={item.id}
onClick={() => {
setCurrentIndex(idx)
setCurrentItem(item)
setCorrectedText(item.ocr_text || '')
setLabelStartTime(Date.now())
}}
className={`flex-shrink-0 w-24 h-24 rounded-lg overflow-hidden border-2 ${
idx === currentIndex
? 'border-primary-500'
: 'border-transparent hover:border-slate-300'
}`}
>
<img
src={item.image_url || `${API_BASE}${item.image_path}`}
alt=""
className="w-full h-full object-cover"
/>
</button>
))}
{queue.length > 10 && (
<div className="flex-shrink-0 w-24 h-24 rounded-lg bg-slate-100 flex items-center justify-center text-slate-500">
+{queue.length - 10} mehr
</div>
)}
</div>
</div>
</div>
)
// Render Sessions Tab
const renderSessionsTab = () => {
const [newSession, setNewSession] = useState<CreateSessionRequest>({
name: '',
source_type: 'klausur',
description: '',
ocr_model: 'llama3.2-vision:11b',
})
const createSession = async () => {
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/sessions`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(newSession),
})
if (res.ok) {
setNewSession({ name: '', source_type: 'klausur', description: '', ocr_model: 'llama3.2-vision:11b' })
fetchSessions()
} else {
setError('Session erstellen fehlgeschlagen')
}
} catch (err) {
setError('Netzwerkfehler')
}
}
return (
<div className="space-y-6">
{/* Create Session */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Neue Session erstellen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Name</label>
<input
type="text"
value={newSession.name}
onChange={(e) => setNewSession(prev => ({ ...prev, name: e.target.value }))}
placeholder="z.B. Mathe Klausur Q1 2025"
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Typ</label>
<select
value={newSession.source_type}
onChange={(e) => setNewSession(prev => ({ ...prev, source_type: e.target.value as 'klausur' | 'handwriting_sample' | 'scan' }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="klausur">Klausur</option>
<option value="handwriting_sample">Handschriftprobe</option>
<option value="scan">Scan</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">OCR Modell</label>
<select
value={newSession.ocr_model}
onChange={(e) => setNewSession(prev => ({ ...prev, ocr_model: e.target.value as OCRModel }))}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="llama3.2-vision:11b">llama3.2-vision:11b - Vision LLM (Standard)</option>
<option value="trocr">TrOCR - Microsoft Transformer (schnell)</option>
<option value="paddleocr">PaddleOCR + LLM (4x schneller)</option>
<option value="donut">Donut - Document Understanding (strukturiert)</option>
</select>
<p className="mt-1 text-xs text-slate-500">
{newSession.ocr_model === 'paddleocr' && 'PaddleOCR erkennt Text schnell, LLM strukturiert die Ergebnisse.'}
{newSession.ocr_model === 'donut' && 'Speziell fuer Dokumente mit Tabellen und Formularen.'}
{newSession.ocr_model === 'trocr' && 'Schnelles Transformer-Modell fuer gedruckten Text.'}
{newSession.ocr_model === 'llama3.2-vision:11b' && 'Beste Qualitaet bei Handschrift, aber langsamer.'}
</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Beschreibung</label>
<input
type="text"
value={newSession.description}
onChange={(e) => setNewSession(prev => ({ ...prev, description: e.target.value }))}
placeholder="Optional..."
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
/>
</div>
</div>
<button
onClick={createSession}
disabled={!newSession.name}
className="mt-4 px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
Session erstellen
</button>
</div>
{/* Sessions List */}
<div className="bg-white rounded-lg shadow">
<div className="px-6 py-4 border-b border-slate-200">
<h3 className="text-lg font-semibold">Sessions ({sessions.length})</h3>
</div>
<div className="divide-y divide-slate-200">
{sessions.map((session) => (
<div
key={session.id}
className={`p-4 hover:bg-slate-50 cursor-pointer ${
selectedSession === session.id ? 'bg-primary-50 border-l-4 border-primary-500' : ''
}`}
onClick={() => setSelectedSession(session.id === selectedSession ? null : session.id)}
>
<div className="flex items-center justify-between">
<div>
<h4 className="font-medium">{session.name}</h4>
<p className="text-sm text-slate-500">
{session.source_type} | {session.ocr_model}
</p>
</div>
<div className="text-right">
<p className="text-sm font-medium">
{session.labeled_items}/{session.total_items} gelabelt
</p>
<div className="w-32 bg-slate-200 rounded-full h-2 mt-1">
<div
className="bg-primary-600 rounded-full h-2"
style={{
width: `${session.total_items > 0 ? (session.labeled_items / session.total_items) * 100 : 0}%`
}}
/>
</div>
</div>
</div>
{session.description && (
<p className="text-sm text-slate-600 mt-2">{session.description}</p>
)}
</div>
))}
{sessions.length === 0 && (
<p className="p-4 text-slate-500 text-center">Keine Sessions vorhanden</p>
)}
</div>
</div>
</div>
)
}
// Render Upload Tab
const renderUploadTab = () => {
const [uploading, setUploading] = useState(false)
const [uploadResults, setUploadResults] = useState<any[]>([])
const fileInputRef = useRef<HTMLInputElement>(null)
const handleUpload = async (files: FileList) => {
if (!selectedSession) {
setError('Bitte zuerst eine Session auswaehlen')
return
}
setUploading(true)
const formData = new FormData()
Array.from(files).forEach(file => formData.append('files', file))
formData.append('run_ocr', 'true')
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/sessions/${selectedSession}/upload`, {
method: 'POST',
body: formData,
})
if (res.ok) {
const data = await res.json()
setUploadResults(data.items || [])
fetchQueue()
fetchStats()
} else {
setError('Upload fehlgeschlagen')
}
} catch (err) {
setError('Netzwerkfehler beim Upload')
} finally {
setUploading(false)
}
}
return (
<div className="space-y-6">
{/* Session Selection */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Session auswaehlen</h3>
<select
value={selectedSession || ''}
onChange={(e) => setSelectedSession(e.target.value || null)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">-- Session waehlen --</option>
{sessions.map((session) => (
<option key={session.id} value={session.id}>
{session.name} ({session.total_items} Items)
</option>
))}
</select>
</div>
{/* Upload Area */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Bilder hochladen</h3>
<div
className={`border-2 border-dashed rounded-lg p-8 text-center ${
selectedSession ? 'border-slate-300 hover:border-primary-500' : 'border-slate-200 opacity-50'
}`}
onDragOver={(e) => {
e.preventDefault()
e.currentTarget.classList.add('border-primary-500', 'bg-primary-50')
}}
onDragLeave={(e) => {
e.currentTarget.classList.remove('border-primary-500', 'bg-primary-50')
}}
onDrop={(e) => {
e.preventDefault()
e.currentTarget.classList.remove('border-primary-500', 'bg-primary-50')
if (e.dataTransfer.files.length > 0) {
handleUpload(e.dataTransfer.files)
}
}}
>
<input
ref={fileInputRef}
type="file"
multiple
accept="image/png,image/jpeg,image/jpg"
onChange={(e) => e.target.files && handleUpload(e.target.files)}
className="hidden"
disabled={!selectedSession}
/>
{uploading ? (
<div className="flex flex-col items-center gap-2">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
<p>Hochladen & OCR ausfuehren...</p>
</div>
) : (
<>
<svg className="w-12 h-12 text-slate-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
<p className="text-slate-600 mb-2">
Bilder hierher ziehen oder{' '}
<button
onClick={() => fileInputRef.current?.click()}
disabled={!selectedSession}
className="text-primary-600 hover:underline"
>
auswaehlen
</button>
</p>
<p className="text-sm text-slate-500">PNG, JPG (max. 10MB pro Bild)</p>
</>
)}
</div>
</div>
{/* Upload Results */}
{uploadResults.length > 0 && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Upload-Ergebnisse ({uploadResults.length})</h3>
<div className="space-y-2">
{uploadResults.map((result) => (
<div key={result.id} className="flex items-center justify-between p-2 bg-slate-50 rounded">
<span className="text-sm">{result.filename}</span>
<span className={`text-xs px-2 py-1 rounded ${
result.ocr_text ? 'bg-green-100 text-green-800' : 'bg-yellow-100 text-yellow-800'
}`}>
{result.ocr_text ? `OCR OK (${Math.round((result.ocr_confidence || 0) * 100)}%)` : 'Kein OCR'}
</span>
</div>
))}
</div>
</div>
)}
</div>
)
}
// Render Stats Tab
const renderStatsTab = () => (
<div className="space-y-6">
{/* Global Stats */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
<div className="bg-white rounded-lg shadow p-6">
<h4 className="text-sm font-medium text-slate-500">Gesamt Items</h4>
<p className="text-3xl font-bold mt-2">{stats?.total_items || 0}</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h4 className="text-sm font-medium text-slate-500">Gelabelt</h4>
<p className="text-3xl font-bold mt-2 text-green-600">{stats?.labeled_items || 0}</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h4 className="text-sm font-medium text-slate-500">Ausstehend</h4>
<p className="text-3xl font-bold mt-2 text-yellow-600">{stats?.pending_items || 0}</p>
</div>
<div className="bg-white rounded-lg shadow p-6">
<h4 className="text-sm font-medium text-slate-500">OCR-Genauigkeit</h4>
<p className="text-3xl font-bold mt-2">{stats?.accuracy_rate || 0}%</p>
</div>
</div>
{/* Detailed Stats */}
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Details</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-sm text-slate-500">Bestaetigt</p>
<p className="text-xl font-semibold text-green-600">{stats?.confirmed_items || 0}</p>
</div>
<div>
<p className="text-sm text-slate-500">Korrigiert</p>
<p className="text-xl font-semibold text-primary-600">{stats?.corrected_items || 0}</p>
</div>
<div>
<p className="text-sm text-slate-500">Exportierbar</p>
<p className="text-xl font-semibold">{stats?.exportable_items || 0}</p>
</div>
<div>
<p className="text-sm text-slate-500">Durchschn. Label-Zeit</p>
<p className="text-xl font-semibold">{stats?.avg_label_time_seconds || 0}s</p>
</div>
</div>
</div>
{/* Progress Bar */}
{stats?.total_items ? (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Fortschritt</h3>
<div className="w-full bg-slate-200 rounded-full h-4">
<div
className="bg-primary-600 rounded-full h-4 transition-all"
style={{ width: `${(stats.labeled_items / stats.total_items) * 100}%` }}
/>
</div>
<p className="text-sm text-slate-500 mt-2">
{Math.round((stats.labeled_items / stats.total_items) * 100)}% abgeschlossen
</p>
</div>
) : null}
</div>
)
// Render Export Tab
const renderExportTab = () => {
const [exportFormat, setExportFormat] = useState<'generic' | 'trocr' | 'llama_vision'>('generic')
const [exporting, setExporting] = useState(false)
const [exportResult, setExportResult] = useState<any>(null)
const handleExport = async () => {
setExporting(true)
try {
const res = await fetch(`${API_BASE}/api/v1/ocr-label/export`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
export_format: exportFormat,
session_id: selectedSession,
}),
})
if (res.ok) {
const data = await res.json()
setExportResult(data)
} else {
setError('Export fehlgeschlagen')
}
} catch (err) {
setError('Netzwerkfehler')
} finally {
setExporting(false)
}
}
return (
<div className="space-y-6">
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Training-Daten exportieren</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Export-Format</label>
<select
value={exportFormat}
onChange={(e) => setExportFormat(e.target.value as typeof exportFormat)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="generic">Generic JSON</option>
<option value="trocr">TrOCR Fine-Tuning</option>
<option value="llama_vision">Llama Vision Fine-Tuning</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">Session (optional)</label>
<select
value={selectedSession || ''}
onChange={(e) => setSelectedSession(e.target.value || null)}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-primary-500"
>
<option value="">Alle Sessions</option>
{sessions.map((session) => (
<option key={session.id} value={session.id}>{session.name}</option>
))}
</select>
</div>
<button
onClick={handleExport}
disabled={exporting || (stats?.exportable_items || 0) === 0}
className="w-full px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
>
{exporting ? 'Exportiere...' : `${stats?.exportable_items || 0} Samples exportieren`}
</button>
{/* Cross-Link to Magic Help for TrOCR Fine-Tuning */}
{exportFormat === 'trocr' && (stats?.exportable_items || 0) > 0 && (
<Link
href="/ai/magic-help?source=ocr-labeling"
className="w-full mt-3 px-4 py-2 bg-purple-100 text-purple-700 border border-purple-300 rounded-lg hover:bg-purple-200 flex items-center justify-center gap-2 transition-colors"
>
<span></span>
Mit Magic Help testen & fine-tunen
</Link>
)}
</div>
</div>
{exportResult && (
<div className="bg-white rounded-lg shadow p-6">
<h3 className="text-lg font-semibold mb-4">Export-Ergebnis</h3>
<div className="bg-green-50 border border-green-200 rounded-lg p-4 mb-4">
<p className="text-green-800">
{exportResult.exported_count} Samples erfolgreich exportiert
</p>
<p className="text-sm text-green-600">
Batch: {exportResult.batch_id}
</p>
</div>
<div className="bg-slate-50 p-4 rounded-lg overflow-auto max-h-64">
<pre className="text-xs">{JSON.stringify(exportResult.samples?.slice(0, 3), null, 2)}</pre>
{(exportResult.samples?.length || 0) > 3 && (
<p className="text-slate-500 mt-2">... und {exportResult.samples.length - 3} weitere</p>
)}
</div>
</div>
)}
</div>
)
}
return (
<div className="p-6">
{/* Header */}
<div className="mb-6">
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">OCR-Labeling</h1>
<p className="text-gray-600 dark:text-gray-400">Handschrift-Training & Ground Truth Erfassung</p>
</div>
{/* Page Purpose with Related Pages */}
<PagePurpose
title="OCR-Labeling"
purpose="Erstellen Sie Ground Truth Daten für das Training von Handschrift-Erkennungsmodellen. Labeln Sie OCR-Ergebnisse, korrigieren Sie Fehler und exportieren Sie Trainingsdaten für TrOCR, Llama Vision und andere Modelle. Teil der KI-Daten-Pipeline: Gelabelte Daten können zur RAG Pipeline exportiert werden."
audience={['Entwickler', 'Data Scientists', 'QA-Team']}
architecture={{
services: ['klausur-service (Python)'],
databases: ['PostgreSQL', 'MinIO (Bilder)'],
}}
relatedPages={[
{ name: 'Magic Help', href: '/ai/magic-help', description: 'TrOCR testen & fine-tunen' },
{ name: 'RAG Pipeline', href: '/ai/rag-pipeline', description: 'Trainierte Daten indexieren' },
{ name: 'Klausur-Korrektur', href: '/ai/klausur-korrektur', description: 'OCR in Aktion' },
{ name: 'Daten & RAG', href: '/ai/rag', description: 'Indexierte Daten durchsuchen' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* AI Module Sidebar - Desktop: Fixed, Mobile: FAB + Drawer */}
<AIModuleSidebarResponsive currentModule="ocr-labeling" />
{/* Error Toast */}
{error && (
<div className="fixed top-4 right-4 bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded z-50">
<span>{error}</span>
<button onClick={() => setError(null)} className="ml-4">X</button>
</div>
)}
{/* Tabs */}
<div className="mb-6">
<div className="border-b border-slate-200">
<nav className="flex space-x-4" aria-label="Tabs">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`flex items-center gap-2 px-4 py-3 border-b-2 font-medium text-sm transition-colors ${
activeTab === tab.id
? 'border-primary-500 text-primary-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
}`}
>
{tab.icon}
{tab.name}
</button>
))}
</nav>
</div>
</div>
{/* Tab Content */}
{loading ? (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-primary-600" />
</div>
) : (
<>
{activeTab === 'labeling' && renderLabelingTab()}
{activeTab === 'sessions' && renderSessionsTab()}
{activeTab === 'upload' && renderUploadTab()}
{activeTab === 'stats' && renderStatsTab()}
{activeTab === 'export' && renderExportTab()}
</>
)}
</div>
)
}

View File

@@ -0,0 +1,123 @@
/**
* TypeScript types for OCR Labeling UI
*/
/**
* Available OCR Models
*
* - llama3.2-vision:11b: Vision LLM, beste Qualitaet bei Handschrift (Standard)
* - trocr: Microsoft TrOCR, schnell bei gedrucktem Text
* - paddleocr: PaddleOCR + LLM, 4x schneller durch Hybrid-Ansatz
* - donut: Document Understanding Transformer, strukturierte Dokumente
*/
export type OCRModel = 'llama3.2-vision:11b' | 'trocr' | 'paddleocr' | 'donut'
export const OCR_MODEL_INFO: Record<OCRModel, { label: string; description: string; speed: string }> = {
'llama3.2-vision:11b': {
label: 'Vision LLM',
description: 'Beste Qualitaet bei Handschrift',
speed: 'langsam',
},
trocr: {
label: 'Microsoft TrOCR',
description: 'Schnell bei gedrucktem Text',
speed: 'schnell',
},
paddleocr: {
label: 'PaddleOCR + LLM',
description: 'Hybrid-Ansatz: OCR + Strukturierung',
speed: 'sehr schnell',
},
donut: {
label: 'Donut',
description: 'Document Understanding fuer Tabellen/Formulare',
speed: 'mittel',
},
}
export interface OCRSession {
id: string
name: string
source_type: 'klausur' | 'handwriting_sample' | 'scan'
description?: string
ocr_model?: OCRModel
total_items: number
labeled_items: number
confirmed_items: number
corrected_items: number
skipped_items: number
created_at: string
}
export interface OCRItem {
id: string
session_id: string
session_name: string
image_path: string
image_url?: string
ocr_text?: string
ocr_confidence?: number
ground_truth?: string
status: 'pending' | 'confirmed' | 'corrected' | 'skipped'
metadata?: Record<string, unknown>
created_at: string
}
export interface OCRStats {
total_sessions?: number
session_id?: string
name?: string
total_items: number
labeled_items: number
confirmed_items: number
corrected_items: number
skipped_items?: number
pending_items: number
exportable_items?: number
accuracy_rate: number
avg_label_time_seconds?: number
progress_percent?: number
}
export interface TrainingSample {
id: string
image_path: string
ground_truth: string
export_format: 'generic' | 'trocr' | 'llama_vision'
training_batch: string
exported_at?: string
}
export interface CreateSessionRequest {
name: string
source_type: 'klausur' | 'handwriting_sample' | 'scan'
description?: string
ocr_model?: OCRModel
}
export interface ConfirmRequest {
item_id: string
label_time_seconds?: number
}
export interface CorrectRequest {
item_id: string
ground_truth: string
label_time_seconds?: number
}
export interface ExportRequest {
export_format: 'generic' | 'trocr' | 'llama_vision'
session_id?: string
batch_id?: string
}
export interface UploadResult {
id: string
filename: string
image_path: string
image_hash: string
ocr_text?: string
ocr_confidence?: number
status: string
}

View File

@@ -0,0 +1,53 @@
'use client'
import { getCategoryById } from '@/lib/navigation'
import { ModuleCard } from '@/components/common/ModuleCard'
import { PagePurpose } from '@/components/common/PagePurpose'
export default function AIPage() {
const category = getCategoryById('ai')
if (!category) {
return <div>Kategorie nicht gefunden</div>
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title={category.name}
purpose="Diese Kategorie umfasst alle KI- und Machine-Learning-Module. Hier vergleichen Sie LLM-Provider, verwalten RAG-Pipelines, labeln OCR-Daten und nutzen KI-gestuetzte Korrektur-Tools."
audience={['Entwickler', 'Data Scientists', 'Lehrer']}
architecture={{
services: ['klausur-service (Python)', 'embedding-service (Python)', 'backend (Python)'],
databases: ['PostgreSQL', 'Qdrant (Vector)', 'MinIO (Object Storage)'],
}}
relatedPages={[
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU-Ressourcen fuer Training' },
]}
collapsible={true}
defaultCollapsed={false}
/>
{/* Modules Grid */}
<h2 className="text-lg font-semibold text-slate-900 mb-4">Module</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{category.modules.map((module) => (
<ModuleCard key={module.id} module={module} category={category} />
))}
</div>
{/* Info Section */}
<div className="mt-8 bg-teal-50 border border-teal-200 rounded-xl p-6">
<h3 className="font-semibold text-teal-800 flex items-center gap-2">
<span>🧠</span>
DSGVO-konforme KI
</h3>
<p className="text-sm text-teal-700 mt-2">
Alle KI-Modelle koennen lokal auf dem Mac Mini mit Ollama ausgefuehrt werden.
Keine Daten werden an externe Cloud-Anbieter gesendet, sofern nicht explizit konfiguriert.
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,524 @@
'use client'
/**
* Quality & Audit Page
*
* Ermoeglicht Auditoren:
* - Chunk-Suche und Stichproben
* - Traceability: Chunk → Requirement → Control
* - Dokumenten-Vollstaendigkeitspruefung
*/
import { useState, useCallback } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
const API_PROXY = '/api/legal-corpus'
// Types
interface ChunkDetail {
id: string
text: string
regulation_code: string
regulation_name: string
article: string | null
paragraph: string | null
chunk_index: number
chunk_position: 'beginning' | 'middle' | 'end'
source_url: string
score?: number
}
interface Requirement {
id: string
text: string
category: string
source_chunk_id: string
regulation_code: string
}
interface Control {
id: string
name: string
description: string
source_requirement_ids: string[]
regulation_codes: string[]
}
interface TraceabilityResult {
chunk: ChunkDetail
requirements: Requirement[]
controls: Control[]
}
// Regulations for filtering
const REGULATIONS = [
{ code: 'GDPR', name: 'DSGVO' },
{ code: 'EPRIVACY', name: 'ePrivacy' },
{ code: 'TDDDG', name: 'TDDDG' },
{ code: 'SCC', name: 'Standardvertragsklauseln' },
{ code: 'DPF', name: 'EU-US DPF' },
{ code: 'AIACT', name: 'EU AI Act' },
{ code: 'CRA', name: 'Cyber Resilience Act' },
{ code: 'NIS2', name: 'NIS2' },
{ code: 'EUCSA', name: 'EU Cybersecurity Act' },
{ code: 'DATAACT', name: 'Data Act' },
{ code: 'DGA', name: 'Data Governance Act' },
{ code: 'DSA', name: 'Digital Services Act' },
{ code: 'EAA', name: 'Accessibility Act' },
{ code: 'DSM', name: 'DSM-Urheberrecht' },
{ code: 'PLD', name: 'Produkthaftung' },
{ code: 'GPSR', name: 'Product Safety' },
{ code: 'BSI-TR-03161-1', name: 'BSI-TR Teil 1' },
{ code: 'BSI-TR-03161-2', name: 'BSI-TR Teil 2' },
{ code: 'BSI-TR-03161-3', name: 'BSI-TR Teil 3' },
]
const TYPE_COLORS: Record<string, string> = {
eu_regulation: 'bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400',
eu_directive: 'bg-purple-100 text-purple-700 dark:bg-purple-900/30 dark:text-purple-400',
de_law: 'bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400',
bsi_standard: 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400',
}
export default function QualityPage() {
// Search state
const [searchQuery, setSearchQuery] = useState('')
const [searchResults, setSearchResults] = useState<ChunkDetail[]>([])
const [searching, setSearching] = useState(false)
const [selectedRegulation, setSelectedRegulation] = useState<string>('')
const [topK, setTopK] = useState(10)
// Traceability state
const [selectedChunk, setSelectedChunk] = useState<ChunkDetail | null>(null)
const [traceability, setTraceability] = useState<TraceabilityResult | null>(null)
const [loadingTrace, setLoadingTrace] = useState(false)
// Quick sample queries for auditors
const sampleQueries = [
{ label: 'Art. 17 DSGVO (Recht auf Loeschung)', query: 'Recht auf Löschung Artikel 17', reg: 'GDPR' },
{ label: 'Einwilligung TDDDG', query: 'Einwilligung Endeinrichtung speichern', reg: 'TDDDG' },
{ label: 'AI Act Hochrisiko', query: 'Hochrisiko-KI-System Anforderungen', reg: 'AIACT' },
{ label: 'NIS2 Sicherheitsmaßnahmen', query: 'Cybersicherheitsrisikomanagement Maßnahmen', reg: 'NIS2' },
{ label: 'BSI Authentifizierung', query: 'Authentifizierung Zwei-Faktor mobile', reg: 'BSI-TR-03161-1' },
]
const handleSearch = useCallback(async () => {
if (!searchQuery.trim()) return
setSearching(true)
setSearchResults([])
setSelectedChunk(null)
setTraceability(null)
try {
let url = `${API_PROXY}?action=search&query=${encodeURIComponent(searchQuery)}&top_k=${topK}`
if (selectedRegulation) {
url += `&regulations=${encodeURIComponent(selectedRegulation)}`
}
const res = await fetch(url)
if (res.ok) {
const data = await res.json()
setSearchResults(data.results || [])
}
} catch (error) {
console.error('Search failed:', error)
} finally {
setSearching(false)
}
}, [searchQuery, selectedRegulation, topK])
const loadTraceability = useCallback(async (chunk: ChunkDetail) => {
setSelectedChunk(chunk)
setLoadingTrace(true)
try {
// Try to load traceability (requirements and controls derived from this chunk)
const res = await fetch(`${API_PROXY}?action=traceability&chunk_id=${encodeURIComponent(chunk.id || chunk.regulation_code + '_' + chunk.chunk_index)}&regulation=${encodeURIComponent(chunk.regulation_code)}`)
if (res.ok) {
const data = await res.json()
setTraceability({
chunk,
requirements: data.requirements || [],
controls: data.controls || [],
})
} else {
// If traceability endpoint doesn't exist yet, show placeholder
setTraceability({
chunk,
requirements: [],
controls: [],
})
}
} catch (error) {
console.error('Failed to load traceability:', error)
setTraceability({
chunk,
requirements: [],
controls: [],
})
} finally {
setLoadingTrace(false)
}
}, [])
const handleSampleQuery = (query: string, reg: string) => {
setSearchQuery(query)
setSelectedRegulation(reg)
// Auto-search after setting
setTimeout(() => {
handleSearch()
}, 100)
}
const highlightText = (text: string, query: string) => {
if (!query) return text
const words = query.toLowerCase().split(' ').filter(w => w.length > 2)
let result = text
words.forEach(word => {
const regex = new RegExp(`(${word})`, 'gi')
result = result.replace(regex, '<mark class="bg-yellow-200 dark:bg-yellow-800 px-0.5 rounded">$1</mark>')
})
return result
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">
Qualitaet & Audit
</h1>
<p className="mt-1 text-sm text-gray-500 dark:text-gray-400">
Stichproben und Traceability fuer Compliance-Auditoren
</p>
</div>
<Link
href="/ai/rag"
className="text-sm text-blue-600 hover:text-blue-800 dark:text-blue-400"
>
Zurueck zu RAG
</Link>
</div>
<PagePurpose
title="Audit-Werkzeuge"
purpose="Pruefen Sie die Qualitaet der Compliance-Datenbank. Suchen Sie gezielt nach Paragraphen, Saetzen oder Begriffen und verfolgen Sie, wie Anforderungen und Controls abgeleitet wurden."
audience={['Auditoren', 'Compliance-Beauftragte', 'Qualitaetssicherung']}
architecture={{
services: ['klausur-service', 'embedding-service', 'qdrant'],
databases: ['Qdrant Vector DB']
}}
/>
{/* Quick Sample Queries */}
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
<h3 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-3">
Schnell-Stichproben
</h3>
<div className="flex flex-wrap gap-2">
{sampleQueries.map((sq, idx) => (
<button
key={idx}
onClick={() => handleSampleQuery(sq.query, sq.reg)}
className="px-3 py-1.5 text-xs bg-gray-100 hover:bg-gray-200 dark:bg-slate-700 dark:hover:bg-slate-600 text-gray-700 dark:text-gray-300 rounded-full transition-colors"
>
{sq.label}
</button>
))}
</div>
</div>
{/* Search Section */}
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-6">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white mb-4">
Chunk-Suche
</h2>
<div className="space-y-4">
{/* Search Input */}
<div className="flex gap-4">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Suchbegriff / Paragraph / Artikeltext
</label>
<input
type="text"
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
placeholder="z.B. 'Recht auf Löschung' oder 'Art. 17 Abs. 1'"
className="w-full px-4 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
/>
</div>
<div className="w-48">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Regulierung
</label>
<select
value={selectedRegulation}
onChange={(e) => setSelectedRegulation(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
>
<option value="">Alle</option>
{REGULATIONS.map((reg) => (
<option key={reg.code} value={reg.code}>
{reg.name}
</option>
))}
</select>
</div>
<div className="w-24">
<label className="block text-sm font-medium text-gray-700 dark:text-gray-300 mb-1">
Anzahl
</label>
<select
value={topK}
onChange={(e) => setTopK(parseInt(e.target.value))}
className="w-full px-3 py-2 border border-gray-300 dark:border-slate-600 rounded-lg focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white"
>
<option value="5">5</option>
<option value="10">10</option>
<option value="20">20</option>
</select>
</div>
</div>
<button
onClick={handleSearch}
disabled={searching || !searchQuery.trim()}
className="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
>
{searching ? 'Suche laeuft...' : 'Suchen'}
</button>
</div>
</div>
{/* Results Grid */}
{searchResults.length > 0 && (
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Search Results List */}
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-4">
Gefundene Chunks ({searchResults.length})
</h3>
<div className="space-y-3 max-h-[600px] overflow-y-auto">
{searchResults.map((result, idx) => (
<div
key={idx}
onClick={() => loadTraceability(result)}
className={`p-4 border rounded-lg cursor-pointer transition-all ${
selectedChunk?.text === result.text
? 'border-blue-500 bg-blue-50 dark:bg-blue-900/20'
: 'border-gray-200 dark:border-slate-700 hover:border-gray-300 dark:hover:border-slate-600'
}`}
>
{/* Header */}
<div className="flex items-center justify-between mb-2">
<div className="flex items-center gap-2">
<span className="text-xs font-medium px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded">
{result.regulation_code}
</span>
{result.article && (
<span className="text-xs text-gray-500 dark:text-gray-400">
Art. {result.article}
{result.paragraph && ` Abs. ${result.paragraph}`}
</span>
)}
</div>
<span className="text-xs text-gray-400">
Score: {(result.score || 0).toFixed(3)}
</span>
</div>
{/* Text Preview */}
<p
className="text-sm text-gray-700 dark:text-gray-300 line-clamp-4"
dangerouslySetInnerHTML={{
__html: highlightText(result.text.substring(0, 400) + (result.text.length > 400 ? '...' : ''), searchQuery)
}}
/>
{/* Metadata */}
<div className="mt-2 flex items-center gap-4 text-xs text-gray-400">
<span>Chunk #{result.chunk_index || idx}</span>
<span>{result.text.length} Zeichen</span>
</div>
</div>
))}
</div>
</div>
{/* Traceability Panel */}
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-4">
<h3 className="text-md font-semibold text-gray-900 dark:text-white mb-4">
Traceability
</h3>
{!selectedChunk ? (
<div className="text-center py-12 text-gray-500 dark:text-gray-400">
<svg className="w-12 h-12 mx-auto mb-4 opacity-50" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p>Waehlen Sie einen Chunk aus der Liste, um die Traceability zu sehen.</p>
</div>
) : loadingTrace ? (
<div className="text-center py-12">
<div className="animate-spin w-8 h-8 border-2 border-blue-500 border-t-transparent rounded-full mx-auto mb-4"></div>
<p className="text-gray-500 dark:text-gray-400">Lade Traceability...</p>
</div>
) : traceability ? (
<div className="space-y-6">
{/* Selected Chunk Detail */}
<div className="border-l-4 border-blue-500 pl-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
📄 Ausgewaehlter Chunk
</h4>
<div className="bg-gray-50 dark:bg-slate-700 rounded p-3">
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-medium px-2 py-0.5 bg-blue-100 text-blue-700 dark:bg-blue-900/30 dark:text-blue-400 rounded">
{traceability.chunk.regulation_code}
</span>
{traceability.chunk.article && (
<span className="text-xs text-gray-500 dark:text-gray-400">
Art. {traceability.chunk.article}
{traceability.chunk.paragraph && ` Abs. ${traceability.chunk.paragraph}`}
</span>
)}
</div>
<p className="text-sm text-gray-600 dark:text-gray-300 whitespace-pre-wrap">
{traceability.chunk.text}
</p>
{traceability.chunk.source_url && (
<a
href={traceability.chunk.source_url}
target="_blank"
rel="noopener noreferrer"
className="mt-2 inline-flex items-center gap-1 text-xs text-blue-600 hover:underline"
>
🔗 Quelle oeffnen
</a>
)}
</div>
</div>
{/* Arrow Down */}
<div className="flex justify-center">
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
{/* Requirements */}
<div className="border-l-4 border-orange-500 pl-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
📋 Extrahierte Anforderungen ({traceability.requirements.length})
</h4>
{traceability.requirements.length > 0 ? (
<div className="space-y-2">
{traceability.requirements.map((req, idx) => (
<div key={idx} className="bg-orange-50 dark:bg-orange-900/20 rounded p-3">
<div className="flex items-center gap-2 mb-1">
<span className="text-xs font-medium text-orange-700 dark:text-orange-400">
{req.category || 'Anforderung'}
</span>
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">{req.text}</p>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
Keine Anforderungen aus diesem Chunk extrahiert.
<br />
<span className="text-xs">(Requirements-Extraktion ist noch nicht implementiert)</span>
</p>
)}
</div>
{/* Arrow Down */}
<div className="flex justify-center">
<svg className="w-6 h-6 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 14l-7 7m0 0l-7-7m7 7V3" />
</svg>
</div>
{/* Controls */}
<div className="border-l-4 border-green-500 pl-4">
<h4 className="text-sm font-medium text-gray-700 dark:text-gray-300 mb-2">
Abgeleitete Controls ({traceability.controls.length})
</h4>
{traceability.controls.length > 0 ? (
<div className="space-y-2">
{traceability.controls.map((ctrl, idx) => (
<div key={idx} className="bg-green-50 dark:bg-green-900/20 rounded p-3">
<div className="font-medium text-sm text-green-700 dark:text-green-400 mb-1">
{ctrl.name}
</div>
<p className="text-sm text-gray-600 dark:text-gray-300">{ctrl.description}</p>
</div>
))}
</div>
) : (
<p className="text-sm text-gray-500 dark:text-gray-400 italic">
Keine Controls aus diesem Chunk abgeleitet.
<br />
<span className="text-xs">(Control-Ableitung ist noch nicht implementiert)</span>
</p>
)}
</div>
</div>
) : null}
</div>
</div>
)}
{/* Empty State */}
{!searching && searchResults.length === 0 && searchQuery && (
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-12 text-center">
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.172 16.172a4 4 0 015.656 0M9 10h.01M15 10h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Keine Ergebnisse gefunden
</h3>
<p className="text-gray-500 dark:text-gray-400">
Versuchen Sie einen anderen Suchbegriff oder waehlen Sie eine andere Regulierung.
</p>
</div>
)}
{/* Initial State */}
{!searching && searchResults.length === 0 && !searchQuery && (
<div className="bg-white dark:bg-slate-800 rounded-lg shadow p-12 text-center">
<svg className="w-16 h-16 mx-auto mb-4 text-gray-300 dark:text-gray-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
</svg>
<h3 className="text-lg font-medium text-gray-900 dark:text-white mb-2">
Bereit fuer Stichproben
</h3>
<p className="text-gray-500 dark:text-gray-400 max-w-md mx-auto">
Geben Sie einen Suchbegriff ein, um Chunks zu finden. Sie koennen nach Artikeln,
Paragraphen oder spezifischen Textpassagen suchen.
</p>
</div>
)}
{/* Audit Info */}
<div className="bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-lg p-4">
<h3 className="text-sm font-medium text-blue-800 dark:text-blue-400 mb-2">
Hinweise fuer Auditoren
</h3>
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1 list-disc list-inside">
<li>Die Suche ist semantisch - aehnliche Begriffe werden gefunden, auch wenn die exakte Formulierung abweicht</li>
<li>Jeder Chunk entspricht einem logischen Textabschnitt aus dem Originaldokument</li>
<li>Die Traceability zeigt, wie aus dem Originaltext Anforderungen und Controls abgeleitet wurden</li>
<li>Klicken Sie auf "Quelle oeffnen", um das Originaldokument zu pruefen</li>
</ul>
</div>
</div>
)
}

View File

@@ -0,0 +1,674 @@
'use client'
/**
* DSFA Document Manager
*
* Manages DSFA-related sources and documents for the RAG pipeline.
* Features:
* - View all registered DSFA sources with license info
* - Upload new documents
* - Trigger re-indexing
* - View corpus statistics
*/
import { useState, useEffect } from 'react'
import Link from 'next/link'
import {
ArrowLeft,
RefreshCw,
Upload,
FileText,
Database,
Scale,
ExternalLink,
ChevronDown,
ChevronUp,
Search,
Filter,
CheckCircle,
Clock,
AlertCircle,
BookOpen
} from 'lucide-react'
import {
DSFASource,
DSFACorpusStats,
DSFASourceStats,
DSFALicenseCode,
DSFA_LICENSE_LABELS,
DSFA_DOCUMENT_TYPE_LABELS
} from '@/lib/sdk/types'
// ============================================================================
// TYPES
// ============================================================================
interface APIError {
message: string
status?: number
}
// ============================================================================
// API FUNCTIONS
// ============================================================================
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://localhost:8086'
async function fetchSources(): Promise<DSFASource[]> {
try {
const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/sources`)
if (!response.ok) throw new Error('Failed to fetch sources')
return await response.json()
} catch {
// Return mock data for demo
return MOCK_SOURCES
}
}
async function fetchStats(): Promise<DSFACorpusStats> {
try {
const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/stats`)
if (!response.ok) throw new Error('Failed to fetch stats')
return await response.json()
} catch {
return MOCK_STATS
}
}
async function initializeCorpus(): Promise<{ sources_registered: number }> {
const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/init`, {
method: 'POST',
})
if (!response.ok) throw new Error('Failed to initialize corpus')
return await response.json()
}
async function triggerIngestion(sourceCode: string): Promise<void> {
const response = await fetch(`${API_BASE}/api/v1/dsfa-rag/sources/${sourceCode}/ingest`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({}),
})
if (!response.ok) throw new Error('Failed to trigger ingestion')
}
// ============================================================================
// MOCK DATA
// ============================================================================
const MOCK_SOURCES: DSFASource[] = [
{
id: '1',
sourceCode: 'WP248',
name: 'WP248 rev.01 - Leitlinien zur DSFA',
fullName: 'Leitlinien zur Datenschutz-Folgenabschaetzung',
organization: 'Artikel-29-Datenschutzgruppe / EDPB',
sourceUrl: 'https://ec.europa.eu/newsroom/article29/items/611236/en',
licenseCode: 'EDPB-LICENSE',
licenseName: 'EDPB Document License',
attributionRequired: true,
attributionText: 'Quelle: WP248 rev.01, Artikel-29-Datenschutzgruppe (2017)',
documentType: 'guideline',
language: 'de',
},
{
id: '2',
sourceCode: 'DSK_KP5',
name: 'Kurzpapier Nr. 5 - DSFA nach Art. 35 DS-GVO',
organization: 'Datenschutzkonferenz (DSK)',
sourceUrl: 'https://www.datenschutzkonferenz-online.de/media/kp/dsk_kpnr_5.pdf',
licenseCode: 'DL-DE-BY-2.0',
licenseName: 'Datenlizenz DE Namensnennung 2.0',
licenseUrl: 'https://www.govdata.de/dl-de/by-2-0',
attributionRequired: true,
attributionText: 'Quelle: DSK Kurzpapier Nr. 5 (Stand: 2018)',
documentType: 'guideline',
language: 'de',
},
{
id: '3',
sourceCode: 'BFDI_MUSS_PUBLIC',
name: 'BfDI DSFA-Liste (oeffentlicher Bereich)',
organization: 'BfDI',
sourceUrl: 'https://www.bfdi.bund.de',
licenseCode: 'DL-DE-ZERO-2.0',
licenseName: 'Datenlizenz DE Zero 2.0',
attributionRequired: false,
attributionText: 'Quelle: BfDI, Liste gem. Art. 35 Abs. 4 DSGVO',
documentType: 'checklist',
language: 'de',
},
{
id: '4',
sourceCode: 'NI_MUSS_PRIVATE',
name: 'LfD NI DSFA-Liste (nicht-oeffentlich)',
organization: 'LfD Niedersachsen',
sourceUrl: 'https://www.lfd.niedersachsen.de/download/131098',
licenseCode: 'DL-DE-BY-2.0',
licenseName: 'Datenlizenz DE Namensnennung 2.0',
attributionRequired: true,
attributionText: 'Quelle: LfD Niedersachsen, DSFA-Muss-Liste',
documentType: 'checklist',
language: 'de',
},
]
const MOCK_STATS: DSFACorpusStats = {
sources: [
{
sourceId: '1',
sourceCode: 'WP248',
name: 'WP248 rev.01',
organization: 'EDPB',
licenseCode: 'EDPB-LICENSE',
documentType: 'guideline',
documentCount: 1,
chunkCount: 50,
lastIndexedAt: '2026-02-09T10:00:00Z',
},
{
sourceId: '2',
sourceCode: 'DSK_KP5',
name: 'DSK Kurzpapier Nr. 5',
organization: 'DSK',
licenseCode: 'DL-DE-BY-2.0',
documentType: 'guideline',
documentCount: 1,
chunkCount: 35,
lastIndexedAt: '2026-02-09T10:00:00Z',
},
],
totalSources: 45,
totalDocuments: 45,
totalChunks: 850,
qdrantCollection: 'bp_dsfa_corpus',
qdrantPointsCount: 850,
qdrantStatus: 'green',
}
// ============================================================================
// COMPONENTS
// ============================================================================
function LicenseBadge({ licenseCode }: { licenseCode: DSFALicenseCode }) {
const colorMap: Record<DSFALicenseCode, string> = {
'DL-DE-BY-2.0': 'bg-blue-100 text-blue-700 border-blue-200',
'DL-DE-ZERO-2.0': 'bg-gray-100 text-gray-700 border-gray-200',
'CC-BY-4.0': 'bg-green-100 text-green-700 border-green-200',
'EDPB-LICENSE': 'bg-purple-100 text-purple-700 border-purple-200',
'PUBLIC_DOMAIN': 'bg-gray-100 text-gray-600 border-gray-200',
'PROPRIETARY': 'bg-amber-100 text-amber-700 border-amber-200',
}
return (
<span className={`inline-flex items-center gap-1 px-2 py-0.5 rounded text-xs border ${colorMap[licenseCode] || 'bg-gray-100 text-gray-700 border-gray-200'}`}>
<Scale className="w-3 h-3" />
{DSFA_LICENSE_LABELS[licenseCode] || licenseCode}
</span>
)
}
function DocumentTypeBadge({ type }: { type?: string }) {
if (!type) return null
const colorMap: Record<string, string> = {
guideline: 'bg-indigo-100 text-indigo-700',
checklist: 'bg-emerald-100 text-emerald-700',
regulation: 'bg-red-100 text-red-700',
template: 'bg-orange-100 text-orange-700',
}
return (
<span className={`inline-flex items-center px-2 py-0.5 rounded text-xs ${colorMap[type] || 'bg-gray-100 text-gray-700'}`}>
{DSFA_DOCUMENT_TYPE_LABELS[type as keyof typeof DSFA_DOCUMENT_TYPE_LABELS] || type}
</span>
)
}
function StatusIndicator({ status }: { status: string }) {
const statusConfig: Record<string, { color: string; icon: React.ReactNode; label: string }> = {
green: { color: 'text-green-500', icon: <CheckCircle className="w-4 h-4" />, label: 'Aktiv' },
yellow: { color: 'text-yellow-500', icon: <Clock className="w-4 h-4" />, label: 'Ausstehend' },
red: { color: 'text-red-500', icon: <AlertCircle className="w-4 h-4" />, label: 'Fehler' },
}
const config = statusConfig[status] || statusConfig.yellow
return (
<span className={`inline-flex items-center gap-1 ${config.color}`}>
{config.icon}
<span className="text-sm">{config.label}</span>
</span>
)
}
function SourceCard({
source,
stats,
onIngest,
isIngesting
}: {
source: DSFASource
stats?: DSFASourceStats
onIngest: () => void
isIngesting: boolean
}) {
const [isExpanded, setIsExpanded] = useState(false)
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 overflow-hidden">
<div className="p-4">
<div className="flex items-start justify-between">
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1">
<span className="font-mono text-xs bg-gray-100 dark:bg-gray-700 px-2 py-0.5 rounded">
{source.sourceCode}
</span>
<DocumentTypeBadge type={source.documentType} />
</div>
<h3 className="font-semibold text-gray-900 dark:text-white truncate">
{source.name}
</h3>
{source.organization && (
<p className="text-sm text-gray-500 dark:text-gray-400">
{source.organization}
</p>
)}
</div>
<button
onClick={() => setIsExpanded(!isExpanded)}
className="p-1 hover:bg-gray-100 dark:hover:bg-gray-700 rounded"
>
{isExpanded ? (
<ChevronUp className="w-5 h-5 text-gray-400" />
) : (
<ChevronDown className="w-5 h-5 text-gray-400" />
)}
</button>
</div>
<div className="flex items-center gap-4 mt-3">
<LicenseBadge licenseCode={source.licenseCode} />
{stats && (
<>
<span className="text-sm text-gray-500">
{stats.documentCount} Dok.
</span>
<span className="text-sm text-gray-500">
{stats.chunkCount} Chunks
</span>
</>
)}
</div>
{source.attributionRequired && (
<div className="mt-3 p-2 bg-amber-50 dark:bg-amber-900/20 rounded text-xs text-amber-700 dark:text-amber-300">
<strong>Attribution:</strong> {source.attributionText}
</div>
)}
</div>
{isExpanded && (
<div className="border-t border-gray-200 dark:border-gray-700 p-4 bg-gray-50 dark:bg-gray-900">
<dl className="grid grid-cols-2 gap-3 text-sm">
{source.sourceUrl && (
<>
<dt className="text-gray-500">Quelle:</dt>
<dd>
<a
href={source.sourceUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1"
>
Link <ExternalLink className="w-3 h-3" />
</a>
</dd>
</>
)}
{source.licenseUrl && (
<>
<dt className="text-gray-500">Lizenz-URL:</dt>
<dd>
<a
href={source.licenseUrl}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 hover:underline flex items-center gap-1"
>
{source.licenseName} <ExternalLink className="w-3 h-3" />
</a>
</dd>
</>
)}
<dt className="text-gray-500">Sprache:</dt>
<dd className="uppercase">{source.language}</dd>
{stats?.lastIndexedAt && (
<>
<dt className="text-gray-500">Zuletzt indexiert:</dt>
<dd>{new Date(stats.lastIndexedAt).toLocaleString('de-DE')}</dd>
</>
)}
</dl>
<div className="mt-4 flex gap-2">
<button
onClick={onIngest}
disabled={isIngesting}
className="px-3 py-1.5 text-sm bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 flex items-center gap-1"
>
{isIngesting ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<RefreshCw className="w-4 h-4" />
)}
Neu indexieren
</button>
</div>
</div>
)}
</div>
)
}
function StatsOverview({ stats }: { stats: DSFACorpusStats }) {
return (
<div className="bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700 p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white flex items-center gap-2">
<Database className="w-5 h-5" />
Corpus-Statistik
</h2>
<StatusIndicator status={stats.qdrantStatus} />
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<div className="text-center p-4 bg-blue-50 dark:bg-blue-900/20 rounded-lg">
<p className="text-2xl font-bold text-blue-600 dark:text-blue-400">
{stats.totalSources}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Quellen</p>
</div>
<div className="text-center p-4 bg-emerald-50 dark:bg-emerald-900/20 rounded-lg">
<p className="text-2xl font-bold text-emerald-600 dark:text-emerald-400">
{stats.totalDocuments}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Dokumente</p>
</div>
<div className="text-center p-4 bg-purple-50 dark:bg-purple-900/20 rounded-lg">
<p className="text-2xl font-bold text-purple-600 dark:text-purple-400">
{stats.totalChunks.toLocaleString()}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Chunks</p>
</div>
<div className="text-center p-4 bg-orange-50 dark:bg-orange-900/20 rounded-lg">
<p className="text-2xl font-bold text-orange-600 dark:text-orange-400">
{stats.qdrantPointsCount.toLocaleString()}
</p>
<p className="text-sm text-gray-600 dark:text-gray-400">Vektoren</p>
</div>
</div>
<div className="mt-4 p-3 bg-gray-50 dark:bg-gray-900 rounded-lg">
<p className="text-sm text-gray-600 dark:text-gray-400">
<strong>Collection:</strong>{' '}
<code className="font-mono bg-gray-200 dark:bg-gray-700 px-1 rounded">
{stats.qdrantCollection}
</code>
</p>
</div>
</div>
)
}
// ============================================================================
// MAIN PAGE
// ============================================================================
export default function DSFADocumentManagerPage() {
const [sources, setSources] = useState<DSFASource[]>([])
const [stats, setStats] = useState<DSFACorpusStats | null>(null)
const [isLoading, setIsLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [searchQuery, setSearchQuery] = useState('')
const [filterType, setFilterType] = useState<string>('all')
const [ingestingSource, setIngestingSource] = useState<string | null>(null)
const [isInitializing, setIsInitializing] = useState(false)
useEffect(() => {
async function loadData() {
setIsLoading(true)
try {
const [sourcesData, statsData] = await Promise.all([
fetchSources(),
fetchStats(),
])
setSources(sourcesData)
setStats(statsData)
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to load data')
setSources(MOCK_SOURCES)
setStats(MOCK_STATS)
} finally {
setIsLoading(false)
}
}
loadData()
}, [])
const handleInitialize = async () => {
setIsInitializing(true)
try {
await initializeCorpus()
// Reload data
const [sourcesData, statsData] = await Promise.all([
fetchSources(),
fetchStats(),
])
setSources(sourcesData)
setStats(statsData)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to initialize')
} finally {
setIsInitializing(false)
}
}
const handleIngest = async (sourceCode: string) => {
setIngestingSource(sourceCode)
try {
await triggerIngestion(sourceCode)
// Reload stats
const statsData = await fetchStats()
setStats(statsData)
} catch (err) {
setError(err instanceof Error ? err.message : 'Failed to ingest')
} finally {
setIngestingSource(null)
}
}
// Filter sources
const filteredSources = sources.filter(source => {
const matchesSearch = searchQuery === '' ||
source.name.toLowerCase().includes(searchQuery.toLowerCase()) ||
source.sourceCode.toLowerCase().includes(searchQuery.toLowerCase()) ||
source.organization?.toLowerCase().includes(searchQuery.toLowerCase())
const matchesType = filterType === 'all' || source.documentType === filterType
return matchesSearch && matchesType
})
// Get stats by source code
const getStatsForSource = (sourceCode: string): DSFASourceStats | undefined => {
return stats?.sources.find(s => s.sourceCode === sourceCode)
}
return (
<div className="min-h-screen bg-gradient-to-br from-gray-50 to-gray-100 dark:from-gray-900 dark:to-gray-800 p-8">
<div className="max-w-6xl mx-auto">
{/* Header */}
<div className="mb-8">
<Link
href="/ai/rag-pipeline"
className="inline-flex items-center gap-2 text-gray-600 dark:text-gray-400 hover:text-gray-900 dark:hover:text-white mb-4"
>
<ArrowLeft className="w-4 h-4" />
Zurueck zur RAG-Pipeline
</Link>
<div className="flex justify-between items-start">
<div>
<h1 className="text-3xl font-bold text-gray-900 dark:text-white flex items-center gap-3">
<BookOpen className="w-8 h-8 text-blue-600" />
DSFA-Quellen Manager
</h1>
<p className="text-gray-500 dark:text-gray-400 mt-1">
Verwalten Sie DSFA-Guidance Dokumente mit vollstaendiger Lizenzattribution
</p>
</div>
<div className="flex gap-2">
<button
onClick={handleInitialize}
disabled={isInitializing}
className="px-4 py-2 bg-emerald-600 text-white rounded-lg hover:bg-emerald-700 disabled:opacity-50 flex items-center gap-2"
>
{isInitializing ? (
<RefreshCw className="w-4 h-4 animate-spin" />
) : (
<Database className="w-4 h-4" />
)}
Initialisieren
</button>
<button className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 flex items-center gap-2">
<Upload className="w-4 h-4" />
Dokument hochladen
</button>
</div>
</div>
</div>
{/* Error Banner */}
{error && (
<div className="mb-6 p-4 bg-red-50 dark:bg-red-900/30 border border-red-200 dark:border-red-700 rounded-xl">
<div className="flex items-center gap-3">
<AlertCircle className="w-5 h-5 text-red-500" />
<span className="text-red-800 dark:text-red-200">{error}</span>
<button
onClick={() => setError(null)}
className="ml-auto text-red-600 hover:text-red-800"
>
&times;
</button>
</div>
</div>
)}
{/* Stats Overview */}
{stats && <StatsOverview stats={stats} />}
{/* Search & Filter */}
<div className="mt-6 flex gap-4">
<div className="flex-1 relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-5 h-5 text-gray-400" />
<input
type="text"
placeholder="Quellen durchsuchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800"
/>
</div>
<div className="relative">
<Filter className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-gray-400" />
<select
value={filterType}
onChange={(e) => setFilterType(e.target.value)}
className="pl-9 pr-8 py-2 border border-gray-200 dark:border-gray-700 rounded-lg bg-white dark:bg-gray-800 appearance-none"
>
<option value="all">Alle Typen</option>
<option value="guideline">Leitlinien</option>
<option value="checklist">Prueflisten</option>
<option value="regulation">Verordnungen</option>
</select>
</div>
</div>
{/* Sources List */}
<div className="mt-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold text-gray-900 dark:text-white">
Registrierte Quellen ({filteredSources.length})
</h2>
</div>
{isLoading ? (
<div className="flex items-center justify-center py-20">
<div className="text-center">
<div className="w-12 h-12 border-4 border-blue-200 border-t-blue-600 rounded-full animate-spin mx-auto mb-4" />
<p className="text-gray-500 dark:text-gray-400">Lade Quellen...</p>
</div>
</div>
) : filteredSources.length === 0 ? (
<div className="text-center py-12 bg-white dark:bg-gray-800 rounded-xl border border-gray-200 dark:border-gray-700">
<FileText className="w-12 h-12 text-gray-400 mx-auto mb-4" />
<p className="text-gray-500 dark:text-gray-400">
{searchQuery || filterType !== 'all'
? 'Keine Quellen gefunden'
: 'Noch keine Quellen registriert'}
</p>
{!searchQuery && filterType === 'all' && (
<button
onClick={handleInitialize}
className="mt-4 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
>
Quellen initialisieren
</button>
)}
</div>
) : (
<div className="grid gap-4">
{filteredSources.map(source => (
<SourceCard
key={source.id}
source={source}
stats={getStatsForSource(source.sourceCode)}
onIngest={() => handleIngest(source.sourceCode)}
isIngesting={ingestingSource === source.sourceCode}
/>
))}
</div>
)}
</div>
{/* Info Box */}
<div className="mt-8 p-6 bg-blue-50 dark:bg-blue-900/20 border border-blue-200 dark:border-blue-800 rounded-xl">
<h3 className="font-semibold text-blue-900 dark:text-blue-100 mb-2">
Ueber die Lizenzattribution
</h3>
<p className="text-sm text-blue-800 dark:text-blue-200 mb-4">
Alle DSFA-Quellen werden mit vollstaendiger Lizenzinformation gespeichert.
Bei der Nutzung der RAG-Suche werden automatisch die korrekten Attributionen angezeigt.
</p>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3 text-sm">
<div className="flex items-center gap-2">
<LicenseBadge licenseCode="DL-DE-BY-2.0" />
<span className="text-blue-700 dark:text-blue-300">Namensnennung</span>
</div>
<div className="flex items-center gap-2">
<LicenseBadge licenseCode="DL-DE-ZERO-2.0" />
<span className="text-blue-700 dark:text-blue-300">Keine Attribution</span>
</div>
<div className="flex items-center gap-2">
<LicenseBadge licenseCode="CC-BY-4.0" />
<span className="text-blue-700 dark:text-blue-300">CC Attribution</span>
</div>
</div>
</div>
</div>
</div>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,52 @@
/**
* TypeScript Types for BQAS (Breakpilot Quality Assurance System)
*/
export interface TestResult {
test_id: string
test_name: string
passed: boolean
composite_score: number
intent_accuracy: number
faithfulness: number
relevance: number
coherence: number
safety: string
reasoning: string
expected_intent: string
detected_intent: string
}
export interface TestRun {
id: number
timestamp: string
git_commit: string
golden_score: number
synthetic_score: number
total_tests: number
passed_tests: number
failed_tests: number
duration_seconds: number
}
export interface BQASMetrics {
total_tests: number
passed_tests: number
failed_tests: number
avg_intent_accuracy: number
avg_faithfulness: number
avg_relevance: number
avg_coherence: number
safety_pass_rate: number
avg_composite_score: number
scores_by_intent: Record<string, number>
failed_test_ids: string[]
}
export interface TrendData {
dates: string[]
scores: number[]
trend: 'improving' | 'stable' | 'declining' | 'insufficient_data'
}
export type TabType = 'overview' | 'golden' | 'rag' | 'synthetic' | 'history' | 'guide'

View File

@@ -0,0 +1,152 @@
'use client'
/**
* Architecture Overview Page
*
* Central view of all backend modules and their connections.
* Helps track migration progress and ensure no modules are lost.
*/
import { useState } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import { ArchitectureView } from '@/components/common/ArchitectureView'
import { DataFlowDiagram } from '@/components/common/DataFlowDiagram'
import { getModuleStats } from '@/lib/module-registry'
type ViewMode = 'list' | 'diagram'
export default function ArchitecturePage() {
const [viewMode, setViewMode] = useState<ViewMode>('list')
const stats = getModuleStats()
return (
<div className="space-y-6">
<PagePurpose
title="Architektur-Uebersicht"
purpose="Zentrale Uebersicht aller Backend-Module und deren Verbindung zum Frontend. Dient zur Sicherstellung, dass bei der Migration keine Module verloren gehen."
audience={['Entwickler', 'DevOps', 'Architekten', 'Auditoren']}
architecture={{
services: ['consent-service', 'python-backend', 'klausur-service', 'voice-service'],
databases: ['PostgreSQL', 'Qdrant']
}}
relatedPages={[
{ name: 'Compliance Hub', href: '/sdk/compliance-hub', description: 'Compliance-Module' },
{ name: 'AI Hub', href: '/ai', description: 'KI-Module' },
]}
/>
{/* Summary Cards */}
<div className="grid grid-cols-4 gap-4">
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="text-sm text-slate-500 mb-1">Migrations-Fortschritt</div>
<div className="text-3xl font-bold text-purple-600">{stats.percentComplete}%</div>
<div className="text-sm text-slate-400">{stats.connected} von {stats.total} Modulen</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="text-sm text-slate-500 mb-1">Verbunden</div>
<div className="text-3xl font-bold text-green-600">{stats.connected}</div>
<div className="text-sm text-green-500">Vollstaendig migriert</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="text-sm text-slate-500 mb-1">Teilweise verbunden</div>
<div className="text-3xl font-bold text-yellow-600">{stats.partial}</div>
<div className="text-sm text-yellow-500">In Bearbeitung</div>
</div>
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="text-sm text-slate-500 mb-1">Nicht verbunden</div>
<div className="text-3xl font-bold text-red-600">{stats.notConnected}</div>
<div className="text-sm text-red-500">Noch zu migrieren</div>
</div>
</div>
{/* View Toggle */}
<div className="bg-white rounded-xl shadow-sm border p-4">
<div className="flex items-center gap-4">
<span className="text-sm font-medium text-slate-700">Ansicht:</span>
<div className="flex rounded-lg border border-slate-200 overflow-hidden">
<button
onClick={() => setViewMode('list')}
className={`px-4 py-2 text-sm font-medium transition-colors ${
viewMode === 'list'
? 'bg-purple-600 text-white'
: 'bg-white text-slate-600 hover:bg-slate-50'
}`}
>
Modul-Liste
</button>
<button
onClick={() => setViewMode('diagram')}
className={`px-4 py-2 text-sm font-medium transition-colors ${
viewMode === 'diagram'
? 'bg-purple-600 text-white'
: 'bg-white text-slate-600 hover:bg-slate-50'
}`}
>
Datenfluss-Diagramm
</button>
</div>
</div>
</div>
{/* Content based on view mode */}
{viewMode === 'list' ? (
<ArchitectureView showAllCategories />
) : (
<DataFlowDiagram />
)}
{/* Migration Checklist */}
<div className="bg-white rounded-xl shadow-sm border p-6">
<h3 className="text-lg font-semibold text-slate-900 mb-4">Migrations-Checkliste</h3>
<div className="space-y-3">
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Grundgeruest Admin v2 erstellt (Layout, Navigation)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Compliance Hub migriert (DSR, DSMS, VVT, TOM, DSFA, Controls, Evidence, Risks)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Consent Verwaltung migriert (inkl. Einwilligungen)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Workflow (Versionierung) migriert mit Sync-Scroll</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">KI-Module migriert (LLM Compare, RAG, AI Quality, Agents)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Infrastruktur-Module migriert (GPU, Security, SBOM, CI/CD, Middleware)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Communication-Module migriert (Mail, Alerts, Matrix, Video-Chat)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
<input type="checkbox" checked readOnly className="w-4 h-4 text-green-600" />
<span className="text-slate-700">Development-Module migriert (Brandbook, Content, Docs, Game, Unity)</span>
</div>
<div className="flex items-center gap-3 p-3 border border-yellow-200 bg-yellow-50 rounded-lg">
<input type="checkbox" className="w-4 h-4" />
<span className="text-slate-700">Klausur-Korrektur migrieren</span>
<span className="text-xs text-yellow-600 ml-auto">Bleibt vorerst im alten Admin</span>
</div>
<div className="flex items-center gap-3 p-3 border border-yellow-200 bg-yellow-50 rounded-lg">
<input type="checkbox" className="w-4 h-4" />
<span className="text-slate-700">OCR-Labeling migrieren</span>
<span className="text-xs text-yellow-600 ml-auto">Prioritaet: Mittel</span>
</div>
<div className="flex items-center gap-3 p-3 border border-slate-200 rounded-lg">
<input type="checkbox" className="w-4 h-4" />
<span className="text-slate-700">Verwaiste Module identifiziert (voice, training, multiplayer, pca-platform)</span>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,991 @@
'use client'
/**
* Production Readiness Backlog
*
* Comprehensive checklist of items needed before going live with BreakPilot
* Includes CI/CD, Security, RBAC, Data Protection, and Release Workflows
*
* Migrated from website/app/admin/backlog/page.tsx
* Updated: 2026-02-03
*/
import { useState, useEffect } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
import { metaModules } from '@/lib/navigation'
import {
ChevronRight,
Search,
Package,
RefreshCw,
Shield,
ClipboardCheck,
Users,
GitBranch,
Tag,
Database,
FileText,
CheckSquare,
AlertTriangle,
} from 'lucide-react'
interface BacklogItem {
id: string
title: string
description: string
category: string
priority: 'critical' | 'high' | 'medium' | 'low'
status: 'not_started' | 'in_progress' | 'review' | 'completed' | 'blocked'
assignee?: string
dueDate?: string
notes?: string
subtasks?: { id: string; title: string; completed: boolean }[]
}
interface BacklogCategory {
id: string
name: string
icon: React.ReactNode
color: string
bgColor: string
description: string
}
const categories: BacklogCategory[] = [
{
id: 'modules',
name: 'Module Progress',
icon: <Package className="w-5 h-5" />,
color: 'text-violet-700',
bgColor: 'bg-violet-100 border-violet-300',
description: 'Fertigstellungsgrad aller Services & Module',
},
{
id: 'cicd',
name: 'CI/CD Pipelines',
icon: <RefreshCw className="w-5 h-5" />,
color: 'text-blue-700',
bgColor: 'bg-blue-100 border-blue-300',
description: 'Build, Test & Deployment Automation',
},
{
id: 'security',
name: 'Security & Vulnerability',
icon: <Shield className="w-5 h-5" />,
color: 'text-red-700',
bgColor: 'bg-red-100 border-red-300',
description: 'Security Scans, Dependency Checks & Penetration Testing',
},
{
id: 'testing',
name: 'Testing & Quality',
icon: <ClipboardCheck className="w-5 h-5" />,
color: 'text-emerald-700',
bgColor: 'bg-emerald-100 border-emerald-300',
description: 'Unit Tests, Integration Tests & E2E Testing',
},
{
id: 'rbac',
name: 'RBAC & Access Control',
icon: <Users className="w-5 h-5" />,
color: 'text-purple-700',
bgColor: 'bg-purple-100 border-purple-300',
description: 'Developer Roles, Permissions & Team Management',
},
{
id: 'git',
name: 'Git & Branch Protection',
icon: <GitBranch className="w-5 h-5" />,
color: 'text-orange-700',
bgColor: 'bg-orange-100 border-orange-300',
description: 'Protected Branches, Merge Requests & Code Reviews',
},
{
id: 'release',
name: 'Release Management',
icon: <Tag className="w-5 h-5" />,
color: 'text-green-700',
bgColor: 'bg-green-100 border-green-300',
description: 'Versioning, Changelog & Release Notes',
},
{
id: 'data',
name: 'Data Protection',
icon: <Database className="w-5 h-5" />,
color: 'text-cyan-700',
bgColor: 'bg-cyan-100 border-cyan-300',
description: 'Backup, Migration & Customer Data Safety',
},
{
id: 'compliance',
name: 'Compliance & SBOM',
icon: <FileText className="w-5 h-5" />,
color: 'text-teal-700',
bgColor: 'bg-teal-100 border-teal-300',
description: 'SBOM, Lizenzen & Open Source Compliance',
},
{
id: 'approval',
name: 'Approval Workflow',
icon: <CheckSquare className="w-5 h-5" />,
color: 'text-indigo-700',
bgColor: 'bg-indigo-100 border-indigo-300',
description: 'Developer Approval, QA Sign-off & Release Gates',
},
]
// UPDATED: 2026-02-03 - Reflects actual project state
const initialBacklogItems: BacklogItem[] = [
// ==================== MODULE PROGRESS ====================
{
id: 'mod-1',
title: 'Consent Service (Go) - 90% fertig',
description: 'DSGVO Consent Management Microservice - Production Ready',
category: 'modules',
priority: 'high',
status: 'in_progress',
notes: 'Port 8081. Umfangreiche Tests. JWT Auth, OAuth 2.0, TOTP 2FA, DSR Workflow, Matrix/Jitsi Integration, Session Management, PII Redactor.',
subtasks: [
{ id: 'mod-1-1', title: 'Core Consent API (CRUD, Versioning)', completed: true },
{ id: 'mod-1-2', title: 'Authentication (JWT, OAuth 2.0, TOTP)', completed: true },
{ id: 'mod-1-3', title: 'DSR Workflow (Art. 15-21)', completed: true },
{ id: 'mod-1-4', title: 'Email Templates & Notifications', completed: true },
{ id: 'mod-1-5', title: 'Matrix/Jitsi Integration', completed: true },
{ id: 'mod-1-6', title: 'Session Management & Middleware', completed: true },
{ id: 'mod-1-7', title: 'PII Redactor & Security Headers', completed: true },
{ id: 'mod-1-8', title: 'Performance Tests (High-Load)', completed: false },
],
},
{
id: 'mod-2',
title: 'Admin-v2 Frontend (Next.js 15) - 95% fertig',
description: 'Neues Admin Dashboard - Feature Complete',
category: 'modules',
priority: 'critical',
status: 'completed',
notes: 'Port 3002. 73 Seiten, 154 Dateien, 50k+ Zeilen Code. Alle Module migriert.',
subtasks: [
{ id: 'mod-2-1', title: 'Layout mit Sidebar Navigation', completed: true },
{ id: 'mod-2-2', title: 'AI Module (Agents, RAG, Quality, LLM Compare)', completed: true },
{ id: 'mod-2-3', title: 'Compliance Module (AI Act, DSFA, Controls, Evidence)', completed: true },
{ id: 'mod-2-4', title: 'Communication Module (Mail, Matrix, Video-Chat, Alerts)', completed: true },
{ id: 'mod-2-5', title: 'DSGVO Module (Advisory Board, Consent, DSR, TOM, VVT)', completed: true },
{ id: 'mod-2-6', title: 'Infrastructure Module (CI/CD, GPU, SBOM, Security, Tests)', completed: true },
{ id: 'mod-2-7', title: 'Education Module (Edu-Search, Foerderantrag)', completed: true },
{ id: 'mod-2-8', title: 'Wizard Framework (Stepper, TestRunner, etc.)', completed: true },
{ id: 'mod-2-9', title: 'API Proxy Routes', completed: true },
{ id: 'mod-2-10', title: 'E2E Tests mit Playwright', completed: false },
],
},
{
id: 'mod-3',
title: 'Studio-v2 Frontend (Next.js 15) - 90% fertig',
description: 'Lehrer/Schueler Studio mit Apple Weather UI',
category: 'modules',
priority: 'high',
status: 'completed',
notes: 'Port 3001. 21 Seiten, 111 Dateien, 38k+ Zeilen. Experimental Dashboard, Korrektur, Geo-Lernwelt.',
subtasks: [
{ id: 'mod-3-1', title: 'Experimental Dashboard (Glassmorphism)', completed: true },
{ id: 'mod-3-2', title: 'Korrektur-System mit Fairness-Analyse', completed: true },
{ id: 'mod-3-3', title: 'Geo-Lernwelt (Maps, AOI)', completed: true },
{ id: 'mod-3-4', title: 'Voice Components', completed: true },
{ id: 'mod-3-5', title: 'Worksheet Editor', completed: true },
{ id: 'mod-3-6', title: 'Alerts & B2B Migration Wizard', completed: true },
{ id: 'mod-3-7', title: 'Document Upload & QR Code', completed: true },
{ id: 'mod-3-8', title: 'Messages & Meet Integration', completed: true },
],
},
{
id: 'mod-4',
title: 'Backend (Python FastAPI) - 85% fertig',
description: 'Hauptbackend mit umfangreichen Erweiterungen',
category: 'modules',
priority: 'critical',
status: 'in_progress',
notes: 'Port 8000. 238 Dateien, 94k+ Zeilen. Alerts Agent, Compliance, Classroom Engine, Game, Klausur.',
subtasks: [
{ id: 'mod-4-1', title: 'Alerts Agent (Rules, Digests, Actions)', completed: true },
{ id: 'mod-4-2', title: 'Compliance Module (AI Act, ISMS, Audit)', completed: true },
{ id: 'mod-4-3', title: 'Classroom Engine (FSM, Analytics, Timer)', completed: true },
{ id: 'mod-4-4', title: 'Game API (Learning Rules, Quiz)', completed: true },
{ id: 'mod-4-5', title: 'Klausur Backend (OCR, Correction)', completed: true },
{ id: 'mod-4-6', title: 'Unit API & Analytics', completed: true },
{ id: 'mod-4-7', title: 'Middleware (Rate Limiter, Security)', completed: true },
{ id: 'mod-4-8', title: 'Session Management (RBAC)', completed: true },
{ id: 'mod-4-9', title: 'Transcription Worker', completed: true },
{ id: 'mod-4-10', title: 'Alembic Migrations', completed: true },
{ id: 'mod-4-11', title: 'Integration Tests erweitern', completed: false },
],
},
{
id: 'mod-5',
title: 'Klausur Service (Python) - 85% fertig',
description: 'BYOEH Abitur-Klausurkorrektur System',
category: 'modules',
priority: 'critical',
status: 'in_progress',
notes: 'Port 8086. 45 Dateien, 20k+ Zeilen. BYOEH, Qdrant RAG, Embedding Service, Legal Corpus.',
subtasks: [
{ id: 'mod-5-1', title: 'BYOEH Upload & Encryption', completed: true },
{ id: 'mod-5-2', title: 'Key-Sharing zwischen Pruefern', completed: true },
{ id: 'mod-5-3', title: 'Qdrant RAG Integration', completed: true },
{ id: 'mod-5-4', title: 'Hybrid Search (Keyword + Semantic)', completed: true },
{ id: 'mod-5-5', title: 'Embedding Service', completed: true },
{ id: 'mod-5-6', title: 'Legal Corpus Ingestion', completed: true },
{ id: 'mod-5-7', title: 'PDF Export', completed: true },
{ id: 'mod-5-8', title: 'OCR Pipeline (TrOCR, Vision)', completed: true },
{ id: 'mod-5-9', title: 'Vocab Worksheet API', completed: true },
{ id: 'mod-5-10', title: 'KI-gestuetzte Korrektur verbessern', completed: false },
],
},
{
id: 'mod-6',
title: 'Agent-Core - 80% fertig',
description: 'Multi-Agent Architecture Framework',
category: 'modules',
priority: 'high',
status: 'in_progress',
notes: 'Neuer Service. Sessions, Shared Brain, Orchestrator, SOUL Files.',
subtasks: [
{ id: 'mod-6-1', title: 'Session Management & Heartbeat', completed: true },
{ id: 'mod-6-2', title: 'Checkpoint System', completed: true },
{ id: 'mod-6-3', title: 'Memory Store (mit TTL)', completed: true },
{ id: 'mod-6-4', title: 'Context Manager', completed: true },
{ id: 'mod-6-5', title: 'Knowledge Graph', completed: true },
{ id: 'mod-6-6', title: 'Message Bus (Pub/Sub)', completed: true },
{ id: 'mod-6-7', title: 'Supervisor & Task Router', completed: true },
{ id: 'mod-6-8', title: 'SOUL Files (Agent Personalities)', completed: true },
{ id: 'mod-6-9', title: 'Integration mit Voice Service', completed: false },
],
},
{
id: 'mod-7',
title: 'AI Compliance SDK (Go) - 75% fertig',
description: 'UCCA Obligations Framework',
category: 'modules',
priority: 'high',
status: 'in_progress',
notes: 'Neuer Go Service. AI Act, DSGVO, NIS2 Module. Policy Engine.',
subtasks: [
{ id: 'mod-7-1', title: 'UCCA Obligations Framework', completed: true },
{ id: 'mod-7-2', title: 'AI Act Module', completed: true },
{ id: 'mod-7-3', title: 'DSGVO Module', completed: true },
{ id: 'mod-7-4', title: 'NIS2 Module', completed: true },
{ id: 'mod-7-5', title: 'Policy Engine', completed: true },
{ id: 'mod-7-6', title: 'Legal RAG Integration', completed: true },
{ id: 'mod-7-7', title: 'Audit Trail & Export', completed: true },
{ id: 'mod-7-8', title: 'Escalation System', completed: false },
{ id: 'mod-7-9', title: 'Funding/Foerderantrag Wizard', completed: false },
],
},
{
id: 'mod-8',
title: 'Geo Service (Python) - 70% fertig',
description: 'Geographic Data Service fuer Geo-Lernwelt',
category: 'modules',
priority: 'medium',
status: 'in_progress',
notes: 'Neuer Service. AOI Packager, DEM Service, Tile Server.',
subtasks: [
{ id: 'mod-8-1', title: 'AOI Packager', completed: true },
{ id: 'mod-8-2', title: 'DEM Service', completed: true },
{ id: 'mod-8-3', title: 'OSM Extractor', completed: true },
{ id: 'mod-8-4', title: 'Tile Server', completed: true },
{ id: 'mod-8-5', title: 'Learning Generator', completed: true },
{ id: 'mod-8-6', title: 'License Checker', completed: true },
{ id: 'mod-8-7', title: 'Unity Integration', completed: false },
{ id: 'mod-8-8', title: 'Performance Optimization', completed: false },
],
},
{
id: 'mod-9',
title: 'Edu-Search Service (Go) - 65% fertig',
description: 'Educational Search mit Policy Engine',
category: 'modules',
priority: 'medium',
status: 'in_progress',
notes: 'Policy Handlers, Bundeslaender Policies, PII Detector.',
subtasks: [
{ id: 'mod-9-1', title: 'Policy Enforcer', completed: true },
{ id: 'mod-9-2', title: 'PII Detector', completed: true },
{ id: 'mod-9-3', title: 'Bundeslaender Policies', completed: true },
{ id: 'mod-9-4', title: 'German Universities Data', completed: true },
{ id: 'mod-9-5', title: 'Search API erweitern', completed: false },
{ id: 'mod-9-6', title: 'Caching Layer', completed: false },
],
},
// ==================== CI/CD PIPELINES ====================
{
id: 'cicd-1',
title: 'Woodpecker CI Setup',
description: 'Self-hosted CI/CD auf Mac Mini',
category: 'cicd',
priority: 'critical',
status: 'completed',
notes: 'Implementiert. Woodpecker CI laeuft auf macmini:8082. Pipelines fuer alle Services.',
subtasks: [
{ id: 'cicd-1-1', title: 'Woodpecker Server & Agent installiert', completed: true },
{ id: 'cicd-1-2', title: 'Gitea Integration', completed: true },
{ id: 'cicd-1-3', title: 'Docker Build Pipelines', completed: true },
{ id: 'cicd-1-4', title: 'Test Pipelines (Go, Python, Node)', completed: true },
],
},
{
id: 'cicd-2',
title: 'SBOM Generation Pipeline',
description: 'Automatische SBOM-Generierung in CI',
category: 'cicd',
priority: 'high',
status: 'completed',
notes: 'Implementiert in .gitea/workflows/sbom.yaml',
subtasks: [
{ id: 'cicd-2-1', title: 'CycloneDX SBOM Generation', completed: true },
{ id: 'cicd-2-2', title: 'Artifact Upload', completed: true },
{ id: 'cicd-2-3', title: 'SBOM Viewer in Admin', completed: true },
],
},
{
id: 'cicd-3',
title: 'Production Deployment Pipeline',
description: 'Kontrolliertes Deployment mit Rollback',
category: 'cicd',
priority: 'critical',
status: 'not_started',
subtasks: [
{ id: 'cicd-3-1', title: 'Blue-Green oder Canary Strategy', completed: false },
{ id: 'cicd-3-2', title: 'Automatischer Rollback', completed: false },
{ id: 'cicd-3-3', title: 'Health Checks nach Deploy', completed: false },
{ id: 'cicd-3-4', title: 'Deployment Notifications', completed: false },
],
},
// ==================== SECURITY ====================
{
id: 'sec-1',
title: 'Dependency Vulnerability Scanning',
description: 'Automatische Pruefung auf Schwachstellen',
category: 'security',
priority: 'critical',
status: 'completed',
notes: 'Dependabot konfiguriert fuer Go, Python, npm, Docker.',
subtasks: [
{ id: 'sec-1-1', title: 'Dependabot fuer Go', completed: true },
{ id: 'sec-1-2', title: 'Dependabot fuer Python', completed: true },
{ id: 'sec-1-3', title: 'Dependabot fuer npm', completed: true },
{ id: 'sec-1-4', title: 'Block Merge bei kritischen CVEs', completed: true },
],
},
{
id: 'sec-2',
title: 'Container Image Scanning',
description: 'Trivy Scans fuer alle Docker Images',
category: 'security',
priority: 'high',
status: 'completed',
notes: 'Trivy in CI integriert.',
subtasks: [
{ id: 'sec-2-1', title: 'Trivy Integration', completed: true },
{ id: 'sec-2-2', title: 'Base Image Policy', completed: true },
{ id: 'sec-2-3', title: 'Scan Report bei Build', completed: true },
],
},
{
id: 'sec-3',
title: 'SAST (Static Application Security Testing)',
description: 'Code-Analyse auf Sicherheitsluecken',
category: 'security',
priority: 'high',
status: 'completed',
notes: 'Gosec, Bandit, npm audit in CI.',
subtasks: [
{ id: 'sec-3-1', title: 'Gosec fuer Go', completed: true },
{ id: 'sec-3-2', title: 'Bandit fuer Python', completed: true },
{ id: 'sec-3-3', title: 'npm audit', completed: true },
{ id: 'sec-3-4', title: 'Semgrep Regeln', completed: false },
],
},
{
id: 'sec-4',
title: 'Secret Scanning',
description: 'Verhindern dass Secrets in Git landen',
category: 'security',
priority: 'critical',
status: 'completed',
notes: 'Gitleaks in CI. SSH Keys in .gitignore.',
subtasks: [
{ id: 'sec-4-1', title: 'Gitleaks Pre-commit', completed: true },
{ id: 'sec-4-2', title: 'SSH Keys in .gitignore', completed: true },
{ id: 'sec-4-3', title: 'Historische Commits gescannt', completed: true },
],
},
// ==================== TESTING ====================
{
id: 'test-1',
title: 'Backend Test Coverage erweitern',
description: 'Integration & E2E Tests fuer Backend APIs',
category: 'testing',
priority: 'high',
status: 'in_progress',
notes: '238 Backend-Dateien, davon 20+ Test-Dateien.',
subtasks: [
{ id: 'test-1-1', title: 'Alerts Agent Tests', completed: true },
{ id: 'test-1-2', title: 'Compliance API Tests', completed: true },
{ id: 'test-1-3', title: 'Classroom API Tests', completed: true },
{ id: 'test-1-4', title: 'Session Middleware Tests', completed: true },
{ id: 'test-1-5', title: 'Load Testing mit k6', completed: false },
],
},
{
id: 'test-2',
title: 'Frontend E2E Tests',
description: 'Playwright Tests fuer Admin-v2 und Studio-v2',
category: 'testing',
priority: 'critical',
status: 'not_started',
notes: 'Kritischer Mangel - keine E2E Tests!',
subtasks: [
{ id: 'test-2-1', title: 'Playwright Setup', completed: false },
{ id: 'test-2-2', title: 'Admin-v2 Critical Paths', completed: false },
{ id: 'test-2-3', title: 'Studio-v2 User Flows', completed: false },
{ id: 'test-2-4', title: 'Visual Regression', completed: false },
],
},
{
id: 'test-3',
title: 'Agent-Core Tests',
description: 'Unit Tests fuer Multi-Agent Framework',
category: 'testing',
priority: 'high',
status: 'completed',
notes: 'Umfangreiche Test-Suite vorhanden.',
subtasks: [
{ id: 'test-3-1', title: 'Session Manager Tests', completed: true },
{ id: 'test-3-2', title: 'Memory Store Tests', completed: true },
{ id: 'test-3-3', title: 'Message Bus Tests', completed: true },
{ id: 'test-3-4', title: 'Task Router Tests', completed: true },
{ id: 'test-3-5', title: 'Heartbeat Tests', completed: true },
],
},
// ==================== RBAC ====================
{
id: 'rbac-1',
title: 'Gitea Team Permissions',
description: 'Team-basierte Zugriffsrechte',
category: 'rbac',
priority: 'high',
status: 'not_started',
subtasks: [
{ id: 'rbac-1-1', title: 'Maintainers Team (Full Access)', completed: false },
{ id: 'rbac-1-2', title: 'Developers Team (Write)', completed: false },
{ id: 'rbac-1-3', title: 'Reviewers Team (Read + Review)', completed: false },
],
},
{
id: 'rbac-2',
title: 'Admin Panel Access Control',
description: 'Rollenbasierte Zugriffsrechte im Admin',
category: 'rbac',
priority: 'medium',
status: 'in_progress',
notes: 'RBAC Middleware im Backend implementiert.',
subtasks: [
{ id: 'rbac-2-1', title: 'RBAC Middleware', completed: true },
{ id: 'rbac-2-2', title: 'Session Store', completed: true },
{ id: 'rbac-2-3', title: 'Protected Routes', completed: true },
{ id: 'rbac-2-4', title: 'Admin Authentication UI', completed: false },
],
},
// ==================== GIT ====================
{
id: 'git-1',
title: 'Protected Branches Setup',
description: 'Schutz fuer main Branch',
category: 'git',
priority: 'critical',
status: 'not_started',
subtasks: [
{ id: 'git-1-1', title: 'No direct push to main', completed: false },
{ id: 'git-1-2', title: 'Require PR with Approval', completed: false },
{ id: 'git-1-3', title: 'Require Status Checks', completed: false },
],
},
{
id: 'git-2',
title: 'Alle Dateien committet',
description: 'Keine ungetrackten Produktionsdateien',
category: 'git',
priority: 'critical',
status: 'completed',
notes: 'Am 2026-02-03 bereinigt: ~870 Dateien, 329k Zeilen committet.',
subtasks: [
{ id: 'git-2-1', title: 'admin-v2 (154 Dateien)', completed: true },
{ id: 'git-2-2', title: 'studio-v2 (111 Dateien)', completed: true },
{ id: 'git-2-3', title: 'backend (238 Dateien)', completed: true },
{ id: 'git-2-4', title: 'website (120 Dateien)', completed: true },
{ id: 'git-2-5', title: 'klausur-service (45 Dateien)', completed: true },
{ id: 'git-2-6', title: 'consent-service (15 Dateien)', completed: true },
{ id: 'git-2-7', title: 'Neue Services (161 Dateien)', completed: true },
{ id: 'git-2-8', title: '.gitignore aktualisiert', completed: true },
],
},
// ==================== RELEASE ====================
{
id: 'rel-1',
title: 'Semantic Versioning',
description: 'Automatische Versionierung nach SemVer',
category: 'release',
priority: 'high',
status: 'not_started',
subtasks: [
{ id: 'rel-1-1', title: 'Conventional Commits', completed: false },
{ id: 'rel-1-2', title: 'Automatische Git Tags', completed: false },
{ id: 'rel-1-3', title: 'CHANGELOG Generation', completed: false },
],
},
// ==================== DATA ====================
{
id: 'data-1',
title: 'Database Backup Strategy',
description: 'Automatische Backups mit Retention',
category: 'data',
priority: 'critical',
status: 'not_started',
subtasks: [
{ id: 'data-1-1', title: 'Taegliche Backups', completed: false },
{ id: 'data-1-2', title: 'Point-in-Time Recovery', completed: false },
{ id: 'data-1-3', title: 'Backup Encryption', completed: false },
{ id: 'data-1-4', title: 'Restore Test dokumentieren', completed: false },
],
},
{
id: 'data-2',
title: 'Customer Data Protection',
description: 'Schutz von Stammdaten & Dokumenten',
category: 'data',
priority: 'critical',
status: 'in_progress',
subtasks: [
{ id: 'data-2-1', title: 'Encryption at Rest', completed: true },
{ id: 'data-2-2', title: 'Audit Log fuer Consent', completed: true },
{ id: 'data-2-3', title: 'PII Masking in Logs', completed: true },
{ id: 'data-2-4', title: 'Secure Document Storage', completed: false },
],
},
// ==================== COMPLIANCE ====================
{
id: 'sbom-1',
title: 'SBOM erstellt und dokumentiert',
description: 'Software Bill of Materials',
category: 'compliance',
priority: 'high',
status: 'completed',
notes: 'Umfassende SBOM in /admin/sbom verfuegbar.',
subtasks: [
{ id: 'sbom-1-1', title: 'Go Dependencies', completed: true },
{ id: 'sbom-1-2', title: 'Python Dependencies', completed: true },
{ id: 'sbom-1-3', title: 'npm Dependencies', completed: true },
{ id: 'sbom-1-4', title: 'Docker Base Images', completed: true },
{ id: 'sbom-1-5', title: 'CycloneDX Export', completed: true },
],
},
{
id: 'sbom-2',
title: 'Lizenz-Compliance',
description: 'Alle Lizenzen geprueft',
category: 'compliance',
priority: 'high',
status: 'in_progress',
subtasks: [
{ id: 'sbom-2-1', title: 'Lizenzen identifiziert', completed: true },
{ id: 'sbom-2-2', title: 'Kompatibilitaet geprueft', completed: false },
{ id: 'sbom-2-3', title: 'LICENSES.md erstellt', completed: false },
],
},
// ==================== APPROVAL ====================
{
id: 'appr-1',
title: 'Release Approval Gates',
description: 'Mehrstufige Freigabe',
category: 'approval',
priority: 'critical',
status: 'not_started',
subtasks: [
{ id: 'appr-1-1', title: 'QA Sign-off', completed: false },
{ id: 'appr-1-2', title: 'Security Review', completed: false },
{ id: 'appr-1-3', title: 'Product Owner Freigabe', completed: false },
],
},
{
id: 'appr-2',
title: 'Post-Deployment Verification',
description: 'Checks nach Deployment',
category: 'approval',
priority: 'high',
status: 'not_started',
subtasks: [
{ id: 'appr-2-1', title: 'Smoke Tests', completed: false },
{ id: 'appr-2-2', title: 'Error Rate Monitoring', completed: false },
{ id: 'appr-2-3', title: 'Rollback Kriterien', completed: false },
],
},
]
const statusLabels: Record<BacklogItem['status'], { label: string; color: string }> = {
not_started: { label: 'Nicht begonnen', color: 'bg-slate-100 text-slate-600' },
in_progress: { label: 'In Arbeit', color: 'bg-blue-100 text-blue-700' },
review: { label: 'In Review', color: 'bg-yellow-100 text-yellow-700' },
completed: { label: 'Abgeschlossen', color: 'bg-green-100 text-green-700' },
blocked: { label: 'Blockiert', color: 'bg-red-100 text-red-700' },
}
const priorityLabels: Record<BacklogItem['priority'], { label: string; color: string }> = {
critical: { label: 'Kritisch', color: 'bg-red-500 text-white' },
high: { label: 'Hoch', color: 'bg-orange-500 text-white' },
medium: { label: 'Mittel', color: 'bg-yellow-500 text-white' },
low: { label: 'Niedrig', color: 'bg-slate-500 text-white' },
}
export default function BacklogPage() {
const module = metaModules.find((m) => m.id === 'backlog')
const [items, setItems] = useState<BacklogItem[]>(initialBacklogItems)
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
const [selectedPriority, setSelectedPriority] = useState<string | null>(null)
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set())
const [searchQuery, setSearchQuery] = useState('')
// Load saved state from localStorage
useEffect(() => {
const saved = localStorage.getItem('backlogItems-v2')
if (saved) {
try {
setItems(JSON.parse(saved))
} catch (e) {
console.error('Failed to load backlog items:', e)
}
}
}, [])
// Save state to localStorage
useEffect(() => {
localStorage.setItem('backlogItems-v2', JSON.stringify(items))
}, [items])
const filteredItems = items.filter((item) => {
if (selectedCategory && item.category !== selectedCategory) return false
if (selectedPriority && item.priority !== selectedPriority) return false
if (searchQuery) {
const query = searchQuery.toLowerCase()
return (
item.title.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query) ||
item.subtasks?.some((st) => st.title.toLowerCase().includes(query))
)
}
return true
})
const toggleExpand = (id: string) => {
const newExpanded = new Set(expandedItems)
if (newExpanded.has(id)) {
newExpanded.delete(id)
} else {
newExpanded.add(id)
}
setExpandedItems(newExpanded)
}
const updateItemStatus = (id: string, status: BacklogItem['status']) => {
setItems(items.map((item) => (item.id === id ? { ...item, status } : item)))
}
const toggleSubtask = (itemId: string, subtaskId: string) => {
setItems(
items.map((item) => {
if (item.id !== itemId) return item
return {
...item,
subtasks: item.subtasks?.map((st) =>
st.id === subtaskId ? { ...st, completed: !st.completed } : st
),
}
})
)
}
const getProgress = () => {
const total = items.length
const completed = items.filter((i) => i.status === 'completed').length
return { total, completed, percentage: Math.round((completed / total) * 100) }
}
const getCategoryProgress = (categoryId: string) => {
const categoryItems = items.filter((i) => i.category === categoryId)
const completed = categoryItems.filter((i) => i.status === 'completed').length
return { total: categoryItems.length, completed }
}
const resetToDefaults = () => {
if (confirm('Backlog auf Standardwerte zuruecksetzen? Alle lokalen Aenderungen gehen verloren.')) {
setItems(initialBacklogItems)
localStorage.removeItem('backlogItems-v2')
}
}
const progress = getProgress()
return (
<div className="space-y-6">
{module && (
<PagePurpose
title={module.name}
purpose={module.purpose}
audience={module.audience}
collapsible={true}
defaultCollapsed={true}
/>
)}
{/* Overall Progress */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-semibold text-slate-900">Gesamtfortschritt</h2>
<p className="text-sm text-slate-500">
{progress.completed} von {progress.total} Aufgaben abgeschlossen
</p>
</div>
<div className="flex items-center gap-4">
<button
onClick={resetToDefaults}
className="text-sm text-slate-500 hover:text-slate-700"
>
Zuruecksetzen
</button>
<div className="text-3xl font-bold text-blue-600">{progress.percentage}%</div>
</div>
</div>
<div className="w-full bg-slate-200 rounded-full h-3">
<div
className="bg-blue-600 h-3 rounded-full transition-all duration-500"
style={{ width: `${progress.percentage}%` }}
/>
</div>
</div>
{/* Category Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-3">
{categories.map((cat) => {
const catProgress = getCategoryProgress(cat.id)
return (
<button
key={cat.id}
onClick={() => setSelectedCategory(selectedCategory === cat.id ? null : cat.id)}
className={`p-3 rounded-xl border-2 text-left transition-all ${
selectedCategory === cat.id
? `${cat.bgColor} ring-2 ring-offset-2`
: 'bg-white border-slate-200 hover:border-slate-300'
}`}
>
<div className="flex items-center gap-2 mb-1">
<span className={selectedCategory === cat.id ? cat.color : 'text-slate-500'}>
{cat.icon}
</span>
<span className="font-medium text-xs truncate">{cat.name}</span>
</div>
<div className="text-xs text-slate-500">
{catProgress.completed}/{catProgress.total}
</div>
</button>
)
})}
</div>
{/* Filters & Search */}
<div className="flex flex-wrap gap-4">
<div className="flex-1 min-w-[200px] relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-slate-400" />
<input
type="text"
placeholder="Suchen..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
/>
</div>
<select
value={selectedPriority || ''}
onChange={(e) => setSelectedPriority(e.target.value || null)}
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500"
>
<option value="">Alle Prioritaeten</option>
<option value="critical">Kritisch</option>
<option value="high">Hoch</option>
<option value="medium">Mittel</option>
<option value="low">Niedrig</option>
</select>
{(selectedCategory || selectedPriority || searchQuery) && (
<button
onClick={() => {
setSelectedCategory(null)
setSelectedPriority(null)
setSearchQuery('')
}}
className="px-4 py-2 text-sm text-slate-600 hover:text-slate-900"
>
Filter zuruecksetzen
</button>
)}
</div>
{/* Backlog Items */}
<div className="space-y-3">
{filteredItems.map((item) => {
const category = categories.find((c) => c.id === item.category)
const isExpanded = expandedItems.has(item.id)
const completedSubtasks = item.subtasks?.filter((st) => st.completed).length || 0
const totalSubtasks = item.subtasks?.length || 0
return (
<div
key={item.id}
className="bg-white rounded-xl border border-slate-200 overflow-hidden"
>
<div
className="p-4 cursor-pointer hover:bg-slate-50 transition-colors"
onClick={() => toggleExpand(item.id)}
>
<div className="flex items-start gap-3">
{/* Expand Icon */}
<button className="mt-1 text-slate-400">
<ChevronRight
className={`w-5 h-5 transition-transform ${isExpanded ? 'rotate-90' : ''}`}
/>
</button>
{/* Content */}
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 mb-1 flex-wrap">
<h3 className="font-semibold text-slate-900">{item.title}</h3>
<span
className={`px-2 py-0.5 rounded text-xs font-medium ${
priorityLabels[item.priority].color
}`}
>
{priorityLabels[item.priority].label}
</span>
</div>
<p className="text-sm text-slate-500 mb-2">{item.description}</p>
{item.notes && (
<p className="text-xs text-slate-400 mb-2 italic">{item.notes}</p>
)}
<div className="flex items-center gap-3 text-xs">
<span className={`px-2 py-1 rounded border ${category?.bgColor}`}>
{category?.name}
</span>
{totalSubtasks > 0 && (
<span className="text-slate-500">
{completedSubtasks}/{totalSubtasks} Teilaufgaben
</span>
)}
</div>
</div>
{/* Status */}
<select
value={item.status}
onChange={(e) => {
e.stopPropagation()
updateItemStatus(item.id, e.target.value as BacklogItem['status'])
}}
onClick={(e) => e.stopPropagation()}
className={`px-3 py-1.5 rounded-lg text-sm font-medium border-0 cursor-pointer ${
statusLabels[item.status].color
}`}
>
{Object.entries(statusLabels).map(([value, { label }]) => (
<option key={value} value={value}>
{label}
</option>
))}
</select>
</div>
{/* Progress Bar */}
{totalSubtasks > 0 && (
<div className="mt-3 ml-8">
<div className="w-full bg-slate-200 rounded-full h-1.5">
<div
className="bg-green-500 h-1.5 rounded-full transition-all"
style={{ width: `${(completedSubtasks / totalSubtasks) * 100}%` }}
/>
</div>
</div>
)}
</div>
{/* Expanded Subtasks */}
{isExpanded && item.subtasks && item.subtasks.length > 0 && (
<div className="border-t border-slate-200 bg-slate-50 p-4 pl-12">
<h4 className="text-sm font-medium text-slate-700 mb-3">Teilaufgaben</h4>
<ul className="space-y-2">
{item.subtasks.map((subtask) => (
<li key={subtask.id} className="flex items-center gap-3">
<input
type="checkbox"
checked={subtask.completed}
onChange={() => toggleSubtask(item.id, subtask.id)}
className="w-4 h-4 rounded border-slate-300 text-blue-600 focus:ring-blue-500"
/>
<span
className={`text-sm ${
subtask.completed ? 'text-slate-400 line-through' : 'text-slate-700'
}`}
>
{subtask.title}
</span>
</li>
))}
</ul>
</div>
)}
</div>
)
})}
</div>
{filteredItems.length === 0 && (
<div className="text-center py-12 text-slate-500">
Keine Aufgaben gefunden. Versuche einen anderen Filter.
</div>
)}
{/* Info Box */}
<div className="bg-amber-50 border border-amber-200 rounded-xl p-6">
<div className="flex items-start gap-4">
<AlertTriangle className="w-6 h-6 text-amber-600 flex-shrink-0 mt-0.5" />
<div>
<h3 className="font-semibold text-amber-900">Wichtiger Hinweis</h3>
<p className="text-sm text-amber-800 mt-1">
Diese Backlog-Liste muss vollstaendig abgearbeitet sein, bevor BreakPilot in den
Produktivbetrieb gehen kann. Alle kritischen Items muessen abgeschlossen sein. Der
Fortschritt wird lokal im Browser gespeichert und kann mit &quot;Zuruecksetzen&quot;
auf die Standardwerte zurueckgesetzt werden.
</p>
<p className="text-xs text-amber-700 mt-2">
Letzte Aktualisierung: 2026-02-03
</p>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,912 @@
'use client'
/**
* Alerts Monitoring Admin Page (migrated from website/admin/alerts)
*
* Google Alerts & Feed-Ueberwachung Dashboard
* Provides inbox management, topic configuration, rule builder, and relevance profiles
*/
import { useEffect, useState, useCallback } from 'react'
import { PagePurpose } from '@/components/common/PagePurpose'
// Types
interface AlertItem {
id: string
title: string
url: string
snippet: string
topic_name: string
relevance_score: number | null
relevance_decision: string | null
status: string
fetched_at: string
published_at: string | null
matched_rule: string | null
tags: string[]
}
interface Topic {
id: string
name: string
feed_url: string
feed_type: string
is_active: boolean
fetch_interval_minutes: number
last_fetched_at: string | null
alert_count: number
}
interface Rule {
id: string
name: string
topic_id: string | null
conditions: Array<{
field: string
operator: string
value: string | number
}>
action_type: string
action_config: Record<string, unknown>
priority: number
is_active: boolean
}
interface Profile {
priorities: string[]
exclusions: string[]
positive_examples: Array<{ title: string; url: string }>
negative_examples: Array<{ title: string; url: string }>
policies: {
keep_threshold: number
drop_threshold: number
}
}
interface Stats {
total_alerts: number
new_alerts: number
kept_alerts: number
review_alerts: number
dropped_alerts: number
total_topics: number
active_topics: number
total_rules: number
}
// Tab type
type TabId = 'dashboard' | 'inbox' | 'topics' | 'rules' | 'profile' | 'audit' | 'documentation'
export default function AlertsPage() {
const [activeTab, setActiveTab] = useState<TabId>('dashboard')
const [stats, setStats] = useState<Stats | null>(null)
const [alerts, setAlerts] = useState<AlertItem[]>([])
const [topics, setTopics] = useState<Topic[]>([])
const [rules, setRules] = useState<Rule[]>([])
const [profile, setProfile] = useState<Profile | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const [inboxFilter, setInboxFilter] = useState<string>('all')
const API_BASE = '/api/alerts'
const fetchData = useCallback(async () => {
try {
const [statsRes, alertsRes, topicsRes, rulesRes, profileRes] = await Promise.all([
fetch(`${API_BASE}/stats`),
fetch(`${API_BASE}/inbox?limit=50`),
fetch(`${API_BASE}/topics`),
fetch(`${API_BASE}/rules`),
fetch(`${API_BASE}/profile`),
])
if (statsRes.ok) setStats(await statsRes.json())
if (alertsRes.ok) {
const data = await alertsRes.json()
setAlerts(data.items || [])
}
if (topicsRes.ok) {
const data = await topicsRes.json()
setTopics(data.topics || data.items || [])
}
if (rulesRes.ok) {
const data = await rulesRes.json()
setRules(data.rules || data.items || [])
}
if (profileRes.ok) setProfile(await profileRes.json())
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Verbindungsfehler')
// Set demo data
setStats({
total_alerts: 147,
new_alerts: 23,
kept_alerts: 89,
review_alerts: 12,
dropped_alerts: 23,
total_topics: 5,
active_topics: 4,
total_rules: 8,
})
setAlerts([
{
id: 'demo_1',
title: 'Neue Studie zur digitalen Bildung an Schulen',
url: 'https://example.com/artikel1',
snippet: 'Eine aktuelle Studie zeigt, dass digitale Lernmittel den Lernerfolg steigern koennen...',
topic_name: 'Digitale Bildung',
relevance_score: 0.85,
relevance_decision: 'KEEP',
status: 'new',
fetched_at: new Date().toISOString(),
published_at: null,
matched_rule: null,
tags: ['bildung', 'digital'],
},
{
id: 'demo_2',
title: 'Inklusion: Fortbildungen fuer Lehrkraefte',
url: 'https://example.com/artikel2',
snippet: 'Das Kultusministerium bietet neue Fortbildungsangebote zum Thema Inklusion an...',
topic_name: 'Inklusion',
relevance_score: 0.72,
relevance_decision: 'KEEP',
status: 'new',
fetched_at: new Date(Date.now() - 3600000).toISOString(),
published_at: null,
matched_rule: null,
tags: ['inklusion'],
},
])
setTopics([
{
id: 'topic_1',
name: 'Digitale Bildung',
feed_url: 'https://google.com/alerts/feeds/123',
feed_type: 'rss',
is_active: true,
fetch_interval_minutes: 60,
last_fetched_at: new Date().toISOString(),
alert_count: 47,
},
{
id: 'topic_2',
name: 'Inklusion',
feed_url: 'https://google.com/alerts/feeds/456',
feed_type: 'rss',
is_active: true,
fetch_interval_minutes: 60,
last_fetched_at: new Date(Date.now() - 1800000).toISOString(),
alert_count: 32,
},
])
setRules([
{
id: 'rule_1',
name: 'Stellenanzeigen ausschliessen',
topic_id: null,
conditions: [{ field: 'title', operator: 'contains', value: 'Stellenangebot' }],
action_type: 'drop',
action_config: {},
priority: 10,
is_active: true,
},
])
setProfile({
priorities: ['Inklusion', 'digitale Bildung'],
exclusions: ['Stellenanzeigen', 'Werbung'],
positive_examples: [],
negative_examples: [],
policies: { keep_threshold: 0.7, drop_threshold: 0.3 },
})
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
}, [fetchData])
const formatTimeAgo = (dateStr: string | null) => {
if (!dateStr) return '-'
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
if (diffMins < 1) return 'gerade eben'
if (diffMins < 60) return `vor ${diffMins} Min.`
if (diffMins < 1440) return `vor ${Math.floor(diffMins / 60)} Std.`
return `vor ${Math.floor(diffMins / 1440)} Tagen`
}
const getScoreBadge = (score: number | null) => {
if (score === null) return null
const pct = Math.round(score * 100)
let cls = 'bg-slate-100 text-slate-600'
if (pct >= 70) cls = 'bg-green-100 text-green-800'
else if (pct >= 40) cls = 'bg-amber-100 text-amber-800'
else cls = 'bg-red-100 text-red-800'
return <span className={`px-2 py-0.5 rounded text-xs font-semibold ${cls}`}>{pct}%</span>
}
const getDecisionBadge = (decision: string | null) => {
if (!decision) return null
const styles: Record<string, string> = {
KEEP: 'bg-green-100 text-green-800',
REVIEW: 'bg-amber-100 text-amber-800',
DROP: 'bg-red-100 text-red-800',
}
return (
<span className={`px-2 py-0.5 rounded text-xs font-semibold uppercase ${styles[decision] || 'bg-slate-100'}`}>
{decision}
</span>
)
}
const filteredAlerts = alerts.filter((alert) => {
if (inboxFilter === 'all') return true
if (inboxFilter === 'new') return alert.status === 'new'
if (inboxFilter === 'keep') return alert.relevance_decision === 'KEEP'
if (inboxFilter === 'review') return alert.relevance_decision === 'REVIEW'
return true
})
const tabs: { id: TabId; label: string; badge?: number }[] = [
{ id: 'dashboard', label: 'Dashboard' },
{ id: 'inbox', label: 'Inbox', badge: stats?.new_alerts || 0 },
{ id: 'topics', label: 'Topics' },
{ id: 'rules', label: 'Regeln' },
{ id: 'profile', label: 'Profil' },
{ id: 'audit', label: 'Audit' },
{ id: 'documentation', label: 'Dokumentation' },
]
if (loading) {
return (
<div className="flex items-center justify-center h-64">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-green-600" />
</div>
)
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="Alerts Monitoring"
purpose="Google Alerts & Feed-Ueberwachung mit KI-gestuetzter Relevanzpruefung. Verwalten Sie Topics, konfigurieren Sie Filterregeln und nutzen Sie LLM-basiertes Scoring fuer automatische Kategorisierung."
audience={['Marketing', 'Admins', 'DSB']}
architecture={{
services: ['backend (FastAPI)', 'APScheduler', 'LLM Gateway'],
databases: ['PostgreSQL', 'Valkey Cache'],
}}
relatedPages={[
{ name: 'Unified Inbox', href: '/communication/mail', description: 'E-Mail-Konten verwalten' },
{ name: 'Voice Service', href: '/communication/matrix', description: 'Voice-First Interface' },
]}
collapsible={true}
defaultCollapsed={false}
/>
{/* Stats Overview */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-3xl font-bold text-slate-900">{stats?.total_alerts || 0}</div>
<div className="text-sm text-slate-500">Alerts gesamt</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-3xl font-bold text-blue-600">{stats?.new_alerts || 0}</div>
<div className="text-sm text-slate-500">Neue Alerts</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-3xl font-bold text-green-600">{stats?.kept_alerts || 0}</div>
<div className="text-sm text-slate-500">Relevant</div>
</div>
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className="text-3xl font-bold text-amber-600">{stats?.review_alerts || 0}</div>
<div className="text-sm text-slate-500">Zur Pruefung</div>
</div>
</div>
{/* Tab Navigation */}
<div className="bg-white rounded-lg shadow mb-6">
<div className="border-b border-slate-200 px-4">
<nav className="flex gap-4 overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`pb-3 pt-4 px-1 text-sm font-medium border-b-2 transition-colors flex items-center gap-2 whitespace-nowrap ${
activeTab === tab.id
? 'border-green-600 text-green-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
{tab.label}
{tab.badge !== undefined && tab.badge > 0 && (
<span className="px-2 py-0.5 rounded-full text-xs font-semibold bg-red-500 text-white">
{tab.badge}
</span>
)}
</button>
))}
</nav>
</div>
<div className="p-6">
{/* Dashboard Tab */}
{activeTab === 'dashboard' && (
<div className="space-y-6">
{/* Quick Actions */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-slate-50 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4">Aktive Topics</h3>
<div className="space-y-3">
{topics.slice(0, 5).map((topic) => (
<div key={topic.id} className="flex items-center justify-between p-3 bg-white rounded-lg border border-slate-200">
<div>
<div className="font-medium text-slate-900">{topic.name}</div>
<div className="text-xs text-slate-500">{topic.alert_count} Alerts</div>
</div>
<span className={`px-2 py-1 rounded text-xs font-semibold ${topic.is_active ? 'bg-green-100 text-green-800' : 'bg-slate-100 text-slate-600'}`}>
{topic.is_active ? 'Aktiv' : 'Pausiert'}
</span>
</div>
))}
{topics.length === 0 && (
<div className="text-sm text-slate-500 text-center py-4">Keine Topics konfiguriert</div>
)}
</div>
</div>
<div className="bg-slate-50 rounded-xl p-6">
<h3 className="font-semibold text-slate-900 mb-4">Letzte Alerts</h3>
<div className="space-y-3">
{alerts.slice(0, 5).map((alert) => (
<div key={alert.id} className="p-3 bg-white rounded-lg border border-slate-200">
<div className="font-medium text-slate-900 text-sm truncate">{alert.title}</div>
<div className="flex items-center gap-2 mt-1">
<span className="text-xs text-slate-500">{alert.topic_name}</span>
{getScoreBadge(alert.relevance_score)}
</div>
</div>
))}
{alerts.length === 0 && (
<div className="text-sm text-slate-500 text-center py-4">Keine Alerts vorhanden</div>
)}
</div>
</div>
</div>
{error && (
<div className="bg-amber-50 border border-amber-200 rounded-lg p-4">
<p className="text-sm text-amber-800">
<strong>Hinweis:</strong> API nicht erreichbar. Demo-Daten werden angezeigt.
</p>
</div>
)}
</div>
)}
{/* Inbox Tab */}
{activeTab === 'inbox' && (
<div className="space-y-4">
{/* Filters */}
<div className="flex gap-2 flex-wrap">
{['all', 'new', 'keep', 'review'].map((filter) => (
<button
key={filter}
onClick={() => setInboxFilter(filter)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
inboxFilter === filter
? 'bg-green-600 text-white'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{filter === 'all' && 'Alle'}
{filter === 'new' && 'Neu'}
{filter === 'keep' && 'Relevant'}
{filter === 'review' && 'Pruefung'}
</button>
))}
</div>
{/* Alerts Table */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<table className="w-full">
<thead className="bg-slate-50 border-b border-slate-200">
<tr>
<th className="text-left p-4 text-xs font-semibold text-slate-500 uppercase">Alert</th>
<th className="text-left p-4 text-xs font-semibold text-slate-500 uppercase">Topic</th>
<th className="text-left p-4 text-xs font-semibold text-slate-500 uppercase">Score</th>
<th className="text-left p-4 text-xs font-semibold text-slate-500 uppercase">Decision</th>
<th className="text-left p-4 text-xs font-semibold text-slate-500 uppercase">Zeit</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{filteredAlerts.map((alert) => (
<tr key={alert.id} className="hover:bg-slate-50">
<td className="p-4">
<a href={alert.url} target="_blank" rel="noopener noreferrer" className="font-medium text-slate-900 hover:text-green-600">
{alert.title}
</a>
<p className="text-sm text-slate-500 truncate max-w-md">{alert.snippet}</p>
</td>
<td className="p-4 text-sm text-slate-600">{alert.topic_name}</td>
<td className="p-4">{getScoreBadge(alert.relevance_score)}</td>
<td className="p-4">{getDecisionBadge(alert.relevance_decision)}</td>
<td className="p-4 text-sm text-slate-500">{formatTimeAgo(alert.fetched_at)}</td>
</tr>
))}
{filteredAlerts.length === 0 && (
<tr>
<td colSpan={5} className="p-8 text-center text-slate-500">
Keine Alerts gefunden
</td>
</tr>
)}
</tbody>
</table>
</div>
</div>
)}
{/* Topics Tab */}
{activeTab === 'topics' && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-semibold text-slate-900">Feed Topics</h3>
<button className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700">
+ Topic hinzufuegen
</button>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{topics.map((topic) => (
<div key={topic.id} className="bg-white rounded-xl border border-slate-200 p-4">
<div className="flex justify-between items-start mb-3">
<div className="w-10 h-10 bg-green-100 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 5c7.18 0 13 5.82 13 13M6 11a7 7 0 017 7m-6 0a1 1 0 11-2 0 1 1 0 012 0z" />
</svg>
</div>
<span className={`px-2 py-1 rounded text-xs font-semibold ${topic.is_active ? 'bg-green-100 text-green-800' : 'bg-slate-100 text-slate-600'}`}>
{topic.is_active ? 'Aktiv' : 'Pausiert'}
</span>
</div>
<h4 className="font-semibold text-slate-900">{topic.name}</h4>
<p className="text-sm text-slate-500 truncate">{topic.feed_url}</p>
<div className="flex justify-between items-center mt-4 pt-4 border-t border-slate-100">
<div className="text-sm">
<span className="font-semibold text-slate-900">{topic.alert_count}</span>
<span className="text-slate-500"> Alerts</span>
</div>
<div className="text-xs text-slate-500">
{formatTimeAgo(topic.last_fetched_at)}
</div>
</div>
</div>
))}
{topics.length === 0 && (
<div className="col-span-full text-center py-8 text-slate-500">
Keine Topics konfiguriert
</div>
)}
</div>
</div>
)}
{/* Rules Tab */}
{activeTab === 'rules' && (
<div className="space-y-4">
<div className="flex justify-between items-center">
<h3 className="font-semibold text-slate-900">Filterregeln</h3>
<button className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700">
+ Regel erstellen
</button>
</div>
<div className="bg-white rounded-xl border border-slate-200 divide-y divide-slate-100">
{rules.map((rule) => (
<div key={rule.id} className="p-4 flex items-center gap-4">
<div className="text-slate-400 cursor-grab">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6h16M4 12h16M4 18h16" />
</svg>
</div>
<div className="flex-1">
<div className="font-medium text-slate-900">{rule.name}</div>
<div className="text-sm text-slate-500">
Wenn: {rule.conditions[0]?.field} {rule.conditions[0]?.operator} &quot;{rule.conditions[0]?.value}&quot;
</div>
</div>
<span className={`px-3 py-1 rounded text-xs font-semibold uppercase ${
rule.action_type === 'keep' ? 'bg-green-100 text-green-800' :
rule.action_type === 'drop' ? 'bg-red-100 text-red-800' :
rule.action_type === 'email' ? 'bg-blue-100 text-blue-800' :
'bg-purple-100 text-purple-800'
}`}>
{rule.action_type}
</span>
<div
className={`w-12 h-6 rounded-full relative cursor-pointer transition-colors ${
rule.is_active ? 'bg-green-500' : 'bg-slate-300'
}`}
>
<div
className={`absolute w-5 h-5 bg-white rounded-full top-0.5 transition-all shadow ${
rule.is_active ? 'left-6' : 'left-0.5'
}`}
/>
</div>
</div>
))}
{rules.length === 0 && (
<div className="p-8 text-center text-slate-500">
Keine Regeln konfiguriert
</div>
)}
</div>
</div>
)}
{/* Profile Tab */}
{activeTab === 'profile' && (
<div className="max-w-2xl space-y-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Relevanzprofil</h3>
<div className="space-y-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Prioritaeten (wichtige Themen)
</label>
<textarea
className="w-full p-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
rows={4}
defaultValue={profile?.priorities?.join('\n') || ''}
placeholder="Ein Thema pro Zeile..."
/>
<p className="text-xs text-slate-500 mt-1">Alerts zu diesen Themen werden hoeher bewertet.</p>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Ausschluesse (unerwuenschte Themen)
</label>
<textarea
className="w-full p-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
rows={4}
defaultValue={profile?.exclusions?.join('\n') || ''}
placeholder="Ein Thema pro Zeile..."
/>
<p className="text-xs text-slate-500 mt-1">Alerts zu diesen Themen werden niedriger bewertet.</p>
</div>
<div className="grid grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Schwellenwert KEEP
</label>
<select
className="w-full p-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
defaultValue={profile?.policies?.keep_threshold || 0.7}
>
<option value={0.8}>80% (sehr streng)</option>
<option value={0.7}>70% (empfohlen)</option>
<option value={0.6}>60% (weniger streng)</option>
</select>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Schwellenwert DROP
</label>
<select
className="w-full p-3 border border-slate-200 rounded-lg text-sm focus:ring-2 focus:ring-green-500 focus:border-green-500"
defaultValue={profile?.policies?.drop_threshold || 0.3}
>
<option value={0.4}>40% (strenger)</option>
<option value={0.3}>30% (empfohlen)</option>
<option value={0.2}>20% (lockerer)</option>
</select>
</div>
</div>
<button className="px-4 py-2 bg-green-600 text-white rounded-lg text-sm font-medium hover:bg-green-700">
Profil speichern
</button>
</div>
</div>
</div>
)}
{/* Audit Tab */}
{activeTab === 'audit' && (
<div className="space-y-6">
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wide">Audit-relevante Informationen</h3>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{/* Database Info */}
<div className="bg-white rounded-xl border border-slate-200 p-4">
<h4 className="font-semibold text-slate-900 mb-3 flex items-center gap-2">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4" />
</svg>
Datenbank
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-600">Tabellen</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">4 (topics, items, rules, profiles)</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-600">Indizes</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">URL-Hash, Topic-ID, Status</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-sm text-slate-600">Backups</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">PostgreSQL pg_dump</span>
</div>
</div>
</div>
{/* API Security */}
<div className="bg-white rounded-xl border border-slate-200 p-4">
<h4 className="font-semibold text-slate-900 mb-3 flex items-center gap-2">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 15v2m-6 4h12a2 2 0 002-2v-6a2 2 0 00-2-2H6a2 2 0 00-2 2v6a2 2 0 002 2zm10-10V7a4 4 0 00-8 0v4h8z" />
</svg>
API Sicherheit
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-600">Authentifizierung</span>
<span className="text-sm font-medium bg-amber-100 text-amber-800 px-2 py-0.5 rounded">Bearer Token (geplant)</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-600">Rate Limiting</span>
<span className="text-sm font-medium bg-amber-100 text-amber-800 px-2 py-0.5 rounded">Nicht implementiert</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-sm text-slate-600">Input Validation</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">Pydantic Models</span>
</div>
</div>
</div>
{/* Logging */}
<div className="bg-white rounded-xl border border-slate-200 p-4">
<h4 className="font-semibold text-slate-900 mb-3 flex items-center gap-2">
<svg className="w-5 h-5 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17v-2m3 2v-4m3 4v-6m2 10H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Logging & Monitoring
</h4>
<div className="space-y-2">
<div className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-600">Structured Logging</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">Python logging</span>
</div>
<div className="flex items-center justify-between py-2 border-b border-slate-100">
<span className="text-sm text-slate-600">Metriken</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">Stats Endpoint</span>
</div>
<div className="flex items-center justify-between py-2">
<span className="text-sm text-slate-600">Health Checks</span>
<span className="text-sm font-medium bg-green-100 text-green-800 px-2 py-0.5 rounded">/api/alerts/health</span>
</div>
</div>
</div>
</div>
{/* Privacy Notes */}
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="text-sm font-semibold text-blue-800 mb-2">Datenschutz-Hinweise</h4>
<ul className="space-y-1">
<li className="text-sm text-blue-700 flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Alle Daten werden in Deutschland gespeichert (PostgreSQL)
</li>
<li className="text-sm text-blue-700 flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
Keine personenbezogenen Daten in Alerts (nur URLs und Snippets)
</li>
<li className="text-sm text-blue-700 flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
LLM-Verarbeitung kann on-premise mit Ollama/vLLM erfolgen
</li>
<li className="text-sm text-blue-700 flex items-start gap-2">
<svg className="w-4 h-4 mt-0.5 text-blue-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
DSGVO-konforme Datenverarbeitung
</li>
</ul>
</div>
</div>
)}
{/* Documentation Tab */}
{activeTab === 'documentation' && (
<div className="bg-white rounded-xl border border-slate-200 p-6 overflow-auto max-h-[calc(100vh-350px)]">
<div className="prose prose-slate max-w-none prose-headings:font-semibold prose-h1:text-2xl prose-h2:text-xl prose-h3:text-lg">
{/* Header */}
<div className="not-prose mb-8 pb-6 border-b border-slate-200">
<h1 className="text-2xl font-bold text-slate-900">BreakPilot Alerts Agent</h1>
<p className="text-sm text-slate-500 mt-1">Version: 1.0.0 | Stand: Januar 2026 | Autor: BreakPilot Development Team</p>
</div>
{/* Audit Box */}
<div className="not-prose bg-blue-50 border border-blue-200 rounded-lg p-4 mb-6">
<h3 className="font-semibold text-blue-900 mb-2">Audit-Relevante Informationen</h3>
<p className="text-sm text-blue-800">
Dieses Dokument dient als technische Dokumentation fuer das Alert-Monitoring-System der BreakPilot Plattform.
Es ist fuer Audits durch Bildungstraeger und Datenschutzbeauftragte konzipiert.
</p>
</div>
{/* Ziel des Systems */}
<h2>Ziel des Alert-Systems</h2>
<p>Das System ermoeglicht automatisierte Ueberwachung von Bildungsthemen mit:</p>
<ul>
<li><strong>Google Alerts Integration</strong>: RSS-Feeds von Google Alerts automatisch abrufen</li>
<li><strong>RSS/Atom Feeds</strong>: Beliebige Nachrichtenquellen einbinden</li>
<li><strong>KI-Relevanzpruefung</strong>: Automatische Bewertung der Relevanz durch LLM</li>
<li><strong>Regelbasierte Filterung</strong>: Flexible Regeln fuer automatische Sortierung</li>
<li><strong>Multi-Channel Actions</strong>: E-Mail, Webhook, Slack Benachrichtigungen</li>
<li><strong>Few-Shot Learning</strong>: Profil verbessert sich durch Nutzerfeedback</li>
</ul>
{/* Architecture Diagram */}
<h2>Systemarchitektur</h2>
<div className="not-prose bg-slate-900 rounded-lg p-4 overflow-x-auto">
<pre className="text-green-400 text-xs">{`
┌─────────────────────────────────────────────────────────────────────┐
│ BreakPilot Alerts Frontend │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐│
│ │ Dashboard │ │ Inbox │ │ Topics │ │ Profile ││
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────┘│
└───────────────────────────────┬─────────────────────────────────────┘
v
┌─────────────────────────────────────────────────────────────────────┐
│ Ingestion Layer │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ RSS Fetcher │ │ Email Parser │ │ APScheduler │ │
│ └───────┬────────┘ └───────┬────────┘ └───────┬────────┘ │
│ └───────────────────┼───────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Deduplication (URL-Hash + SimHash) │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
v
┌─────────────────────────────────────────────────────────────────────┐
│ Processing Layer │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ Rule Engine │ │
│ └──────────────────────────────────────────────────────┘ │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ LLM Relevance Scorer │ │
│ │ Output: { score, decision: KEEP/DROP/REVIEW } │ │
│ └──────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
v
┌─────────────────────────────────────────────────────────────────────┐
│ Action Layer │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ Email Action │ │ Webhook Action │ │ Slack Action │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
v
┌─────────────────────────────────────────────────────────────────────┐
│ Storage Layer │
│ ┌────────────────┐ ┌────────────────┐ ┌────────────────┐ │
│ │ PostgreSQL │ │ Valkey │ │ LLM Gateway │ │
│ └────────────────┘ └────────────────┘ └────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘`}</pre>
</div>
{/* API Endpoints */}
<h2>API Endpoints</h2>
<div className="not-prose overflow-x-auto">
<table className="min-w-full text-sm border border-slate-200 rounded-lg">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Endpoint</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Methode</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr><td className="px-4 py-2 font-mono text-xs">/api/alerts/inbox</td><td className="px-4 py-2">GET</td><td className="px-4 py-2 text-slate-600">Inbox Items abrufen</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">/api/alerts/ingest</td><td className="px-4 py-2">POST</td><td className="px-4 py-2 text-slate-600">Manuell Alert importieren</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">/api/alerts/topics</td><td className="px-4 py-2">GET/POST</td><td className="px-4 py-2 text-slate-600">Topics verwalten</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">/api/alerts/rules</td><td className="px-4 py-2">GET/POST</td><td className="px-4 py-2 text-slate-600">Regeln verwalten</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">/api/alerts/profile</td><td className="px-4 py-2">GET/PUT</td><td className="px-4 py-2 text-slate-600">Profil abrufen/aktualisieren</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">/api/alerts/stats</td><td className="px-4 py-2">GET</td><td className="px-4 py-2 text-slate-600">Statistiken abrufen</td></tr>
</tbody>
</table>
</div>
{/* Rule Engine */}
<h2>Rule Engine - Operatoren</h2>
<div className="not-prose overflow-x-auto">
<table className="min-w-full text-sm border border-slate-200 rounded-lg">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Operator</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Beschreibung</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Beispiel</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr><td className="px-4 py-2 font-mono text-xs">contains</td><td className="px-4 py-2">Text enthaelt</td><td className="px-4 py-2 text-slate-600">title contains &quot;Inklusion&quot;</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">not_contains</td><td className="px-4 py-2">Text enthaelt nicht</td><td className="px-4 py-2 text-slate-600">title not_contains &quot;Werbung&quot;</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">equals</td><td className="px-4 py-2">Exakte Uebereinstimmung</td><td className="px-4 py-2 text-slate-600">status equals &quot;new&quot;</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">regex</td><td className="px-4 py-2">Regulaerer Ausdruck</td><td className="px-4 py-2 text-slate-600">title regex &quot;\d&#123;4&#125;&quot;</td></tr>
<tr><td className="px-4 py-2 font-mono text-xs">gt / lt</td><td className="px-4 py-2">Groesser/Kleiner</td><td className="px-4 py-2 text-slate-600">relevance_score gt 0.8</td></tr>
</tbody>
</table>
</div>
{/* Scoring */}
<h2>LLM Relevanz-Scoring</h2>
<div className="not-prose overflow-x-auto">
<table className="min-w-full text-sm border border-slate-200 rounded-lg">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Entscheidung</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Score-Bereich</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Bedeutung</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr className="bg-green-50"><td className="px-4 py-2 font-semibold text-green-800">KEEP</td><td className="px-4 py-2">0.7 - 1.0</td><td className="px-4 py-2">Klar relevant, in Inbox anzeigen</td></tr>
<tr className="bg-amber-50"><td className="px-4 py-2 font-semibold text-amber-800">REVIEW</td><td className="px-4 py-2">0.4 - 0.7</td><td className="px-4 py-2">Unsicher, Nutzer entscheidet</td></tr>
<tr className="bg-red-50"><td className="px-4 py-2 font-semibold text-red-800">DROP</td><td className="px-4 py-2">0.0 - 0.4</td><td className="px-4 py-2">Irrelevant, automatisch archivieren</td></tr>
</tbody>
</table>
</div>
{/* Contact */}
<h2>Kontakt & Support</h2>
<div className="not-prose overflow-x-auto">
<table className="min-w-full text-sm border border-slate-200 rounded-lg">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Kontakt</th>
<th className="px-4 py-2 text-left font-semibold text-slate-700 border-b">Adresse</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr><td className="px-4 py-2">Technischer Support</td><td className="px-4 py-2">support@breakpilot.de</td></tr>
<tr><td className="px-4 py-2">Datenschutzbeauftragter</td><td className="px-4 py-2">dsb@breakpilot.de</td></tr>
<tr><td className="px-4 py-2">Dokumentation</td><td className="px-4 py-2">docs.breakpilot.de</td></tr>
</tbody>
</table>
</div>
{/* Footer */}
<div className="not-prose mt-8 pt-6 border-t border-slate-200 text-sm text-slate-500">
<p>Dokumentation erstellt: Januar 2026 | Version: 1.0.0</p>
</div>
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,946 @@
'use client'
/**
* Unified Inbox Mail Admin Page
* Migrated from website/admin/mail to admin-v2/communication/mail
*
* Admin interface for managing email accounts, viewing system status,
* and configuring AI analysis settings.
*/
import { useState, useEffect, useCallback } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
// API Base URL for backend operations (accounts, sync, etc.)
const API_BASE = process.env.NEXT_PUBLIC_KLAUSUR_SERVICE_URL || 'http://macmini:8086'
// Types
interface EmailAccount {
id: string
email: string
displayName: string
imapHost: string
imapPort: number
smtpHost: string
smtpPort: number
status: 'active' | 'inactive' | 'error' | 'syncing'
lastSync: string | null
emailCount: number
unreadCount: number
createdAt: string
}
interface MailStats {
totalAccounts: number
activeAccounts: number
totalEmails: number
unreadEmails: number
totalTasks: number
pendingTasks: number
overdueTasks: number
aiAnalyzedCount: number
lastSyncTime: string | null
}
interface SyncStatus {
running: boolean
accountsInProgress: string[]
lastCompleted: string | null
errors: string[]
}
// Tab definitions
type TabId = 'overview' | 'accounts' | 'ai-settings' | 'templates' | 'logs'
const tabs: { id: TabId; name: string }[] = [
{ id: 'overview', name: 'Uebersicht' },
{ id: 'accounts', name: 'Konten' },
{ id: 'ai-settings', name: 'KI-Einstellungen' },
{ id: 'templates', name: 'Vorlagen' },
{ id: 'logs', name: 'Audit-Log' },
]
// Main Component
export default function MailAdminPage() {
const [activeTab, setActiveTab] = useState<TabId>('overview')
const [stats, setStats] = useState<MailStats | null>(null)
const [accounts, setAccounts] = useState<EmailAccount[]>([])
const [syncStatus, setSyncStatus] = useState<SyncStatus | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const fetchData = useCallback(async () => {
try {
setLoading(true)
// Fetch stats via our proxy API (avoids CORS/mixed-content issues)
const response = await fetch('/api/admin/mail')
if (response.ok) {
const data = await response.json()
setStats(data.stats)
setAccounts(data.accounts)
setSyncStatus(data.syncStatus)
setError(null)
} else {
const errorData = await response.json().catch(() => ({}))
throw new Error(errorData.details || `API returned ${response.status}`)
}
} catch (err) {
console.error('Failed to fetch mail data:', err)
setError('Verbindung zum Mail-Service (Mailpit) fehlgeschlagen. Laeuft Mailpit auf Port 8025?')
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchData()
// Refresh every 10 seconds if syncing
const interval = setInterval(() => {
if (syncStatus?.running) {
fetchData()
}
}, 10000)
return () => clearInterval(interval)
}, [fetchData, syncStatus?.running])
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="Unified Inbox"
purpose="Verwalten Sie E-Mail-Konten, synchronisieren Sie Postfaecher und konfigurieren Sie die KI-gestuetzte E-Mail-Analyse fuer automatische Kategorisierung und Aufgabenerkennung."
audience={['Admins', 'Schulleitung']}
architecture={{
services: ['Mailpit (Dev Mail Catcher)', 'IMAP/SMTP Server (Prod)'],
databases: ['PostgreSQL', 'Vault (Credentials)'],
}}
relatedPages={[
{ name: 'Mail Wizard', href: '/communication/mail/wizard', description: 'Interaktives Setup und Testing' },
{ name: 'Voice Service', href: '/communication/matrix', description: 'Voice-First Interface' },
]}
collapsible={true}
defaultCollapsed={false}
/>
{/* Quick Link to Wizard */}
<div className="mb-6">
<Link
href="/communication/mail/wizard"
className="inline-flex items-center gap-2 px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
Mail Wizard starten
</Link>
</div>
{/* Error Banner */}
{error && (
<div className="mb-6 bg-red-50 border border-red-200 rounded-lg p-4 flex items-center gap-3">
<svg className="w-5 h-5 text-red-500 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<span className="text-red-700">{error}</span>
<button onClick={fetchData} className="ml-auto text-red-600 hover:text-red-800 text-sm font-medium">
Erneut versuchen
</button>
</div>
)}
{/* Tab Navigation */}
<div className="border-b border-slate-200 mb-6">
<nav className="-mb-px flex space-x-8">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`
flex items-center gap-2 py-4 px-1 border-b-2 font-medium text-sm transition-colors
${activeTab === tab.id
? 'border-blue-500 text-blue-600'
: 'border-transparent text-slate-500 hover:text-slate-700 hover:border-slate-300'
}
`}
>
{tab.name}
</button>
))}
</nav>
</div>
{/* Tab Content */}
{activeTab === 'overview' && (
<OverviewTab
stats={stats}
syncStatus={syncStatus}
loading={loading}
onRefresh={fetchData}
/>
)}
{activeTab === 'accounts' && (
<AccountsTab
accounts={accounts}
loading={loading}
onRefresh={fetchData}
/>
)}
{activeTab === 'ai-settings' && (
<AISettingsTab />
)}
{activeTab === 'templates' && (
<TemplatesTab />
)}
{activeTab === 'logs' && (
<AuditLogTab />
)}
</div>
)
}
// ============================================================================
// Overview Tab
// ============================================================================
function OverviewTab({
stats,
syncStatus,
loading,
onRefresh
}: {
stats: MailStats | null
syncStatus: SyncStatus | null
loading: boolean
onRefresh: () => void
}) {
const triggerSync = async () => {
try {
await fetch(`${API_BASE}/api/v1/mail/sync/all`, {
method: 'POST',
})
onRefresh()
} catch (err) {
console.error('Failed to trigger sync:', err)
}
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-900">System-Uebersicht</h2>
<p className="text-sm text-slate-500">Status aller E-Mail-Konten und Aufgaben</p>
</div>
<div className="flex gap-3">
<button
onClick={onRefresh}
className="px-4 py-2 text-sm font-medium text-slate-700 bg-white border border-slate-300 rounded-lg hover:bg-slate-50"
>
Aktualisieren
</button>
<button
onClick={triggerSync}
disabled={syncStatus?.running}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{syncStatus?.running ? 'Synchronisiert...' : 'Alle synchronisieren'}
</button>
</div>
</div>
{/* Loading State */}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)}
{/* Stats Grid */}
{!loading && stats && (
<>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<StatCard
title="E-Mail-Konten"
value={stats.totalAccounts}
subtitle={`${stats.activeAccounts} aktiv`}
color="blue"
/>
<StatCard
title="E-Mails gesamt"
value={stats.totalEmails}
subtitle={`${stats.unreadEmails} ungelesen`}
color="green"
/>
<StatCard
title="Aufgaben"
value={stats.totalTasks}
subtitle={`${stats.pendingTasks} offen`}
color="yellow"
/>
<StatCard
title="Ueberfaellig"
value={stats.overdueTasks}
color={stats.overdueTasks > 0 ? 'red' : 'green'}
/>
</div>
{/* Sync Status */}
<div className="bg-white rounded-lg border border-slate-200 p-6">
<h3 className="text-sm font-medium text-slate-700 mb-4">Synchronisierung</h3>
<div className="flex items-center gap-4">
{syncStatus?.running ? (
<>
<div className="w-3 h-3 bg-yellow-500 rounded-full animate-pulse"></div>
<span className="text-slate-600">
Synchronisiere {syncStatus.accountsInProgress.length} Konto(en)...
</span>
</>
) : (
<>
<div className="w-3 h-3 bg-green-500 rounded-full"></div>
<span className="text-slate-600">Bereit</span>
</>
)}
{stats.lastSyncTime && (
<span className="text-sm text-slate-500 ml-auto">
Letzte Sync: {new Date(stats.lastSyncTime).toLocaleString('de-DE')}
</span>
)}
</div>
{syncStatus?.errors && syncStatus.errors.length > 0 && (
<div className="mt-4 p-4 bg-red-50 rounded-lg">
<h4 className="text-sm font-medium text-red-800 mb-2">Fehler</h4>
<ul className="text-sm text-red-700 space-y-1">
{syncStatus.errors.slice(0, 3).map((error, i) => (
<li key={i}>{error}</li>
))}
</ul>
</div>
)}
</div>
{/* AI Stats */}
<div className="bg-white rounded-lg border border-slate-200 p-6">
<h3 className="text-sm font-medium text-slate-700 mb-4">KI-Analyse</h3>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div>
<p className="text-xs text-slate-500 uppercase tracking-wider">Analysiert</p>
<p className="text-2xl font-bold text-slate-900">{stats.aiAnalyzedCount}</p>
</div>
<div>
<p className="text-xs text-slate-500 uppercase tracking-wider">Analyse-Rate</p>
<p className="text-2xl font-bold text-slate-900">
{stats.totalEmails > 0
? `${Math.round((stats.aiAnalyzedCount / stats.totalEmails) * 100)}%`
: '0%'}
</p>
</div>
</div>
</div>
</>
)}
</div>
)
}
function StatCard({
title,
value,
subtitle,
color = 'blue'
}: {
title: string
value: number
subtitle?: string
color?: 'blue' | 'green' | 'yellow' | 'red'
}) {
const colorClasses = {
blue: 'text-blue-600',
green: 'text-green-600',
yellow: 'text-yellow-600',
red: 'text-red-600',
}
return (
<div className="bg-white rounded-lg border border-slate-200 p-6">
<p className="text-xs text-slate-500 uppercase tracking-wider mb-1">{title}</p>
<p className={`text-3xl font-bold ${colorClasses[color]}`}>{value.toLocaleString()}</p>
{subtitle && <p className="text-sm text-slate-500 mt-1">{subtitle}</p>}
</div>
)
}
// ============================================================================
// Accounts Tab
// ============================================================================
function AccountsTab({
accounts,
loading,
onRefresh
}: {
accounts: EmailAccount[]
loading: boolean
onRefresh: () => void
}) {
const [showAddModal, setShowAddModal] = useState(false)
const testConnection = async (accountId: string) => {
try {
const res = await fetch(`${API_BASE}/api/v1/mail/accounts/${accountId}/test`, {
method: 'POST',
})
if (res.ok) {
alert('Verbindung erfolgreich!')
} else {
alert('Verbindungsfehler')
}
} catch (err) {
alert('Verbindungsfehler')
}
}
const statusColors = {
active: 'bg-green-100 text-green-800',
inactive: 'bg-gray-100 text-gray-800',
error: 'bg-red-100 text-red-800',
syncing: 'bg-yellow-100 text-yellow-800',
}
const statusLabels = {
active: 'Aktiv',
inactive: 'Inaktiv',
error: 'Fehler',
syncing: 'Synchronisiert...',
}
return (
<div className="space-y-6">
{/* Header */}
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-900">E-Mail-Konten</h2>
<p className="text-sm text-slate-500">Verwalten Sie die verbundenen E-Mail-Konten</p>
</div>
<button
onClick={() => setShowAddModal(true)}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center gap-2"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Konto hinzufuegen
</button>
</div>
{/* Loading State */}
{loading && (
<div className="flex items-center justify-center py-12">
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
</div>
)}
{/* Accounts Grid */}
{!loading && (
<div className="grid gap-4">
{accounts.length === 0 ? (
<div className="bg-slate-50 rounded-lg p-8 text-center">
<svg className="w-12 h-12 text-slate-400 mx-auto mb-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
<h3 className="text-lg font-medium text-slate-900 mb-2">Keine E-Mail-Konten</h3>
<p className="text-slate-500 mb-4">Fuegen Sie Ihr erstes E-Mail-Konto hinzu.</p>
</div>
) : (
accounts.map((account) => (
<div
key={account.id}
className="bg-white rounded-lg border border-slate-200 p-6 hover:shadow-md transition-shadow"
>
<div className="flex items-start justify-between">
<div className="flex items-center gap-4">
<div className="w-12 h-12 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 className="text-lg font-semibold text-slate-900">
{account.displayName || account.email}
</h3>
<p className="text-sm text-slate-500">{account.email}</p>
</div>
</div>
<div className="flex items-center gap-3">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${statusColors[account.status]}`}>
{statusLabels[account.status]}
</span>
<button
onClick={() => testConnection(account.id)}
className="p-2 text-slate-400 hover:text-slate-600"
title="Verbindung testen"
>
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 10V3L4 14h7v7l9-11h-7z" />
</svg>
</button>
</div>
</div>
<div className="mt-4 grid grid-cols-2 md:grid-cols-4 gap-4">
<div>
<p className="text-xs text-slate-500 uppercase tracking-wider">E-Mails</p>
<p className="text-lg font-semibold text-slate-900">{account.emailCount}</p>
</div>
<div>
<p className="text-xs text-slate-500 uppercase tracking-wider">Ungelesen</p>
<p className="text-lg font-semibold text-slate-900">{account.unreadCount}</p>
</div>
<div>
<p className="text-xs text-slate-500 uppercase tracking-wider">IMAP</p>
<p className="text-sm font-mono text-slate-700">{account.imapHost}:{account.imapPort}</p>
</div>
<div>
<p className="text-xs text-slate-500 uppercase tracking-wider">Letzte Sync</p>
<p className="text-sm text-slate-700">
{account.lastSync
? new Date(account.lastSync).toLocaleString('de-DE')
: 'Nie'}
</p>
</div>
</div>
</div>
))
)}
</div>
)}
{/* Add Account Modal */}
{showAddModal && (
<AddAccountModal onClose={() => setShowAddModal(false)} onSuccess={() => { setShowAddModal(false); onRefresh(); }} />
)}
</div>
)
}
function AddAccountModal({
onClose,
onSuccess
}: {
onClose: () => void
onSuccess: () => void
}) {
const [formData, setFormData] = useState({
email: '',
displayName: '',
imapHost: '',
imapPort: 993,
smtpHost: '',
smtpPort: 587,
username: '',
password: '',
})
const [submitting, setSubmitting] = useState(false)
const [error, setError] = useState<string | null>(null)
const handleSubmit = async (e: React.FormEvent) => {
e.preventDefault()
setSubmitting(true)
setError(null)
try {
const res = await fetch(`${API_BASE}/api/v1/mail/accounts`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
email: formData.email,
display_name: formData.displayName,
imap_host: formData.imapHost,
imap_port: formData.imapPort,
smtp_host: formData.smtpHost,
smtp_port: formData.smtpPort,
username: formData.username,
password: formData.password,
}),
})
if (res.ok) {
onSuccess()
} else {
const data = await res.json()
setError(data.detail || 'Fehler beim Hinzufuegen des Kontos')
}
} catch (err) {
setError('Netzwerkfehler')
} finally {
setSubmitting(false)
}
}
return (
<div className="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
<div className="bg-white rounded-lg shadow-xl max-w-lg w-full mx-4 max-h-[90vh] overflow-y-auto">
<div className="p-6 border-b border-slate-200">
<h2 className="text-lg font-semibold text-slate-900">E-Mail-Konto hinzufuegen</h2>
</div>
<form onSubmit={handleSubmit} className="p-6 space-y-4">
{error && (
<div className="p-3 bg-red-50 text-red-700 rounded-lg text-sm">{error}</div>
)}
<div className="grid grid-cols-2 gap-4">
<div className="col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">E-Mail-Adresse</label>
<input
type="email"
required
value={formData.email}
onChange={(e) => setFormData({ ...formData, email: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="schulleitung@grundschule-xy.de"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Anzeigename</label>
<input
type="text"
value={formData.displayName}
onChange={(e) => setFormData({ ...formData, displayName: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="Schulleitung"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Server</label>
<input
type="text"
required
value={formData.imapHost}
onChange={(e) => setFormData({ ...formData, imapHost: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="imap.example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">IMAP Port</label>
<input
type="number"
required
value={formData.imapPort}
onChange={(e) => setFormData({ ...formData, imapPort: parseInt(e.target.value) })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Server</label>
<input
type="text"
required
value={formData.smtpHost}
onChange={(e) => setFormData({ ...formData, smtpHost: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
placeholder="smtp.example.com"
/>
</div>
<div>
<label className="block text-sm font-medium text-slate-700 mb-1">SMTP Port</label>
<input
type="number"
required
value={formData.smtpPort}
onChange={(e) => setFormData({ ...formData, smtpPort: parseInt(e.target.value) })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Benutzername</label>
<input
type="text"
required
value={formData.username}
onChange={(e) => setFormData({ ...formData, username: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
</div>
<div className="col-span-2">
<label className="block text-sm font-medium text-slate-700 mb-1">Passwort</label>
<input
type="password"
required
value={formData.password}
onChange={(e) => setFormData({ ...formData, password: e.target.value })}
className="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
/>
<p className="text-xs text-slate-500 mt-1">
Das Passwort wird verschluesselt in Vault gespeichert.
</p>
</div>
</div>
<div className="flex justify-end gap-3 pt-4 border-t border-slate-200">
<button
type="button"
onClick={onClose}
className="px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-100 rounded-lg"
>
Abbrechen
</button>
<button
type="submit"
disabled={submitting}
className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 disabled:opacity-50"
>
{submitting ? 'Speichern...' : 'Konto hinzufuegen'}
</button>
</div>
</form>
</div>
</div>
)
}
// ============================================================================
// AI Settings Tab
// ============================================================================
function AISettingsTab() {
const [settings, setSettings] = useState({
autoAnalyze: true,
autoCreateTasks: true,
analysisModel: 'breakpilot-teacher-8b',
confidenceThreshold: 0.7,
})
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">KI-Einstellungen</h2>
<p className="text-sm text-slate-500">Konfigurieren Sie die automatische E-Mail-Analyse</p>
</div>
<div className="bg-white rounded-lg border border-slate-200 p-6 space-y-6">
{/* Auto-Analyze */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-slate-900">Automatische Analyse</h3>
<p className="text-sm text-slate-500">E-Mails automatisch beim Empfang analysieren</p>
</div>
<button
onClick={() => setSettings({ ...settings, autoAnalyze: !settings.autoAnalyze })}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
settings.autoAnalyze ? 'bg-blue-600' : 'bg-slate-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
settings.autoAnalyze ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Auto-Create Tasks */}
<div className="flex items-center justify-between">
<div>
<h3 className="text-sm font-medium text-slate-900">Aufgaben automatisch erstellen</h3>
<p className="text-sm text-slate-500">Erkannte Fristen als Aufgaben anlegen</p>
</div>
<button
onClick={() => setSettings({ ...settings, autoCreateTasks: !settings.autoCreateTasks })}
className={`relative inline-flex h-6 w-11 items-center rounded-full transition-colors ${
settings.autoCreateTasks ? 'bg-blue-600' : 'bg-slate-200'
}`}
>
<span
className={`inline-block h-4 w-4 transform rounded-full bg-white transition-transform ${
settings.autoCreateTasks ? 'translate-x-6' : 'translate-x-1'
}`}
/>
</button>
</div>
{/* Model Selection */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">Analyse-Modell</label>
<select
value={settings.analysisModel}
onChange={(e) => setSettings({ ...settings, analysisModel: e.target.value })}
className="w-full md:w-64 px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-blue-500"
>
<option value="breakpilot-teacher-8b">BreakPilot Teacher 8B (schnell)</option>
<option value="breakpilot-teacher-70b">BreakPilot Teacher 70B (genau)</option>
<option value="llama-3.1-8b-instruct">Llama 3.1 8B Instruct</option>
</select>
</div>
{/* Confidence Threshold */}
<div>
<label className="block text-sm font-medium text-slate-700 mb-2">
Konfidenz-Schwelle: {Math.round(settings.confidenceThreshold * 100)}%
</label>
<input
type="range"
min="0.5"
max="0.95"
step="0.05"
value={settings.confidenceThreshold}
onChange={(e) => setSettings({ ...settings, confidenceThreshold: parseFloat(e.target.value) })}
className="w-full md:w-64"
/>
<p className="text-xs text-slate-500 mt-1">
Mindest-Konfidenz fuer automatische Aufgabenerstellung
</p>
</div>
</div>
{/* Sender Classification */}
<div className="bg-white rounded-lg border border-slate-200 p-6">
<h3 className="text-sm font-medium text-slate-700 mb-4">Bekannte Absender (Niedersachsen)</h3>
<div className="grid grid-cols-2 md:grid-cols-3 gap-3">
{[
{ domain: '@mk.niedersachsen.de', type: 'Kultusministerium', priority: 'Hoch' },
{ domain: '@rlsb.de', type: 'RLSB', priority: 'Hoch' },
{ domain: '@landesschulbehoerde-nds.de', type: 'Landesschulbehoerde', priority: 'Hoch' },
{ domain: '@nibis.de', type: 'NiBiS', priority: 'Mittel' },
{ domain: '@schultraeger.de', type: 'Schultraeger', priority: 'Mittel' },
].map((sender) => (
<div key={sender.domain} className="p-3 bg-slate-50 rounded-lg">
<p className="text-sm font-mono text-slate-700">{sender.domain}</p>
<p className="text-xs text-slate-500">{sender.type}</p>
<span className={`text-xs px-2 py-0.5 rounded ${
sender.priority === 'Hoch' ? 'bg-red-100 text-red-700' : 'bg-yellow-100 text-yellow-700'
}`}>
{sender.priority}
</span>
</div>
))}
</div>
</div>
</div>
)
}
// ============================================================================
// Templates Tab
// ============================================================================
function TemplatesTab() {
const [templates] = useState([
{ id: '1', name: 'Eingangsbestaetigung', category: 'Standard', usageCount: 45 },
{ id: '2', name: 'Terminbestaetigung', category: 'Termine', usageCount: 23 },
{ id: '3', name: 'Elternbrief-Vorlage', category: 'Eltern', usageCount: 67 },
])
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-lg font-semibold text-slate-900">E-Mail-Vorlagen</h2>
<p className="text-sm text-slate-500">Verwalten Sie Antwort-Templates</p>
</div>
<button className="px-4 py-2 text-sm font-medium text-white bg-blue-600 rounded-lg hover:bg-blue-700 flex items-center gap-2">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
Vorlage erstellen
</button>
</div>
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Name</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Kategorie</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Verwendet</th>
<th className="px-6 py-3 text-right text-xs font-medium text-slate-500 uppercase">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{templates.map((template) => (
<tr key={template.id} className="hover:bg-slate-50">
<td className="px-6 py-4 text-sm font-medium text-slate-900">{template.name}</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-slate-100 text-slate-700 text-xs rounded">{template.category}</span>
</td>
<td className="px-6 py-4 text-sm text-slate-500">{template.usageCount}x</td>
<td className="px-6 py-4 text-right">
<button className="text-blue-600 hover:text-blue-800 text-sm font-medium">Bearbeiten</button>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
// ============================================================================
// Audit Log Tab
// ============================================================================
function AuditLogTab() {
const [logs] = useState([
{ id: '1', action: 'account_created', user: 'admin@breakpilot.de', timestamp: new Date().toISOString(), details: 'Konto schulleitung@example.de hinzugefuegt' },
{ id: '2', action: 'email_analyzed', user: 'system', timestamp: new Date(Date.now() - 3600000).toISOString(), details: '5 E-Mails analysiert' },
{ id: '3', action: 'task_created', user: 'system', timestamp: new Date(Date.now() - 7200000).toISOString(), details: 'Aufgabe aus Fristenerkennung erstellt' },
])
const actionLabels: Record<string, string> = {
account_created: 'Konto erstellt',
email_analyzed: 'E-Mail analysiert',
task_created: 'Aufgabe erstellt',
sync_completed: 'Sync abgeschlossen',
}
return (
<div className="space-y-6">
<div>
<h2 className="text-lg font-semibold text-slate-900">Audit-Log</h2>
<p className="text-sm text-slate-500">Alle Aktionen im Mail-System</p>
</div>
<div className="bg-white rounded-lg border border-slate-200 overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Zeit</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Aktion</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Benutzer</th>
<th className="px-6 py-3 text-left text-xs font-medium text-slate-500 uppercase">Details</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{logs.map((log) => (
<tr key={log.id} className="hover:bg-slate-50">
<td className="px-6 py-4 text-sm text-slate-500">
{new Date(log.timestamp).toLocaleString('de-DE')}
</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-blue-100 text-blue-700 text-xs rounded font-medium">
{actionLabels[log.action] || log.action}
</span>
</td>
<td className="px-6 py-4 text-sm text-slate-700">{log.user}</td>
<td className="px-6 py-4 text-sm text-slate-500">{log.details}</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}

View File

@@ -0,0 +1,421 @@
'use client'
/**
* Mail Wizard Page
* Migrated from website/admin/mail/wizard to admin-v2/communication/mail/wizard
*
* Interaktives Lernen und Testen der E-Mail Integration
*/
import { useState } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
import {
WizardStepper,
WizardNavigation,
EducationCard,
ArchitectureContext,
TestRunner,
TestSummary,
type WizardStep,
type TestCategoryResult,
type FullTestResults,
type EducationContent,
type ArchitectureContextType,
} from '@/components/wizard'
// ==============================================
// Constants
// ==============================================
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://macmini:8000'
const STEPS: WizardStep[] = [
{ id: 'welcome', name: 'Willkommen', icon: '👋', status: 'pending' },
{ id: 'smtp', name: 'SMTP', icon: '📤', status: 'pending', category: 'smtp' },
{ id: 'imap', name: 'IMAP', icon: '📥', status: 'pending', category: 'imap' },
{ id: 'templates', name: 'Templates', icon: '📝', status: 'pending', category: 'templates' },
{ id: 'ai-analysis', name: 'KI-Analyse', icon: '🤖', status: 'pending', category: 'ai-analysis' },
{ id: 'summary', name: 'Zusammenfassung', icon: '📊', status: 'pending' },
]
const EDUCATION_CONTENT: Record<string, EducationContent> = {
'welcome': {
title: 'Willkommen zum Mail Wizard',
content: [
'E-Mail ist nach wie vor der wichtigste Kommunikationskanal mit Eltern.',
'',
'BreakPilot bietet:',
'- SMTP: Versand von System-E-Mails (Benachrichtigungen, Newsletter)',
'- IMAP: Empfang und Analyse eingehender E-Mails',
'- Templates: Versionierte E-Mail-Vorlagen mit DSB-Freigabe',
'- KI-Analyse: Automatische Kategorisierung und GFK-Pruefung',
'',
'In der Entwicklung nutzen wir Mailpit als Mail-Catcher.',
'Alle E-Mails werden abgefangen und koennen inspiziert werden.',
],
},
'smtp': {
title: 'SMTP - Ausgehende E-Mails',
content: [
'SMTP (Simple Mail Transfer Protocol) sendet E-Mails.',
'',
'Typische Verwendung:',
'- Passwort-Reset E-Mails',
'- Einwilligungs-Erinnerungen',
'- DSR-Kommunikation (Betroffenenanfragen)',
'- Elternbriefe und Newsletter',
'',
'Entwicklungsumgebung:',
'- Mailpit faengt alle E-Mails ab',
'- Keine echten E-Mails werden versendet',
'- Web-UI unter http://macmini:8025',
'',
'Produktion: Echter SMTP-Server (z.B. Postfix, SES)',
],
},
'imap': {
title: 'IMAP - Eingehende E-Mails',
content: [
'IMAP (Internet Message Access Protocol) empfaengt E-Mails.',
'',
'Anwendungsfaelle:',
'- Eltern-Antworten auf Benachrichtigungen',
'- Automatische Ticket-Erstellung aus E-Mails',
'- Abwesenheitsmeldungen per E-Mail',
'',
'Verarbeitung:',
'1. E-Mail wird empfangen',
'2. KI analysiert Inhalt und Stimmung',
'3. Automatische Kategorisierung',
'4. Weiterleitung an zustaendige Stelle',
'',
'DSGVO: E-Mails werden nach Verarbeitung archiviert/geloescht',
],
},
'templates': {
title: 'E-Mail Templates - Versionierte Vorlagen',
content: [
'Alle System-E-Mails nutzen versionierte Templates.',
'',
'Workflow (wie bei rechtlichen Dokumenten):',
'- draft: Entwurf wird erstellt',
'- review: DSB/Admin prueft Inhalt',
'- approved: Freigabe erteilt',
'- published: Aktiv im System',
'',
'Template-Typen:',
'- welcome: Willkommens-E-Mail',
'- password_reset: Passwort zuruecksetzen',
'- consent_reminder: Einwilligungs-Erinnerung',
'- dsr_receipt: DSR-Eingangsbestaetigung',
'',
'Personalisierung: {{user.name}}, {{deadline}}, etc.',
],
},
'ai-analysis': {
title: 'KI-Analyse - LLM & GFK',
content: [
'KI-gestuetzte Analyse verbessert die Kommunikation.',
'',
'LLM-Funktionen:',
'- Automatische Kategorisierung eingehender E-Mails',
'- Sentiment-Analyse (positiv/neutral/negativ)',
'- Zusammenfassung langer E-Mails',
'- Antwort-Vorschlaege generieren',
'',
'GFK (Gewaltfreie Kommunikation):',
'- Pruefung ausgehender Elternbriefe',
'- Erkennung von "Du-Botschaften"',
'- Vorschlaege fuer wertschaetzende Formulierung',
'- Konfliktvermeidung durch bessere Sprache',
'',
'Optional: Nur aktiv wenn LLM_GATEWAY_ENABLED=true',
],
},
'summary': {
title: 'Test-Zusammenfassung',
content: [
'Hier sehen Sie eine Uebersicht aller durchgefuehrten Tests:',
'- SMTP Server Verfuegbarkeit',
'- IMAP Server Status',
'- Template-Verwaltung',
'- KI-Analyse Bereitschaft',
],
},
}
const ARCHITECTURE_CONTEXTS: Record<string, ArchitectureContextType> = {
'smtp': {
layer: 'service',
services: ['backend', 'mailserver'],
dependencies: ['Mailpit (Dev)', 'Postfix (Prod)', 'DNS/SPF/DKIM'],
dataFlow: ['FastAPI', 'SMTP Client', 'Mailpit/Postfix', 'Recipient'],
},
'imap': {
layer: 'service',
services: ['backend', 'mailserver'],
dependencies: ['IMAP Server', 'PostgreSQL', 'LLM Gateway'],
dataFlow: ['Mailserver', 'IMAP Fetch', 'KI-Analyse', 'PostgreSQL'],
},
'templates': {
layer: 'api',
services: ['backend', 'consent-service'],
dependencies: ['PostgreSQL', 'Template Engine', 'DSB Workflow'],
dataFlow: ['Admin UI', 'FastAPI', 'email_templates', 'PostgreSQL'],
},
'ai-analysis': {
layer: 'service',
services: ['backend'],
dependencies: ['LLM Gateway', 'OpenAI/Anthropic/Local', 'GFK Rules'],
dataFlow: ['E-Mail Text', 'LLM Gateway', 'Analyse Result', 'PostgreSQL'],
},
}
// ==============================================
// Main Component
// ==============================================
export default function MailWizardPage() {
const [currentStep, setCurrentStep] = useState(0)
const [steps, setSteps] = useState<WizardStep[]>(STEPS)
const [categoryResults, setCategoryResults] = useState<Record<string, TestCategoryResult>>({})
const [fullResults, setFullResults] = useState<FullTestResults | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const currentStepData = steps[currentStep]
const isTestStep = currentStepData?.category !== undefined
const isWelcome = currentStepData?.id === 'welcome'
const isSummary = currentStepData?.id === 'summary'
const runCategoryTest = async (category: string) => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${BACKEND_URL}/api/admin/mail-tests/${category}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result: TestCategoryResult = await response.json()
setCategoryResults((prev) => ({ ...prev, [category]: result }))
setSteps((prev) =>
prev.map((step) =>
step.category === category
? { ...step, status: result.failed === 0 ? 'completed' : 'failed' }
: step
)
)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const runAllTests = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${BACKEND_URL}/api/admin/mail-tests/run-all`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const results: FullTestResults = await response.json()
setFullResults(results)
setSteps((prev) =>
prev.map((step) => {
if (step.category) {
const catResult = results.categories.find((c) => c.category === step.category)
if (catResult) {
return { ...step, status: catResult.failed === 0 ? 'completed' : 'failed' }
}
}
return step
})
)
const newCategoryResults: Record<string, TestCategoryResult> = {}
results.categories.forEach((cat) => {
newCategoryResults[cat.category] = cat
})
setCategoryResults(newCategoryResults)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const goToNext = () => {
if (currentStep < steps.length - 1) {
setSteps((prev) =>
prev.map((step, idx) =>
idx === currentStep && step.status === 'pending'
? { ...step, status: 'completed' }
: step
)
)
setCurrentStep((prev) => prev + 1)
}
}
const goToPrev = () => {
if (currentStep > 0) {
setCurrentStep((prev) => prev - 1)
}
}
const handleStepClick = (index: number) => {
if (index <= currentStep || steps[index - 1]?.status !== 'pending') {
setCurrentStep(index)
}
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="Mail Wizard"
purpose="Interaktives Lernen und Testen der E-Mail Integration. Pruefen Sie SMTP, IMAP, Templates und KI-Analyse Schritt fuer Schritt."
audience={['Admins', 'Entwickler']}
architecture={{
services: ['backend (Python)', 'mailpit (Dev)', 'LLM Gateway'],
databases: ['PostgreSQL'],
}}
relatedPages={[
{ name: 'Unified Inbox', href: '/communication/mail', description: 'E-Mail Verwaltung' },
]}
collapsible={true}
defaultCollapsed={true}
/>
{/* Back Link */}
<div className="mb-6">
<Link href="/communication/mail" className="text-blue-600 hover:text-blue-800 text-sm flex items-center gap-1">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zu E-Mail Management
</Link>
</div>
{/* Header */}
<div className="bg-white rounded-lg shadow p-4 mb-6 flex items-center justify-between">
<div className="flex items-center">
<span className="text-3xl mr-3">📧</span>
<div>
<h2 className="text-lg font-bold text-gray-800">E-Mail Test Wizard</h2>
<p className="text-sm text-gray-600">SMTP, IMAP, Templates & KI-Analyse</p>
</div>
</div>
</div>
{/* Stepper */}
<div className="bg-white rounded-lg shadow p-6 mb-6">
<WizardStepper steps={steps} currentStep={currentStep} onStepClick={handleStepClick} />
</div>
{/* Content */}
<div className="bg-white rounded-lg shadow p-6">
<div className="flex items-center mb-6">
<span className="text-3xl mr-3">{currentStepData?.icon}</span>
<div>
<h2 className="text-xl font-bold text-gray-800">
Schritt {currentStep + 1}: {currentStepData?.name}
</h2>
<p className="text-gray-500 text-sm">
{currentStep + 1} von {steps.length}
</p>
</div>
</div>
<EducationCard content={EDUCATION_CONTENT[currentStepData?.id || '']} />
{isTestStep && currentStepData?.category && ARCHITECTURE_CONTEXTS[currentStepData.category] && (
<ArchitectureContext
context={ARCHITECTURE_CONTEXTS[currentStepData.category]}
currentStep={currentStepData.name}
/>
)}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 mb-6">
<strong>Fehler:</strong> {error}
</div>
)}
{isWelcome && (
<div className="text-center py-8">
<button
onClick={goToNext}
className="bg-blue-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Wizard starten
</button>
</div>
)}
{isTestStep && currentStepData?.category && (
<TestRunner
category={currentStepData.category}
categoryResult={categoryResults[currentStepData.category]}
isLoading={isLoading}
onRunTests={() => runCategoryTest(currentStepData.category!)}
/>
)}
{isSummary && (
<div>
{!fullResults ? (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">
Fuehren Sie alle Tests aus um eine Zusammenfassung zu sehen.
</p>
<button
onClick={runAllTests}
disabled={isLoading}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isLoading ? 'Alle Tests laufen...' : 'Alle Tests ausfuehren'}
</button>
</div>
) : (
<TestSummary results={fullResults} />
)}
</div>
)}
<WizardNavigation
currentStep={currentStep}
totalSteps={steps.length}
onPrev={goToPrev}
onNext={goToNext}
showNext={!isSummary}
isLoading={isLoading}
/>
</div>
<div className="text-center text-gray-500 text-sm mt-6">
Diese Tests pruefen die E-Mail-Integration.
Bei Fragen wenden Sie sich an das IT-Team.
</div>
</div>
)
}

View File

@@ -0,0 +1,594 @@
'use client'
/**
* Voice Service Admin Page (migrated from website/admin/voice)
*
* Displays:
* - Voice-First Architecture Overview
* - Developer Guide Content
* - Live Voice Demo (embedded from studio-v2)
* - Task State Machine Documentation
* - DSGVO Compliance Information
*/
import { useState } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
type TabType = 'overview' | 'demo' | 'tasks' | 'intents' | 'dsgvo' | 'api'
// Task State Machine data
const TASK_STATES = [
{ state: 'DRAFT', description: 'Task erstellt, noch nicht verarbeitet', color: 'bg-gray-100 text-gray-800', next: ['QUEUED', 'PAUSED'] },
{ state: 'QUEUED', description: 'In Warteschlange fuer Verarbeitung', color: 'bg-blue-100 text-blue-800', next: ['RUNNING', 'PAUSED'] },
{ state: 'RUNNING', description: 'Wird aktuell verarbeitet', color: 'bg-yellow-100 text-yellow-800', next: ['READY', 'PAUSED'] },
{ state: 'READY', description: 'Fertig, wartet auf User-Bestaetigung', color: 'bg-green-100 text-green-800', next: ['APPROVED', 'REJECTED', 'PAUSED'] },
{ state: 'APPROVED', description: 'Vom User bestaetigt', color: 'bg-emerald-100 text-emerald-800', next: ['COMPLETED'] },
{ state: 'REJECTED', description: 'Vom User abgelehnt', color: 'bg-red-100 text-red-800', next: ['DRAFT'] },
{ state: 'COMPLETED', description: 'Erfolgreich abgeschlossen', color: 'bg-teal-100 text-teal-800', next: [] },
{ state: 'EXPIRED', description: 'TTL ueberschritten', color: 'bg-orange-100 text-orange-800', next: [] },
{ state: 'PAUSED', description: 'Vom User pausiert', color: 'bg-purple-100 text-purple-800', next: ['DRAFT', 'QUEUED', 'RUNNING', 'READY'] },
]
// Intent Types (22 types organized by group)
const INTENT_GROUPS = [
{
group: 'Notizen',
color: 'bg-blue-50 border-blue-200',
intents: [
{ type: 'student_observation', example: 'Notiz zu Max: heute wiederholt gestoert', description: 'Schuelerbeobachtungen' },
{ type: 'reminder', example: 'Erinner mich morgen an Konferenz', description: 'Erinnerungen setzen' },
{ type: 'homework_check', example: '7b Mathe Hausaufgabe kontrollieren', description: 'Hausaufgaben pruefen' },
{ type: 'conference_topic', example: 'Thema Lehrerkonferenz: iPad-Regeln', description: 'Konferenzthemen' },
{ type: 'correction_thought', example: 'Aufgabe 3: haeufiger Fehler erklaeren', description: 'Korrekturgedanken' },
]
},
{
group: 'Content-Generierung',
color: 'bg-green-50 border-green-200',
intents: [
{ type: 'worksheet_generate', example: 'Erstelle 3 Lueckentexte zu Vokabeln', description: 'Arbeitsblaetter erstellen' },
{ type: 'quiz_generate', example: '10-Minuten Vokabeltest mit Loesungen', description: 'Quiz/Tests erstellen' },
{ type: 'quick_activity', example: '10 Minuten Einstieg, 5 Aufgaben', description: 'Schnelle Aktivitaeten' },
{ type: 'differentiation', example: 'Zwei Schwierigkeitsstufen: Basis und Plus', description: 'Differenzierung' },
]
},
{
group: 'Kommunikation',
color: 'bg-yellow-50 border-yellow-200',
intents: [
{ type: 'parent_letter', example: 'Neutraler Elternbrief wegen Stoerungen', description: 'Elternbriefe erstellen' },
{ type: 'class_message', example: 'Nachricht an 8a: Hausaufgaben bis Mittwoch', description: 'Klassennachrichten' },
]
},
{
group: 'Canvas-Editor',
color: 'bg-purple-50 border-purple-200',
intents: [
{ type: 'canvas_edit', example: 'Ueberschriften groesser, Zeilenabstand kleiner', description: 'Formatierung aendern' },
{ type: 'canvas_layout', example: 'Alles auf eine Seite, Drucklayout A4', description: 'Layout anpassen' },
{ type: 'canvas_element', example: 'Kasten fuer Merke hinzufuegen', description: 'Elemente hinzufuegen' },
{ type: 'canvas_image', example: 'Bild 2 nach links, Pfeil auf Aufgabe 3', description: 'Bilder positionieren' },
]
},
{
group: 'RAG & Korrektur',
color: 'bg-pink-50 border-pink-200',
intents: [
{ type: 'operator_checklist', example: 'Operatoren-Checkliste fuer diese Aufgabe', description: 'Operatoren abrufen' },
{ type: 'eh_passage', example: 'Erwartungshorizont-Passage zu diesem Thema', description: 'EH-Passagen suchen' },
{ type: 'feedback_suggestion', example: 'Kurze Feedbackformulierung vorschlagen', description: 'Feedback vorschlagen' },
]
},
{
group: 'Follow-up (TaskOrchestrator)',
color: 'bg-teal-50 border-teal-200',
intents: [
{ type: 'task_summary', example: 'Fasse alle offenen Tasks zusammen', description: 'Task-Uebersicht' },
{ type: 'convert_note', example: 'Mach aus der Notiz von gestern einen Elternbrief', description: 'Notizen konvertieren' },
{ type: 'schedule_reminder', example: 'Erinner mich morgen an das Gespraech mit Max', description: 'Erinnerungen planen' },
]
},
]
// DSGVO Data Categories
const DSGVO_CATEGORIES = [
{ category: 'Audio', processing: 'NUR transient im RAM, NIEMALS persistiert', storage: 'Keine', ttl: '-', icon: '🎤', risk: 'low' },
{ category: 'PII (Schuelernamen)', processing: 'NUR auf Lehrergeraet', storage: 'Client-side', ttl: '-', icon: '👤', risk: 'high' },
{ category: 'Pseudonyme', processing: 'Server erlaubt (student_ref, class_ref)', storage: 'Valkey Cache', ttl: '24h', icon: '🔢', risk: 'low' },
{ category: 'Transkripte', processing: 'NUR verschluesselt (AES-256-GCM)', storage: 'PostgreSQL', ttl: '7 Tage', icon: '📝', risk: 'medium' },
{ category: 'Task States', processing: 'TaskOrchestrator', storage: 'Valkey', ttl: '30 Tage', icon: '📋', risk: 'low' },
{ category: 'Audit Logs', processing: 'Nur truncated IDs, keine PII', storage: 'PostgreSQL', ttl: '90 Tage', icon: '📊', risk: 'low' },
]
// API Endpoints
const API_ENDPOINTS = [
{ method: 'POST', path: '/api/v1/sessions', description: 'Voice Session erstellen' },
{ method: 'GET', path: '/api/v1/sessions/{id}', description: 'Session Status abrufen' },
{ method: 'DELETE', path: '/api/v1/sessions/{id}', description: 'Session beenden' },
{ method: 'GET', path: '/api/v1/sessions/{id}/tasks', description: 'Pending Tasks abrufen' },
{ method: 'POST', path: '/api/v1/tasks', description: 'Task erstellen' },
{ method: 'GET', path: '/api/v1/tasks/{id}', description: 'Task Status abrufen' },
{ method: 'PUT', path: '/api/v1/tasks/{id}/transition', description: 'Task State aendern' },
{ method: 'DELETE', path: '/api/v1/tasks/{id}', description: 'Task loeschen' },
{ method: 'WS', path: '/ws/voice', description: 'Voice Streaming (WebSocket)' },
{ method: 'GET', path: '/health', description: 'Health Check' },
]
export default function VoiceMatrixPage() {
const [activeTab, setActiveTab] = useState<TabType>('overview')
const [demoLoaded, setDemoLoaded] = useState(false)
const tabs = [
{ id: 'overview', name: 'Architektur', icon: '🏗️' },
{ id: 'demo', name: 'Live Demo', icon: '🎤' },
{ id: 'tasks', name: 'Task States', icon: '📋' },
{ id: 'intents', name: 'Intents (22)', icon: '🎯' },
{ id: 'dsgvo', name: 'DSGVO', icon: '🔒' },
{ id: 'api', name: 'API', icon: '🔌' },
]
return (
<div>
{/* Page Purpose */}
<PagePurpose
title="Voice Service"
purpose="Voice-First Interface mit PersonaPlex-7B & TaskOrchestrator. Konfigurieren und testen Sie den Voice-Service fuer Lehrer-Interaktionen per Sprache."
audience={['Entwickler', 'Admins']}
architecture={{
services: ['voice-service (Python, Port 8091)', 'studio-v2 (Next.js)', 'valkey (Cache)'],
databases: ['PostgreSQL', 'Valkey Cache'],
}}
relatedPages={[
{ name: 'Matrix & Jitsi', href: '/communication/matrix', description: 'Kommunikation Monitoring' },
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'KI-Provider vergleichen' },
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU fuer Voice-Service' },
]}
collapsible={true}
defaultCollapsed={false}
/>
{/* Quick Links */}
<div className="mb-6 flex flex-wrap gap-3">
<a
href="https://macmini:3001/voice-test"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11a7 7 0 01-7 7m0 0a7 7 0 01-7-7m7 7v4m0 0H8m4 0h4m-4-8a3 3 0 01-3-3V5a3 3 0 116 0v6a3 3 0 01-3 3z" />
</svg>
Voice Test (Studio)
</a>
<a
href="https://macmini:8091/health"
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-2 px-4 py-2 bg-green-100 text-green-700 rounded-lg hover:bg-green-200 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Health Check
</a>
<Link
href="/development/docs"
className="flex items-center gap-2 px-4 py-2 bg-slate-100 text-slate-700 rounded-lg hover:bg-slate-200 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
Developer Docs
</Link>
</div>
{/* Stats Overview */}
<div className="grid grid-cols-2 md:grid-cols-6 gap-4 mb-6">
<div className="bg-white rounded-lg shadow p-4">
<div className="text-3xl font-bold text-teal-600">8091</div>
<div className="text-sm text-slate-500">Port</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-3xl font-bold text-blue-600">22</div>
<div className="text-sm text-slate-500">Task Types</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-3xl font-bold text-purple-600">9</div>
<div className="text-sm text-slate-500">Task States</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-3xl font-bold text-green-600">24kHz</div>
<div className="text-sm text-slate-500">Audio Rate</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-3xl font-bold text-orange-600">80ms</div>
<div className="text-sm text-slate-500">Frame Size</div>
</div>
<div className="bg-white rounded-lg shadow p-4">
<div className="text-3xl font-bold text-red-600">0</div>
<div className="text-sm text-slate-500">Audio Persist</div>
</div>
</div>
{/* Tabs */}
<div className="bg-white rounded-lg shadow mb-6">
<div className="border-b border-slate-200 px-4">
<div className="flex gap-1 overflow-x-auto">
{tabs.map((tab) => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id as TabType)}
className={`px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors border-b-2 ${
activeTab === tab.id
? 'border-teal-600 text-teal-600'
: 'border-transparent text-slate-500 hover:text-slate-700'
}`}
>
<span className="mr-2">{tab.icon}</span>
{tab.name}
</button>
))}
</div>
</div>
<div className="p-6">
{/* Overview Tab */}
{activeTab === 'overview' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-slate-900">Voice-First Architektur</h3>
{/* Architecture Diagram */}
<div className="bg-slate-50 rounded-lg p-6 font-mono text-sm overflow-x-auto">
<pre className="text-slate-700">{`
┌──────────────────────────────────────────────────────────────────┐
│ LEHRERGERAET (PWA / App) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ VoiceCapture.tsx │ voice-encryption.ts │ voice-api.ts │ │
│ │ Mikrofon │ AES-256-GCM │ WebSocket Client │ │
│ └────────────────────────────────────────────────────────────┘ │
└───────────────────────────┬──────────────────────────────────────┘
│ WebSocket (wss://)
┌──────────────────────────────────────────────────────────────────┐
│ VOICE SERVICE (Port 8091) │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ main.py │ streaming.py │ sessions.py │ tasks.py │ │
│ └────────────────────────────────────────────────────────────┘ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ task_orchestrator.py │ intent_router.py │ encryption │ │
│ └────────────────────────────────────────────────────────────┘ │
└───────────────────────────┬──────────────────────────────────────┘
┌──────────────────┼──────────────────┐
▼ ▼ ▼
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
│ PersonaPlex-7B │ │ Ollama Fallback │ │ Valkey Cache │
│ (A100 GPU) │ │ (Mac Mini) │ │ (Sessions) │
└─────────────────┘ └─────────────────┘ └─────────────────┘
`}</pre>
</div>
{/* Technology Stack */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 className="font-semibold text-blue-800 mb-2">Voice Model (Produktion)</h4>
<p className="text-sm text-blue-700">PersonaPlex-7B (NVIDIA)</p>
<p className="text-xs text-blue-600 mt-1">Full-Duplex Speech-to-Speech</p>
<p className="text-xs text-blue-500">Lizenz: MIT + NVIDIA Open Model</p>
</div>
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-semibold text-green-800 mb-2">Agent Orchestration</h4>
<p className="text-sm text-green-700">TaskOrchestrator</p>
<p className="text-xs text-green-600 mt-1">Task State Machine</p>
<p className="text-xs text-green-500">Lizenz: Proprietary</p>
</div>
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4">
<h4 className="font-semibold text-purple-800 mb-2">Audio Codec</h4>
<p className="text-sm text-purple-700">Mimi (24kHz, 80ms)</p>
<p className="text-xs text-purple-600 mt-1">Low-Latency Streaming</p>
<p className="text-xs text-purple-500">Lizenz: MIT</p>
</div>
</div>
{/* Key Files */}
<div>
<h4 className="font-semibold text-slate-800 mb-3">Wichtige Dateien</h4>
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">Datei</th>
<th className="px-4 py-2 text-left text-xs font-medium text-slate-500 uppercase">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
<tr><td className="px-4 py-2 font-mono text-sm">voice-service/main.py</td><td className="px-4 py-2 text-sm text-slate-600">FastAPI Entry, WebSocket Handler</td></tr>
<tr><td className="px-4 py-2 font-mono text-sm">voice-service/services/task_orchestrator.py</td><td className="px-4 py-2 text-sm text-slate-600">Task State Machine</td></tr>
<tr><td className="px-4 py-2 font-mono text-sm">voice-service/services/intent_router.py</td><td className="px-4 py-2 text-sm text-slate-600">Intent Detection (22 Types)</td></tr>
<tr><td className="px-4 py-2 font-mono text-sm">voice-service/services/encryption_service.py</td><td className="px-4 py-2 text-sm text-slate-600">Namespace Key Management</td></tr>
<tr><td className="px-4 py-2 font-mono text-sm">studio-v2/components/voice/VoiceCapture.tsx</td><td className="px-4 py-2 text-sm text-slate-600">Frontend Mikrofon + Crypto</td></tr>
<tr><td className="px-4 py-2 font-mono text-sm">studio-v2/lib/voice/voice-encryption.ts</td><td className="px-4 py-2 text-sm text-slate-600">AES-256-GCM Client-side</td></tr>
</tbody>
</table>
</div>
</div>
</div>
)}
{/* Demo Tab */}
{activeTab === 'demo' && (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h3 className="text-lg font-semibold text-slate-900">Live Voice Demo</h3>
<a
href="https://macmini:3001/voice-test"
target="_blank"
rel="noopener noreferrer"
className="text-sm text-teal-600 hover:text-teal-700 flex items-center gap-1"
>
In neuem Tab oeffnen
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 6H6a2 2 0 00-2 2v10a2 2 0 002 2h10a2 2 0 002-2v-4M14 4h6m0 0v6m0-6L10 14" />
</svg>
</a>
</div>
<div className="bg-slate-100 rounded-lg p-4 text-sm text-slate-600 mb-4">
<p><strong>Hinweis:</strong> Die Demo erfordert, dass der Voice Service (Port 8091) und das Studio-v2 Frontend (Port 3001) laufen.</p>
<code className="block mt-2 bg-slate-200 p-2 rounded">docker compose up -d voice-service && cd studio-v2 && npm run dev</code>
</div>
{/* Embedded Demo */}
<div className="relative bg-slate-900 rounded-lg overflow-hidden" style={{ height: '600px' }}>
{!demoLoaded && (
<div className="absolute inset-0 flex items-center justify-center">
<button
onClick={() => setDemoLoaded(true)}
className="px-6 py-3 bg-teal-600 text-white rounded-lg hover:bg-teal-700 transition-colors flex items-center gap-2"
>
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z" />
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
Voice Demo laden
</button>
</div>
)}
{demoLoaded && (
<iframe
src="https://macmini:3001/voice-test?embed=true"
className="w-full h-full border-0"
title="Voice Demo"
allow="microphone"
/>
)}
</div>
</div>
)}
{/* Task States Tab */}
{activeTab === 'tasks' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-slate-900">Task State Machine (TaskOrchestrator)</h3>
{/* State Diagram */}
<div className="bg-slate-50 rounded-lg p-6 font-mono text-sm overflow-x-auto">
<pre className="text-slate-700">{`
DRAFT → QUEUED → RUNNING → READY
┌───────────┴───────────┐
│ │
APPROVED REJECTED
│ │
COMPLETED DRAFT (revision)
Any State → EXPIRED (TTL)
Any State → PAUSED (User Interrupt)
`}</pre>
</div>
{/* States Table */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{TASK_STATES.map((state) => (
<div key={state.state} className={`${state.color} rounded-lg p-4`}>
<div className="font-semibold text-lg">{state.state}</div>
<p className="text-sm mt-1">{state.description}</p>
{state.next.length > 0 && (
<div className="mt-2 text-xs">
<span className="opacity-75">Naechste:</span>{' '}
{state.next.join(', ')}
</div>
)}
</div>
))}
</div>
</div>
)}
{/* Intents Tab */}
{activeTab === 'intents' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-slate-900">Intent Types (22 unterstuetzte Typen)</h3>
{INTENT_GROUPS.map((group) => (
<div key={group.group} className={`${group.color} border rounded-lg p-4`}>
<h4 className="font-semibold text-slate-800 mb-3">{group.group}</h4>
<div className="space-y-2">
{group.intents.map((intent) => (
<div key={intent.type} className="bg-white rounded-lg p-3 shadow-sm">
<div className="flex items-start justify-between">
<div>
<code className="text-sm font-mono text-teal-700 bg-teal-50 px-2 py-0.5 rounded">
{intent.type}
</code>
<p className="text-sm text-slate-600 mt-1">{intent.description}</p>
</div>
</div>
<div className="mt-2 text-xs text-slate-500 italic">
Beispiel: &quot;{intent.example}&quot;
</div>
</div>
))}
</div>
</div>
))}
</div>
)}
{/* DSGVO Tab */}
{activeTab === 'dsgvo' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-slate-900">DSGVO-Compliance</h3>
{/* Key Principles */}
<div className="bg-green-50 border border-green-200 rounded-lg p-4">
<h4 className="font-semibold text-green-800 mb-2">Kernprinzipien</h4>
<ul className="list-disc list-inside text-sm text-green-700 space-y-1">
<li><strong>Audio NIEMALS persistiert</strong> - Nur transient im RAM</li>
<li><strong>Namespace-Verschluesselung</strong> - Key nur auf Lehrergeraet</li>
<li><strong>Keine Klartext-PII serverseitig</strong> - Nur verschluesselt oder pseudonymisiert</li>
<li><strong>TTL-basierte Auto-Loeschung</strong> - 7/30/90 Tage je nach Kategorie</li>
</ul>
</div>
{/* Data Categories Table */}
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Kategorie</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Verarbeitung</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Speicherort</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">TTL</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Risiko</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{DSGVO_CATEGORIES.map((cat) => (
<tr key={cat.category}>
<td className="px-4 py-3">
<span className="mr-2">{cat.icon}</span>
<span className="font-medium">{cat.category}</span>
</td>
<td className="px-4 py-3 text-sm text-slate-600">{cat.processing}</td>
<td className="px-4 py-3 text-sm text-slate-600">{cat.storage}</td>
<td className="px-4 py-3 text-sm text-slate-600">{cat.ttl}</td>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${
cat.risk === 'low' ? 'bg-green-100 text-green-700' :
cat.risk === 'medium' ? 'bg-yellow-100 text-yellow-700' :
'bg-red-100 text-red-700'
}`}>
{cat.risk.toUpperCase()}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* Audit Log Info */}
<div className="bg-slate-50 border border-slate-200 rounded-lg p-4">
<h4 className="font-semibold text-slate-800 mb-2">Audit Logs (ohne PII)</h4>
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-green-600 font-medium">Erlaubt:</span>
<ul className="list-disc list-inside text-slate-600 mt-1">
<li>ref_id (truncated)</li>
<li>content_type</li>
<li>size_bytes</li>
<li>ttl_hours</li>
</ul>
</div>
<div>
<span className="text-red-600 font-medium">Verboten:</span>
<ul className="list-disc list-inside text-slate-600 mt-1">
<li>user_name</li>
<li>content / transcript</li>
<li>email</li>
<li>student_name</li>
</ul>
</div>
</div>
</div>
</div>
)}
{/* API Tab */}
{activeTab === 'api' && (
<div className="space-y-6">
<h3 className="text-lg font-semibold text-slate-900">Voice Service API (Port 8091)</h3>
{/* REST Endpoints */}
<div className="bg-white border border-slate-200 rounded-lg overflow-hidden">
<table className="min-w-full divide-y divide-slate-200">
<thead className="bg-slate-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Methode</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Endpoint</th>
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-200">
{API_ENDPOINTS.map((ep, idx) => (
<tr key={idx}>
<td className="px-4 py-3">
<span className={`px-2 py-1 rounded text-xs font-medium ${
ep.method === 'GET' ? 'bg-green-100 text-green-700' :
ep.method === 'POST' ? 'bg-blue-100 text-blue-700' :
ep.method === 'PUT' ? 'bg-yellow-100 text-yellow-700' :
ep.method === 'DELETE' ? 'bg-red-100 text-red-700' :
'bg-purple-100 text-purple-700'
}`}>
{ep.method}
</span>
</td>
<td className="px-4 py-3 font-mono text-sm">{ep.path}</td>
<td className="px-4 py-3 text-sm text-slate-600">{ep.description}</td>
</tr>
))}
</tbody>
</table>
</div>
{/* WebSocket Protocol */}
<div className="bg-slate-50 rounded-lg p-4">
<h4 className="font-semibold text-slate-800 mb-3">WebSocket Protocol</h4>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 text-sm">
<div className="bg-white rounded-lg p-3 border border-slate-200">
<div className="font-medium text-slate-700 mb-2">Client Server</div>
<ul className="list-disc list-inside text-slate-600 space-y-1">
<li><code className="bg-slate-100 px-1 rounded">Binary</code>: Int16 PCM Audio (24kHz, 80ms)</li>
<li><code className="bg-slate-100 px-1 rounded">JSON</code>: {`{type: "config|end_turn|interrupt"}`}</li>
</ul>
</div>
<div className="bg-white rounded-lg p-3 border border-slate-200">
<div className="font-medium text-slate-700 mb-2">Server Client</div>
<ul className="list-disc list-inside text-slate-600 space-y-1">
<li><code className="bg-slate-100 px-1 rounded">Binary</code>: Audio Response (base64)</li>
<li><code className="bg-slate-100 px-1 rounded">JSON</code>: {`{type: "transcript|intent|status|error"}`}</li>
</ul>
</div>
</div>
</div>
{/* Example curl commands */}
<div className="bg-slate-900 rounded-lg p-4 text-sm">
<h4 className="font-semibold text-slate-300 mb-3">Beispiel: Session erstellen</h4>
<pre className="text-green-400 overflow-x-auto">{`curl -X POST https://macmini:8091/api/v1/sessions \\
-H "Content-Type: application/json" \\
-d '{
"namespace_id": "ns-12345678abcdef12345678abcdef12",
"key_hash": "sha256:dGVzdGtleWhhc2h0ZXN0a2V5aGFzaHRlc3Q=",
"device_type": "pwa"
}'`}</pre>
</div>
</div>
)}
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,50 @@
'use client'
import { getCategoryById } from '@/lib/navigation'
import { ModuleCard } from '@/components/common/ModuleCard'
import { PagePurpose } from '@/components/common/PagePurpose'
export default function CommunicationPage() {
const category = getCategoryById('communication')
if (!category) {
return <div>Kategorie nicht gefunden</div>
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title={category.name}
purpose="Diese Kategorie umfasst alle Kommunikations- und Benachrichtigungsmodule. Hier ueberwachen Sie Matrix-Raeume, verwalten E-Mail-Konten und konfigurieren Alert-Feeds."
audience={['Admins', 'Support', 'Marketing']}
architecture={{
services: ['synapse (Matrix)', 'mailpit (Dev SMTP)', 'backend (Python)'],
databases: ['PostgreSQL', 'synapse-db'],
}}
collapsible={true}
defaultCollapsed={false}
/>
{/* Modules Grid */}
<h2 className="text-lg font-semibold text-slate-900 mb-4">Module</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{category.modules.map((module) => (
<ModuleCard key={module.id} module={module} category={category} />
))}
</div>
{/* Info Section */}
<div className="mt-8 bg-green-50 border border-green-200 rounded-xl p-6">
<h3 className="font-semibold text-green-800 flex items-center gap-2">
<span>📬</span>
Ende-zu-Ende-Verschluesselung
</h3>
<p className="text-sm text-green-700 mt-2">
Matrix-Kommunikation ist standardmaessig Ende-zu-Ende verschluesselt.
Jitsi-Konferenzen werden nicht auf dem Server gespeichert (optional: Aufnahme mit Jibri).
</p>
</div>
</div>
)
}

View File

@@ -0,0 +1,635 @@
'use client'
/**
* Video & Chat Admin Page
*
* Matrix & Jitsi Monitoring Dashboard
* Provides system statistics, active calls, user metrics, and service health
* Migrated from website/app/admin/communication
*/
import { useEffect, useState, useCallback } from 'react'
import Link from 'next/link'
import { PagePurpose } from '@/components/common/PagePurpose'
import { getModuleByHref } from '@/lib/navigation'
interface MatrixStats {
total_users: number
active_users: number
total_rooms: number
active_rooms: number
messages_today: number
messages_this_week: number
status: 'online' | 'offline' | 'degraded'
}
interface JitsiStats {
active_meetings: number
total_participants: number
meetings_today: number
average_duration_minutes: number
peak_concurrent_users: number
total_minutes_today: number
status: 'online' | 'offline' | 'degraded'
}
interface TrafficStats {
matrix: {
bandwidth_in_mb: number
bandwidth_out_mb: number
messages_per_minute: number
media_uploads_today: number
media_size_mb: number
}
jitsi: {
bandwidth_in_mb: number
bandwidth_out_mb: number
video_streams_active: number
audio_streams_active: number
estimated_hourly_gb: number
}
total: {
bandwidth_in_mb: number
bandwidth_out_mb: number
estimated_monthly_gb: number
}
}
interface CommunicationStats {
matrix: MatrixStats
jitsi: JitsiStats
traffic?: TrafficStats
last_updated: string
}
interface ActiveMeeting {
room_name: string
display_name: string
participants: number
started_at: string
duration_minutes: number
}
interface RecentRoom {
room_id: string
name: string
member_count: number
last_activity: string
room_type: 'class' | 'parent' | 'staff' | 'general'
}
export default function VideoChatPage() {
const [stats, setStats] = useState<CommunicationStats | null>(null)
const [activeMeetings, setActiveMeetings] = useState<ActiveMeeting[]>([])
const [recentRooms, setRecentRooms] = useState<RecentRoom[]>([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState<string | null>(null)
const moduleInfo = getModuleByHref('/communication/video-chat')
// Use local API proxy
const fetchStats = useCallback(async () => {
try {
const response = await fetch('/api/admin/communication/stats')
if (!response.ok) {
throw new Error(`HTTP ${response.status}`)
}
const data = await response.json()
setStats(data)
setActiveMeetings(data.active_meetings || [])
setRecentRooms(data.recent_rooms || [])
setError(null)
} catch (err) {
setError(err instanceof Error ? err.message : 'Verbindungsfehler')
// Set mock data for display purposes when API unavailable
setStats({
matrix: {
total_users: 0,
active_users: 0,
total_rooms: 0,
active_rooms: 0,
messages_today: 0,
messages_this_week: 0,
status: 'offline'
},
jitsi: {
active_meetings: 0,
total_participants: 0,
meetings_today: 0,
average_duration_minutes: 0,
peak_concurrent_users: 0,
total_minutes_today: 0,
status: 'offline'
},
last_updated: new Date().toISOString()
})
} finally {
setLoading(false)
}
}, [])
useEffect(() => {
fetchStats()
}, [fetchStats])
// Auto-refresh every 15 seconds
useEffect(() => {
const interval = setInterval(fetchStats, 15000)
return () => clearInterval(interval)
}, [fetchStats])
const getStatusBadge = (status: string) => {
const baseClasses = 'px-3 py-1 rounded-full text-xs font-semibold uppercase'
switch (status) {
case 'online':
return `${baseClasses} bg-green-100 text-green-800`
case 'degraded':
return `${baseClasses} bg-yellow-100 text-yellow-800`
case 'offline':
return `${baseClasses} bg-red-100 text-red-800`
default:
return `${baseClasses} bg-slate-100 text-slate-600`
}
}
const getRoomTypeBadge = (type: string) => {
const baseClasses = 'px-2 py-0.5 rounded text-xs font-medium'
switch (type) {
case 'class':
return `${baseClasses} bg-blue-100 text-blue-700`
case 'parent':
return `${baseClasses} bg-purple-100 text-purple-700`
case 'staff':
return `${baseClasses} bg-orange-100 text-orange-700`
default:
return `${baseClasses} bg-slate-100 text-slate-600`
}
}
const formatDuration = (minutes: number) => {
if (minutes < 60) return `${Math.round(minutes)} Min.`
const hours = Math.floor(minutes / 60)
const mins = Math.round(minutes % 60)
return `${hours}h ${mins}m`
}
const formatTimeAgo = (dateStr: string) => {
const date = new Date(dateStr)
const now = new Date()
const diffMs = now.getTime() - date.getTime()
const diffMins = Math.floor(diffMs / 60000)
if (diffMins < 1) return 'gerade eben'
if (diffMins < 60) return `vor ${diffMins} Min.`
if (diffMins < 1440) return `vor ${Math.floor(diffMins / 60)} Std.`
return `vor ${Math.floor(diffMins / 1440)} Tagen`
}
// Traffic estimation helpers for SysEleven planning
const calculateEstimatedTraffic = (direction: 'in' | 'out'): number => {
const messages = stats?.matrix?.messages_today || 0
const callMinutes = stats?.jitsi?.total_minutes_today || 0
const participants = stats?.jitsi?.total_participants || 0
const messageTrafficMB = messages * 0.002
const videoTrafficMB = callMinutes * participants * 0.011
if (direction === 'in') {
return messageTrafficMB * 0.3 + videoTrafficMB * 0.4
}
return messageTrafficMB * 0.7 + videoTrafficMB * 0.6
}
const calculateHourlyEstimate = (): number => {
const activeParticipants = stats?.jitsi?.total_participants || 0
return activeParticipants * 0.675
}
const calculateMonthlyEstimate = (): number => {
const dailyCallMinutes = stats?.jitsi?.total_minutes_today || 0
const avgParticipants = stats?.jitsi?.peak_concurrent_users || 1
const monthlyMinutes = dailyCallMinutes * 22
return (monthlyMinutes * avgParticipants * 11) / 1024
}
const getResourceRecommendation = (): string => {
const peakUsers = stats?.jitsi?.peak_concurrent_users || 0
const monthlyGB = calculateMonthlyEstimate()
if (monthlyGB < 10 || peakUsers < 5) {
return 'Starter (1 vCPU, 2GB RAM, 100GB Traffic)'
} else if (monthlyGB < 50 || peakUsers < 20) {
return 'Standard (2 vCPU, 4GB RAM, 500GB Traffic)'
} else if (monthlyGB < 200 || peakUsers < 50) {
return 'Professional (4 vCPU, 8GB RAM, 2TB Traffic)'
} else {
return 'Enterprise (8+ vCPU, 16GB+ RAM, Unlimited Traffic)'
}
}
return (
<div>
{/* Page Purpose */}
<PagePurpose
title={moduleInfo?.module.name || 'Video & Chat'}
purpose={moduleInfo?.module.purpose || 'Matrix & Jitsi Monitoring Dashboard'}
audience={moduleInfo?.module.audience || ['Admins', 'DevOps']}
architecture={{
services: ['synapse (Matrix)', 'jitsi-meet', 'prosody', 'jvb'],
databases: ['PostgreSQL', 'synapse-db'],
}}
collapsible={true}
defaultCollapsed={true}
/>
{/* Quick Actions */}
<div className="flex gap-3 mb-6">
<Link
href="/communication/video-chat/wizard"
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700 transition-colors text-sm font-medium"
>
Test Wizard starten
</Link>
<button
onClick={fetchStats}
disabled={loading}
className="px-4 py-2 border border-slate-300 rounded-lg hover:bg-slate-50 disabled:opacity-50 text-sm"
>
{loading ? 'Lade...' : 'Aktualisieren'}
</button>
</div>
{/* Service Status Overview */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 mb-6">
{/* Matrix Status Card */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-purple-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-purple-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 12h.01M12 12h.01M16 12h.01M21 12c0 4.418-4.03 8-9 8a9.863 9.863 0 01-4.255-.949L3 20l1.395-3.72C3.512 15.042 3 13.574 3 12c0-4.418 4.03-8 9-8s9 3.582 9 8z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-slate-900">Matrix (Synapse)</h3>
<p className="text-sm text-slate-500">E2EE Messaging</p>
</div>
</div>
<span className={getStatusBadge(stats?.matrix.status || 'offline')}>
{stats?.matrix.status || 'offline'}
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<div className="text-2xl font-bold text-slate-900">{stats?.matrix.total_users || 0}</div>
<div className="text-xs text-slate-500">Benutzer</div>
</div>
<div>
<div className="text-2xl font-bold text-slate-900">{stats?.matrix.active_users || 0}</div>
<div className="text-xs text-slate-500">Aktiv</div>
</div>
<div>
<div className="text-2xl font-bold text-slate-900">{stats?.matrix.total_rooms || 0}</div>
<div className="text-xs text-slate-500">Raeume</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-slate-100">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Nachrichten heute</span>
<span className="font-medium">{stats?.matrix.messages_today || 0}</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span className="text-slate-500">Diese Woche</span>
<span className="font-medium">{stats?.matrix.messages_this_week || 0}</span>
</div>
</div>
</div>
{/* Jitsi Status Card */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-blue-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-blue-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
</div>
<div>
<h3 className="font-semibold text-slate-900">Jitsi Meet</h3>
<p className="text-sm text-slate-500">Videokonferenzen</p>
</div>
</div>
<span className={getStatusBadge(stats?.jitsi.status || 'offline')}>
{stats?.jitsi.status || 'offline'}
</span>
</div>
<div className="grid grid-cols-3 gap-4">
<div>
<div className="text-2xl font-bold text-green-600">{stats?.jitsi.active_meetings || 0}</div>
<div className="text-xs text-slate-500">Live Calls</div>
</div>
<div>
<div className="text-2xl font-bold text-slate-900">{stats?.jitsi.total_participants || 0}</div>
<div className="text-xs text-slate-500">Teilnehmer</div>
</div>
<div>
<div className="text-2xl font-bold text-slate-900">{stats?.jitsi.meetings_today || 0}</div>
<div className="text-xs text-slate-500">Calls heute</div>
</div>
</div>
<div className="mt-4 pt-4 border-t border-slate-100">
<div className="flex justify-between text-sm">
<span className="text-slate-500">Durchschnittliche Dauer</span>
<span className="font-medium">{formatDuration(stats?.jitsi.average_duration_minutes || 0)}</span>
</div>
<div className="flex justify-between text-sm mt-1">
<span className="text-slate-500">Peak gleichzeitig</span>
<span className="font-medium">{stats?.jitsi.peak_concurrent_users || 0} Nutzer</span>
</div>
</div>
</div>
</div>
{/* Traffic & Bandwidth Statistics */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<div className="flex items-center gap-3">
<div className="w-10 h-10 bg-emerald-100 rounded-lg flex items-center justify-center">
<svg className="w-6 h-6 text-emerald-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 7h8m0 0v8m0-8l-8 8-4-4-6 6" />
</svg>
</div>
<div>
<h3 className="font-semibold text-slate-900">Traffic & Bandbreite</h3>
<p className="text-sm text-slate-500">SysEleven Ressourcenplanung</p>
</div>
</div>
<span className="px-3 py-1 rounded-full text-xs font-semibold uppercase bg-emerald-100 text-emerald-800">
Live
</span>
</div>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mb-4">
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">Eingehend (heute)</div>
<div className="text-2xl font-bold text-slate-900">
{stats?.traffic?.total?.bandwidth_in_mb?.toFixed(1) || calculateEstimatedTraffic('in').toFixed(1)} MB
</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">Ausgehend (heute)</div>
<div className="text-2xl font-bold text-slate-900">
{stats?.traffic?.total?.bandwidth_out_mb?.toFixed(1) || calculateEstimatedTraffic('out').toFixed(1)} MB
</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">Geschaetzt/Stunde</div>
<div className="text-2xl font-bold text-blue-600">
{stats?.traffic?.jitsi?.estimated_hourly_gb?.toFixed(2) || calculateHourlyEstimate().toFixed(2)} GB
</div>
</div>
<div className="bg-slate-50 rounded-lg p-4">
<div className="text-xs text-slate-500 mb-1">Geschaetzt/Monat</div>
<div className="text-2xl font-bold text-emerald-600">
{stats?.traffic?.total?.estimated_monthly_gb?.toFixed(1) || calculateMonthlyEstimate().toFixed(1)} GB
</div>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{/* Matrix Traffic */}
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<div className="w-3 h-3 bg-purple-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-700">Matrix Messaging</span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Nachrichten/Min</span>
<span className="font-medium">{stats?.traffic?.matrix?.messages_per_minute || Math.round((stats?.matrix?.messages_today || 0) / (new Date().getHours() || 1) / 60)}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Media Uploads heute</span>
<span className="font-medium">{stats?.traffic?.matrix?.media_uploads_today || 0}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Media Groesse</span>
<span className="font-medium">{stats?.traffic?.matrix?.media_size_mb?.toFixed(1) || '0.0'} MB</span>
</div>
</div>
</div>
{/* Jitsi Traffic */}
<div className="border border-slate-200 rounded-lg p-4">
<div className="flex items-center gap-2 mb-3">
<div className="w-3 h-3 bg-blue-500 rounded-full"></div>
<span className="text-sm font-medium text-slate-700">Jitsi Video</span>
</div>
<div className="space-y-2 text-sm">
<div className="flex justify-between">
<span className="text-slate-500">Video Streams aktiv</span>
<span className="font-medium">{stats?.traffic?.jitsi?.video_streams_active || (stats?.jitsi?.total_participants || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Audio Streams aktiv</span>
<span className="font-medium">{stats?.traffic?.jitsi?.audio_streams_active || (stats?.jitsi?.total_participants || 0)}</span>
</div>
<div className="flex justify-between">
<span className="text-slate-500">Bitrate geschaetzt</span>
<span className="font-medium">{((stats?.jitsi?.total_participants || 0) * 1.5).toFixed(1)} Mbps</span>
</div>
</div>
</div>
</div>
{/* SysEleven Recommendation */}
<div className="mt-4 p-4 bg-emerald-50 border border-emerald-200 rounded-lg">
<h4 className="text-sm font-semibold text-emerald-800 mb-2">SysEleven Empfehlung</h4>
<div className="text-sm text-emerald-700">
<p>Basierend auf aktuellem Traffic: <strong>{getResourceRecommendation()}</strong></p>
<p className="mt-1 text-xs text-emerald-600">
Peak Teilnehmer: {stats?.jitsi?.peak_concurrent_users || 0} |
Durchschnittliche Call-Dauer: {stats?.jitsi?.average_duration_minutes?.toFixed(0) || 0} Min. |
Calls heute: {stats?.jitsi?.meetings_today || 0}
</p>
</div>
</div>
</div>
{/* Active Meetings */}
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
<div className="flex items-center justify-between mb-4">
<h3 className="font-semibold text-slate-900">Aktive Meetings</h3>
</div>
{activeMeetings.length === 0 ? (
<div className="text-center py-8 text-slate-500">
<svg className="w-12 h-12 mx-auto mb-3 text-slate-300" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 10l4.553-2.276A1 1 0 0121 8.618v6.764a1 1 0 01-1.447.894L15 14M5 18h8a2 2 0 002-2V8a2 2 0 00-2-2H5a2 2 0 00-2 2v8a2 2 0 002 2z" />
</svg>
<p>Keine aktiven Meetings</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full">
<thead>
<tr className="text-left text-xs text-slate-500 uppercase border-b border-slate-200">
<th className="pb-3 pr-4">Meeting</th>
<th className="pb-3 pr-4">Teilnehmer</th>
<th className="pb-3 pr-4">Gestartet</th>
<th className="pb-3">Dauer</th>
</tr>
</thead>
<tbody className="divide-y divide-slate-100">
{activeMeetings.map((meeting, idx) => (
<tr key={idx} className="text-sm">
<td className="py-3 pr-4">
<div className="font-medium text-slate-900">{meeting.display_name}</div>
<div className="text-xs text-slate-500">{meeting.room_name}</div>
</td>
<td className="py-3 pr-4">
<span className="inline-flex items-center gap-1">
<span className="w-2 h-2 bg-green-500 rounded-full animate-pulse" />
{meeting.participants}
</span>
</td>
<td className="py-3 pr-4 text-slate-500">{formatTimeAgo(meeting.started_at)}</td>
<td className="py-3 font-medium">{formatDuration(meeting.duration_minutes)}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
{/* Recent Chat Rooms & Usage Stats */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Aktive Chat-Raeume</h3>
{recentRooms.length === 0 ? (
<div className="text-center py-6 text-slate-500">
<p>Keine aktiven Raeume</p>
</div>
) : (
<div className="space-y-3">
{recentRooms.slice(0, 5).map((room, idx) => (
<div key={idx} className="flex items-center justify-between p-3 bg-slate-50 rounded-lg">
<div className="flex items-center gap-3">
<div className="w-8 h-8 bg-slate-200 rounded-lg flex items-center justify-center">
<svg className="w-4 h-4 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" />
</svg>
</div>
<div>
<div className="font-medium text-slate-900 text-sm">{room.name}</div>
<div className="text-xs text-slate-500">{room.member_count} Mitglieder</div>
</div>
</div>
<div className="flex items-center gap-2">
<span className={getRoomTypeBadge(room.room_type)}>{room.room_type}</span>
<span className="text-xs text-slate-400">{formatTimeAgo(room.last_activity)}</span>
</div>
</div>
))}
</div>
)}
</div>
{/* Usage Statistics */}
<div className="bg-white rounded-xl border border-slate-200 p-6">
<h3 className="font-semibold text-slate-900 mb-4">Nutzungsstatistiken</h3>
<div className="space-y-4">
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-600">Call-Minuten heute</span>
<span className="font-semibold">{stats?.jitsi.total_minutes_today || 0} Min.</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-blue-600 h-2 rounded-full transition-all"
style={{ width: `${Math.min((stats?.jitsi.total_minutes_today || 0) / 500 * 100, 100)}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-600">Aktive Chat-Raeume</span>
<span className="font-semibold">{stats?.matrix.active_rooms || 0} / {stats?.matrix.total_rooms || 0}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-purple-600 h-2 rounded-full transition-all"
style={{ width: `${stats?.matrix.total_rooms ? ((stats.matrix.active_rooms / stats.matrix.total_rooms) * 100) : 0}%` }}
/>
</div>
</div>
<div>
<div className="flex justify-between text-sm mb-1">
<span className="text-slate-600">Aktive Nutzer</span>
<span className="font-semibold">{stats?.matrix.active_users || 0} / {stats?.matrix.total_users || 0}</span>
</div>
<div className="w-full bg-slate-100 rounded-full h-2">
<div
className="bg-green-600 h-2 rounded-full transition-all"
style={{ width: `${stats?.matrix.total_users ? ((stats.matrix.active_users / stats.matrix.total_users) * 100) : 0}%` }}
/>
</div>
</div>
</div>
{/* Quick Actions */}
<div className="mt-6 pt-4 border-t border-slate-100">
<h4 className="text-sm font-medium text-slate-700 mb-3">Schnellaktionen</h4>
<div className="flex flex-wrap gap-2">
<a
href="http://localhost:8448/_synapse/admin"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 text-sm bg-purple-100 text-purple-700 rounded-lg hover:bg-purple-200 transition-colors"
>
Synapse Admin
</a>
<a
href="http://localhost:8443"
target="_blank"
rel="noopener noreferrer"
className="px-3 py-1.5 text-sm bg-blue-100 text-blue-700 rounded-lg hover:bg-blue-200 transition-colors"
>
Jitsi Meet
</a>
</div>
</div>
</div>
</div>
{/* Connection Info */}
<div className="bg-blue-50 border border-blue-200 rounded-xl p-4">
<div className="flex gap-3">
<svg className="w-5 h-5 text-blue-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-blue-900">Service Konfiguration</h4>
<p className="text-sm text-blue-800 mt-1">
<strong>Matrix Homeserver:</strong> http://localhost:8448 (Synapse)<br />
<strong>Jitsi Meet:</strong> http://localhost:8443<br />
<strong>Auto-Refresh:</strong> Alle 15 Sekunden
</p>
{error && (
<p className="text-sm text-red-600 mt-2">
<strong>Fehler:</strong> {error} - Backend nicht erreichbar
</p>
)}
{stats?.last_updated && (
<p className="text-xs text-blue-600 mt-2">
Letzte Aktualisierung: {new Date(stats.last_updated).toLocaleString('de-DE')}
</p>
)}
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,366 @@
'use client'
/**
* Video & Chat Wizard Page
*
* Interactive learning and testing wizard for Matrix & Jitsi integration
* Migrated from website/app/admin/communication/wizard
*/
import { useState } from 'react'
import Link from 'next/link'
import {
WizardStepper,
WizardNavigation,
EducationCard,
ArchitectureContext,
TestRunner,
TestSummary,
type WizardStep,
type TestCategoryResult,
type FullTestResults,
type EducationContent,
type ArchitectureContextType,
} from '@/components/wizard'
// ==============================================
// Constants
// ==============================================
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
const STEPS: WizardStep[] = [
{ id: 'welcome', name: 'Willkommen', icon: '👋', status: 'pending' },
{ id: 'api-health', name: 'API Status', icon: '💚', status: 'pending', category: 'api-health' },
{ id: 'matrix', name: 'Matrix', icon: '💬', status: 'pending', category: 'matrix' },
{ id: 'jitsi', name: 'Jitsi', icon: '📹', status: 'pending', category: 'jitsi' },
{ id: 'summary', name: 'Zusammenfassung', icon: '📊', status: 'pending' },
]
const EDUCATION_CONTENT: Record<string, EducationContent> = {
'welcome': {
title: 'Willkommen zum Video & Chat Wizard',
content: [
'Sichere Kommunikation ist das Rueckgrat moderner Bildungsplattformen.',
'',
'BreakPilot nutzt zwei Open-Source Systeme:',
'• Matrix Synapse: Dezentraler Messenger (Ende-zu-Ende verschluesselt)',
'• Jitsi Meet: Video-Konferenzen (WebRTC-basiert)',
'',
'Beide Systeme sind DSGVO-konform und self-hosted.',
'',
'In diesem Wizard testen wir:',
'• Matrix Homeserver und Federation',
'• Jitsi Video-Konferenz Server',
'• Integration mit der Schulverwaltung',
],
},
'api-health': {
title: 'Communication API - Backend Integration',
content: [
'Die Communication API verbindet Matrix und Jitsi mit BreakPilot.',
'',
'Funktionen:',
'• Automatische Raum-Erstellung fuer Klassen',
'• Eltern-Lehrer DM-Raeume',
'• Meeting-Planung mit Kalender-Integration',
'• Benachrichtigungen bei neuen Nachrichten',
'',
'Endpunkte:',
'• /api/v1/communication/admin/stats',
'• /api/v1/communication/admin/matrix/users',
'• /api/v1/communication/rooms',
],
},
'matrix': {
title: 'Matrix Synapse - Dezentraler Messenger',
content: [
'Matrix ist ein offenes Protokoll fuer sichere Kommunikation.',
'',
'Vorteile gegenueber WhatsApp/Teams:',
'• Ende-zu-Ende Verschluesselung (E2EE)',
'• Dezentral: Kein Single Point of Failure',
'• Federation: Kommunikation mit anderen Schulen',
'• Self-Hosted: Volle Datenkontrolle',
'',
'Raum-Typen in BreakPilot:',
'• Klassen-Info (Ankuendigungen)',
'• Elternvertreter-Raum',
'• Lehrer-Eltern DM',
'• Fachgruppen',
],
},
'jitsi': {
title: 'Jitsi Meet - Video-Konferenzen',
content: [
'Jitsi ist eine Open-Source Alternative zu Zoom/Teams.',
'',
'Features:',
'• WebRTC: Keine Software-Installation noetig',
'• Bildschirmfreigabe und Whiteboard',
'• Breakout-Raeume fuer Gruppenarbeit',
'• Aufzeichnung (optional, lokal)',
'',
'Anwendungsfaelle:',
'• Elternsprechtage (online)',
'• Fernunterricht bei Schulausfall',
'• Lehrerkonferenzen',
'• Foerdergespraeche',
],
},
'summary': {
title: 'Test-Zusammenfassung',
content: [
'Hier sehen Sie eine Uebersicht aller durchgefuehrten Tests:',
'• Matrix Homeserver Verfuegbarkeit',
'• Jitsi Server Status',
'• API-Integration',
],
},
}
const ARCHITECTURE_CONTEXTS: Record<string, ArchitectureContextType> = {
'api-health': {
layer: 'api',
services: ['backend', 'consent-service'],
dependencies: ['PostgreSQL', 'Matrix Synapse', 'Jitsi'],
dataFlow: ['Browser', 'FastAPI', 'Go Service', 'Matrix/Jitsi'],
},
'matrix': {
layer: 'service',
services: ['matrix'],
dependencies: ['PostgreSQL', 'Federation', 'TURN Server'],
dataFlow: ['Element Client', 'Matrix Synapse', 'Federation', 'PostgreSQL'],
},
'jitsi': {
layer: 'service',
services: ['jitsi'],
dependencies: ['Prosody XMPP', 'JVB', 'TURN/STUN'],
dataFlow: ['Browser', 'Nginx', 'Prosody', 'Jitsi Videobridge'],
},
}
// ==============================================
// Main Component
// ==============================================
export default function VideoChatWizardPage() {
const [currentStep, setCurrentStep] = useState(0)
const [steps, setSteps] = useState<WizardStep[]>(STEPS)
const [categoryResults, setCategoryResults] = useState<Record<string, TestCategoryResult>>({})
const [fullResults, setFullResults] = useState<FullTestResults | null>(null)
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const currentStepData = steps[currentStep]
const isTestStep = currentStepData?.category !== undefined
const isWelcome = currentStepData?.id === 'welcome'
const isSummary = currentStepData?.id === 'summary'
const runCategoryTest = async (category: string) => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${BACKEND_URL}/api/admin/communication-tests/${category}`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const result: TestCategoryResult = await response.json()
setCategoryResults((prev) => ({ ...prev, [category]: result }))
setSteps((prev) =>
prev.map((step) =>
step.category === category
? { ...step, status: result.failed === 0 ? 'completed' : 'failed' }
: step
)
)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const runAllTests = async () => {
setIsLoading(true)
setError(null)
try {
const response = await fetch(`${BACKEND_URL}/api/admin/communication-tests/run-all`, {
method: 'POST',
})
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`)
}
const results: FullTestResults = await response.json()
setFullResults(results)
setSteps((prev) =>
prev.map((step) => {
if (step.category) {
const catResult = results.categories.find((c) => c.category === step.category)
if (catResult) {
return { ...step, status: catResult.failed === 0 ? 'completed' : 'failed' }
}
}
return step
})
)
const newCategoryResults: Record<string, TestCategoryResult> = {}
results.categories.forEach((cat) => {
newCategoryResults[cat.category] = cat
})
setCategoryResults(newCategoryResults)
} catch (err) {
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
} finally {
setIsLoading(false)
}
}
const goToNext = () => {
if (currentStep < steps.length - 1) {
setSteps((prev) =>
prev.map((step, idx) =>
idx === currentStep && step.status === 'pending'
? { ...step, status: 'completed' }
: step
)
)
setCurrentStep((prev) => prev + 1)
}
}
const goToPrev = () => {
if (currentStep > 0) {
setCurrentStep((prev) => prev - 1)
}
}
const handleStepClick = (index: number) => {
if (index <= currentStep || steps[index - 1]?.status !== 'pending') {
setCurrentStep(index)
}
}
return (
<div>
{/* Header */}
<div className="bg-white rounded-lg border border-slate-200 p-4 mb-6 flex items-center justify-between">
<div className="flex items-center">
<span className="text-3xl mr-3">💬</span>
<div>
<h2 className="text-lg font-bold text-gray-800">Video & Chat Test Wizard</h2>
<p className="text-sm text-gray-600">Matrix Messenger & Jitsi Video</p>
</div>
</div>
<Link href="/communication/video-chat" className="text-blue-600 hover:text-blue-800 text-sm">
&larr; Zurueck zu Video & Chat
</Link>
</div>
{/* Stepper */}
<div className="bg-white rounded-lg border border-slate-200 p-6 mb-6">
<WizardStepper steps={steps} currentStep={currentStep} onStepClick={handleStepClick} />
</div>
{/* Content */}
<div className="bg-white rounded-lg border border-slate-200 p-6">
<div className="flex items-center mb-6">
<span className="text-3xl mr-3">{currentStepData?.icon}</span>
<div>
<h2 className="text-xl font-bold text-gray-800">
Schritt {currentStep + 1}: {currentStepData?.name}
</h2>
<p className="text-gray-500 text-sm">
{currentStep + 1} von {steps.length}
</p>
</div>
</div>
<EducationCard content={EDUCATION_CONTENT[currentStepData?.id || '']} />
{isTestStep && currentStepData?.category && ARCHITECTURE_CONTEXTS[currentStepData.category] && (
<ArchitectureContext
context={ARCHITECTURE_CONTEXTS[currentStepData.category]}
currentStep={currentStepData.name}
/>
)}
{error && (
<div className="bg-red-50 border border-red-200 text-red-700 rounded-lg p-4 mb-6">
<strong>Fehler:</strong> {error}
</div>
)}
{isWelcome && (
<div className="text-center py-8">
<button
onClick={goToNext}
className="bg-blue-600 text-white px-8 py-3 rounded-lg font-medium hover:bg-blue-700 transition-colors"
>
Wizard starten
</button>
</div>
)}
{isTestStep && currentStepData?.category && (
<TestRunner
category={currentStepData.category}
categoryResult={categoryResults[currentStepData.category]}
isLoading={isLoading}
onRunTests={() => runCategoryTest(currentStepData.category!)}
/>
)}
{isSummary && (
<div>
{!fullResults ? (
<div className="text-center py-8">
<p className="text-gray-600 mb-4">
Fuehren Sie alle Tests aus um eine Zusammenfassung zu sehen.
</p>
<button
onClick={runAllTests}
disabled={isLoading}
className={`px-6 py-3 rounded-lg font-medium transition-colors ${
isLoading
? 'bg-gray-400 cursor-not-allowed'
: 'bg-blue-600 text-white hover:bg-blue-700'
}`}
>
{isLoading ? 'Alle Tests laufen...' : 'Alle Tests ausfuehren'}
</button>
</div>
) : (
<TestSummary results={fullResults} />
)}
</div>
)}
<WizardNavigation
currentStep={currentStep}
totalSteps={steps.length}
onPrev={goToPrev}
onNext={goToNext}
showNext={!isSummary}
isLoading={isLoading}
/>
</div>
<div className="text-center text-gray-500 text-sm mt-6">
Diese Tests pruefen die Matrix- und Jitsi-Integration.
Bei Fragen wenden Sie sich an das IT-Team.
</div>
</div>
)
}

View File

@@ -0,0 +1,12 @@
'use client'
import { SDKProvider } from '@/lib/sdk/context'
import { CatalogManagerContent } from '@/components/catalog-manager/CatalogManagerContent'
export default function AdminCatalogManagerPage() {
return (
<SDKProvider>
<CatalogManagerContent />
</SDKProvider>
)
}

View File

@@ -0,0 +1,155 @@
'use client'
import { useEffect, useState } from 'react'
import { navigation, metaModules } from '@/lib/navigation'
import { getStoredRole, isCategoryVisibleForRole, RoleId } from '@/lib/roles'
import { CategoryCard } from '@/components/common/ModuleCard'
import { InfoNote } from '@/components/common/InfoBox'
import { ServiceStatus } from '@/components/common/ServiceStatus'
import { NightModeWidget } from '@/components/dashboard/NightModeWidget'
import Link from 'next/link'
interface Stats {
activeDocuments: number
openDSR: number
registeredUsers: number
totalConsents: number
gpuInstances: number
}
export default function DashboardPage() {
const [stats, setStats] = useState<Stats>({
activeDocuments: 0,
openDSR: 0,
registeredUsers: 0,
totalConsents: 0,
gpuInstances: 0,
})
const [loading, setLoading] = useState(true)
const [currentRole, setCurrentRole] = useState<RoleId | null>(null)
useEffect(() => {
const role = getStoredRole()
setCurrentRole(role)
// Load stats
const loadStats = async () => {
try {
const response = await fetch('http://localhost:8081/api/v1/admin/stats')
if (response.ok) {
const data = await response.json()
setStats({
activeDocuments: data.documents_count || 0,
openDSR: data.open_dsr_count || 0,
registeredUsers: data.users_count || 0,
totalConsents: data.consents_count || 0,
gpuInstances: 0,
})
}
} catch (error) {
console.log('Stats not available')
} finally {
setLoading(false)
}
}
loadStats()
}, [])
const statCards = [
{ label: 'Aktive Dokumente', value: stats.activeDocuments, color: 'text-green-600' },
{ label: 'Offene DSR', value: stats.openDSR, color: stats.openDSR > 0 ? 'text-orange-600' : 'text-slate-600' },
{ label: 'Registrierte Nutzer', value: stats.registeredUsers, color: 'text-blue-600' },
{ label: 'Zustimmungen', value: stats.totalConsents, color: 'text-purple-600' },
{ label: 'GPU Instanzen', value: stats.gpuInstances, color: 'text-pink-600' },
]
const visibleCategories = currentRole
? navigation.filter(cat => isCategoryVisibleForRole(cat.id, currentRole))
: navigation
return (
<div>
{/* Stats Grid */}
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-5 gap-4 mb-8">
{statCards.map((stat) => (
<div key={stat.label} className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
<div className={`text-3xl font-bold ${stat.color}`}>
{loading ? '-' : stat.value}
</div>
<div className="text-sm text-slate-500 mt-1">{stat.label}</div>
</div>
))}
</div>
{/* Categories */}
<h2 className="text-lg font-semibold text-slate-900 mb-4">Bereiche</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4 mb-8">
{visibleCategories.map((category) => (
<CategoryCard key={category.id} category={category} />
))}
</div>
{/* Quick Links */}
<h2 className="text-lg font-semibold text-slate-900 mb-4">Schnellzugriff</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4 mb-8">
{metaModules.filter(m => m.id !== 'dashboard').map((module) => (
<Link
key={module.id}
href={module.href}
className="flex items-center gap-3 p-4 bg-white rounded-xl border border-slate-200 hover:border-primary-300 hover:shadow-md transition-all"
>
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
{module.id === 'onboarding' && '📖'}
{module.id === 'backlog' && '📋'}
{module.id === 'rbac' && '👥'}
</div>
<div>
<h3 className="font-medium text-slate-900">{module.name}</h3>
<p className="text-sm text-slate-500">{module.description}</p>
</div>
</Link>
))}
</div>
{/* Infrastructure & System Status */}
<h2 className="text-lg font-semibold text-slate-900 mb-4">Infrastruktur</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-8">
{/* Night Mode Widget */}
<NightModeWidget />
{/* System Status */}
<ServiceStatus />
</div>
{/* Recent Activity */}
<h2 className="text-lg font-semibold text-slate-900 mb-4">Aktivitaet</h2>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Recent DSR */}
<div className="bg-white rounded-xl border border-slate-200 shadow-sm">
<div className="px-4 py-3 border-b border-slate-200 flex items-center justify-between">
<h3 className="font-semibold text-slate-900">Neueste Datenschutzanfragen</h3>
<Link href="/sdk/dsr" className="text-sm text-primary-600 hover:text-primary-700">
Alle anzeigen
</Link>
</div>
<div className="p-4">
<p className="text-sm text-slate-500 text-center py-4">
Keine offenen Anfragen
</p>
</div>
</div>
</div>
{/* Info Box */}
<div className="mt-8">
<InfoNote title="Admin v2 - Neues Frontend">
<p>
Dieses neue Admin-Frontend bietet eine verbesserte Navigation mit Kategorien und Rollen-basiertem Zugriff.
Das alte Admin-Frontend ist weiterhin unter Port 3000 verfuegbar.
</p>
</InfoNote>
</div>
</div>
)
}

View File

@@ -0,0 +1,271 @@
import { DevPortalLayout, ApiEndpoint, CodeBlock, ParameterTable, InfoBox } from '@/components/developers/DevPortalLayout'
export default function ExportApiPage() {
return (
<DevPortalLayout
title="Export API"
description="Exportieren Sie Compliance-Daten in verschiedenen Formaten"
>
<h2>Uebersicht</h2>
<p>
Die Export API ermoeglicht den Download aller Compliance-Daten in
verschiedenen Formaten fuer Audits, Dokumentation und Archivierung.
</p>
<h2>Unterstuetzte Formate</h2>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Format</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Use Case</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3 font-mono">json</td>
<td className="px-4 py-3 text-gray-600">Kompletter State als JSON</td>
<td className="px-4 py-3 text-gray-600">Backup, Migration, API-Integration</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono">pdf</td>
<td className="px-4 py-3 text-gray-600">Formatierter PDF-Report</td>
<td className="px-4 py-3 text-gray-600">Audits, Management-Reports</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono">zip</td>
<td className="px-4 py-3 text-gray-600">Alle Dokumente als ZIP-Archiv</td>
<td className="px-4 py-3 text-gray-600">Vollstaendige Dokumentation</td>
</tr>
</tbody>
</table>
</div>
<h2>GET /export</h2>
<p>Exportiert den aktuellen State im gewuenschten Format.</p>
<h3>Query-Parameter</h3>
<ParameterTable
parameters={[
{
name: 'format',
type: 'string',
required: true,
description: 'Export-Format: json, pdf, zip',
},
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Tenant-ID',
},
{
name: 'sections',
type: 'string',
required: false,
description: 'Kommaseparierte Liste: useCases,risks,controls,dsfa,toms,vvt (default: alle)',
},
{
name: 'phase',
type: 'number',
required: false,
description: 'Nur bestimmte Phase exportieren: 1 oder 2',
},
{
name: 'language',
type: 'string',
required: false,
description: 'Sprache fuer PDF: de, en (default: de)',
},
]}
/>
<h2>JSON Export</h2>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X GET "https://api.breakpilot.io/sdk/v1/export?format=json&tenantId=your-tenant-id" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-o compliance-export.json`}
</CodeBlock>
<h3>Response</h3>
<CodeBlock language="json" filename="compliance-export.json">
{`{
"exportedAt": "2026-02-04T12:00:00Z",
"version": "1.0.0",
"tenantId": "your-tenant-id",
"state": {
"currentPhase": 2,
"currentStep": "dsfa",
"completedSteps": [...],
"useCases": [...],
"risks": [...],
"controls": [...],
"dsfa": {...},
"toms": [...],
"vvt": [...]
},
"meta": {
"completionPercentage": 75,
"lastModified": "2026-02-04T11:55:00Z"
}
}`}
</CodeBlock>
<h2>PDF Export</h2>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X GET "https://api.breakpilot.io/sdk/v1/export?format=pdf&tenantId=your-tenant-id&sections=dsfa,toms" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-o compliance-report.pdf`}
</CodeBlock>
<h3>PDF Inhalt</h3>
<p>Das generierte PDF enthaelt:</p>
<ul>
<li>Deckblatt mit Tenant-Info und Exportdatum</li>
<li>Inhaltsverzeichnis</li>
<li>Executive Summary mit Fortschritt</li>
<li>Use Case Uebersicht</li>
<li>Risikoanalyse mit Matrix-Visualisierung</li>
<li>DSFA (falls generiert)</li>
<li>TOM-Katalog</li>
<li>VVT-Auszug</li>
<li>Checkpoint-Status</li>
</ul>
<InfoBox type="info" title="PDF Styling">
Das PDF folgt einem professionellen Audit-Layout mit Corporate Design.
Enterprise-Kunden koennen ein Custom-Logo und Farbschema konfigurieren.
</InfoBox>
<h2>ZIP Export</h2>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X GET "https://api.breakpilot.io/sdk/v1/export?format=zip&tenantId=your-tenant-id" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-o compliance-export.zip`}
</CodeBlock>
<h3>ZIP Struktur</h3>
<CodeBlock language="text" filename="compliance-export.zip">
{`compliance-export/
├── README.md
├── state.json # Kompletter State
├── summary.pdf # Executive Summary
├── use-cases/
│ ├── uc-1-ki-analyse.json
│ └── uc-2-chatbot.json
├── risks/
│ ├── risk-matrix.pdf
│ └── risks.json
├── documents/
│ ├── dsfa.pdf
│ ├── dsfa.json
│ ├── toms.pdf
│ ├── toms.json
│ ├── vvt.pdf
│ └── vvt.json
├── checkpoints/
│ └── checkpoint-status.json
└── audit-trail/
└── changes.json`}
</CodeBlock>
<h2>SDK Integration</h2>
<CodeBlock language="typescript" filename="export-examples.ts">
{`import { useSDK, exportToPDF, exportToZIP, downloadExport } from '@breakpilot/compliance-sdk'
// Option 1: Ueber den Hook
function ExportButton() {
const { exportState } = useSDK()
const handlePDFExport = async () => {
const blob = await exportState('pdf')
downloadExport(blob, 'compliance-report.pdf')
}
const handleZIPExport = async () => {
const blob = await exportState('zip')
downloadExport(blob, 'compliance-export.zip')
}
const handleJSONExport = async () => {
const blob = await exportState('json')
downloadExport(blob, 'compliance-state.json')
}
return (
<div className="flex gap-2">
<button onClick={handlePDFExport}>PDF Export</button>
<button onClick={handleZIPExport}>ZIP Export</button>
<button onClick={handleJSONExport}>JSON Export</button>
</div>
)
}
// Option 2: Direkte Funktionen
async function exportManually(state: SDKState) {
// PDF generieren
const pdfBlob = await exportToPDF(state)
downloadExport(pdfBlob, \`compliance-\${Date.now()}.pdf\`)
// ZIP generieren
const zipBlob = await exportToZIP(state)
downloadExport(zipBlob, \`compliance-\${Date.now()}.zip\`)
}`}
</CodeBlock>
<h2>Command Bar Integration</h2>
<p>
Exporte sind auch ueber die Command Bar verfuegbar:
</p>
<CodeBlock language="text" filename="Command Bar">
{`Cmd+K → "pdf" → "Als PDF exportieren"
Cmd+K → "zip" → "Als ZIP exportieren"
Cmd+K → "json" → "Als JSON exportieren"`}
</CodeBlock>
<h2>Automatisierte Exports</h2>
<p>
Fuer regelmaessige Backups oder CI/CD-Integration:
</p>
<CodeBlock language="bash" filename="Cron Job">
{`# Taeglicher Backup-Export um 02:00 Uhr
0 2 * * * curl -X GET "https://api.breakpilot.io/sdk/v1/export?format=zip&tenantId=my-tenant" \\
-H "Authorization: Bearer $API_KEY" \\
-o "/backups/compliance-$(date +%Y%m%d).zip"`}
</CodeBlock>
<InfoBox type="warning" title="Dateigröße">
ZIP-Exporte koennen bei umfangreichen States mehrere MB gross werden.
Die API hat ein Timeout von 60 Sekunden. Bei sehr grossen States
verwenden Sie den asynchronen Export-Endpoint (Enterprise).
</InfoBox>
<h2>Fehlerbehandlung</h2>
<CodeBlock language="typescript" filename="error-handling.ts">
{`import { exportState } from '@breakpilot/compliance-sdk'
try {
const blob = await exportState('pdf')
downloadExport(blob, 'report.pdf')
} catch (error) {
if (error.code === 'EMPTY_STATE') {
console.error('Keine Daten zum Exportieren vorhanden')
} else if (error.code === 'GENERATION_FAILED') {
console.error('PDF-Generierung fehlgeschlagen:', error.message)
} else if (error.code === 'TIMEOUT') {
console.error('Export-Timeout - versuchen Sie ZIP fuer grosse States')
} else {
console.error('Unbekannter Fehler:', error)
}
}`}
</CodeBlock>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,381 @@
import { DevPortalLayout, ApiEndpoint, CodeBlock, ParameterTable, InfoBox } from '@/components/developers/DevPortalLayout'
export default function GenerateApiPage() {
return (
<DevPortalLayout
title="Generation API"
description="Automatische Generierung von Compliance-Dokumenten"
>
<h2>Uebersicht</h2>
<p>
Die Generation API nutzt LLM-Technologie (Claude) zur automatischen Erstellung
von Compliance-Dokumenten basierend auf Ihrem SDK-State:
</p>
<ul>
<li><strong>DSFA</strong> - Datenschutz-Folgenabschaetzung</li>
<li><strong>TOM</strong> - Technische und Organisatorische Massnahmen</li>
<li><strong>VVT</strong> - Verarbeitungsverzeichnis nach Art. 30 DSGVO</li>
</ul>
<InfoBox type="info" title="LLM-Model">
Die Generierung verwendet Claude 3.5 Sonnet fuer optimale Qualitaet
bei deutschen Rechtstexten. RAG-Context wird automatisch einbezogen.
</InfoBox>
<h2>POST /generate/dsfa</h2>
<p>Generiert eine Datenschutz-Folgenabschaetzung basierend auf dem aktuellen State.</p>
<h3>Request Body</h3>
<ParameterTable
parameters={[
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Tenant-ID fuer State-Zugriff',
},
{
name: 'useCaseId',
type: 'string',
required: false,
description: 'Optional: Nur fuer bestimmten Use Case generieren',
},
{
name: 'includeRisks',
type: 'boolean',
required: false,
description: 'Risiken aus Risk Matrix einbeziehen (default: true)',
},
{
name: 'includeControls',
type: 'boolean',
required: false,
description: 'Bestehende Controls referenzieren (default: true)',
},
{
name: 'language',
type: 'string',
required: false,
description: 'Sprache: de, en (default: de)',
},
]}
/>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X POST "https://api.breakpilot.io/sdk/v1/generate/dsfa" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"tenantId": "your-tenant-id",
"useCaseId": "uc-ki-kundenanalyse",
"includeRisks": true,
"includeControls": true,
"language": "de"
}'`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"dsfa": {
"id": "dsfa-2026-02-04-abc123",
"version": "1.0",
"status": "DRAFT",
"createdAt": "2026-02-04T12:00:00Z",
"useCase": {
"id": "uc-ki-kundenanalyse",
"name": "KI-gestuetzte Kundenanalyse",
"description": "Analyse von Kundenverhalten mittels ML..."
},
"sections": {
"systematicDescription": {
"title": "1. Systematische Beschreibung",
"content": "Die geplante Verarbeitungstaetigkeit umfasst..."
},
"necessityAssessment": {
"title": "2. Bewertung der Notwendigkeit",
"content": "Die Verarbeitung ist notwendig fuer..."
},
"riskAssessment": {
"title": "3. Risikobewertung",
"risks": [
{
"id": "risk-1",
"title": "Unbefugter Datenzugriff",
"severity": "HIGH",
"likelihood": 3,
"impact": 4,
"description": "...",
"mitigations": ["Verschluesselung", "Zugriffskontrolle"]
}
]
},
"mitigationMeasures": {
"title": "4. Abhilfemassnahmen",
"controls": [...]
},
"stakeholderConsultation": {
"title": "5. Einbeziehung Betroffener",
"content": "..."
},
"dpoOpinion": {
"title": "6. Stellungnahme des DSB",
"content": "Ausstehend - Freigabe erforderlich"
}
},
"conclusion": {
"overallRisk": "MEDIUM",
"recommendation": "PROCEED_WITH_CONDITIONS",
"conditions": [
"Implementierung der TOM-Empfehlungen",
"Regelmaessige Ueberpruefung"
]
}
},
"generationMeta": {
"model": "claude-3.5-sonnet",
"ragContextUsed": true,
"tokensUsed": 4250,
"durationMs": 8500
}
}
}`}
</CodeBlock>
<h2>POST /generate/tom</h2>
<p>Generiert technische und organisatorische Massnahmen.</p>
<h3>Request Body</h3>
<ParameterTable
parameters={[
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Tenant-ID',
},
{
name: 'categories',
type: 'string[]',
required: false,
description: 'TOM-Kategorien: access_control, encryption, pseudonymization, etc.',
},
{
name: 'basedOnRisks',
type: 'boolean',
required: false,
description: 'TOMs basierend auf Risk Matrix generieren (default: true)',
},
]}
/>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X POST "https://api.breakpilot.io/sdk/v1/generate/tom" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"tenantId": "your-tenant-id",
"categories": ["access_control", "encryption", "backup"],
"basedOnRisks": true
}'`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"toms": [
{
"id": "tom-1",
"category": "access_control",
"categoryLabel": "Zugangskontrolle",
"title": "Multi-Faktor-Authentifizierung",
"description": "Implementierung von MFA fuer alle Systemzugaenge",
"technicalMeasures": [
"TOTP-basierte 2FA",
"Hardware Security Keys (FIDO2)"
],
"organizationalMeasures": [
"Schulung der Mitarbeiter",
"Dokumentation der Zugaenge"
],
"article32Reference": "Art. 32 Abs. 1 lit. b DSGVO",
"priority": "HIGH",
"implementationStatus": "PLANNED"
},
{
"id": "tom-2",
"category": "encryption",
"categoryLabel": "Verschluesselung",
"title": "Transportverschluesselung",
"description": "TLS 1.3 fuer alle Datenuebert\\\\ragungen",
"technicalMeasures": [
"TLS 1.3 mit PFS",
"HSTS Header"
],
"organizationalMeasures": [
"Zertifikatsmanagement",
"Regelmaessige Audits"
],
"article32Reference": "Art. 32 Abs. 1 lit. a DSGVO",
"priority": "CRITICAL",
"implementationStatus": "IMPLEMENTED"
}
],
"summary": {
"totalMeasures": 20,
"byCategory": {
"access_control": 5,
"encryption": 4,
"backup": 3,
"monitoring": 4,
"incident_response": 4
},
"implementationProgress": {
"implemented": 12,
"in_progress": 5,
"planned": 3
}
}
}
}`}
</CodeBlock>
<h2>POST /generate/vvt</h2>
<p>Generiert ein Verarbeitungsverzeichnis nach Art. 30 DSGVO.</p>
<h3>Request Body</h3>
<ParameterTable
parameters={[
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Tenant-ID',
},
{
name: 'organizationInfo',
type: 'object',
required: false,
description: 'Organisationsdaten (Name, Anschrift, DSB-Kontakt)',
},
{
name: 'includeRetentionPolicies',
type: 'boolean',
required: false,
description: 'Loeschfristen einbeziehen (default: true)',
},
]}
/>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X POST "https://api.breakpilot.io/sdk/v1/generate/vvt" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-d '{
"tenantId": "your-tenant-id",
"organizationInfo": {
"name": "Beispiel GmbH",
"address": "Musterstrasse 1, 10115 Berlin",
"dpoContact": "datenschutz@beispiel.de"
},
"includeRetentionPolicies": true
}'`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"vvt": {
"id": "vvt-2026-02-04",
"version": "1.0",
"organization": {
"name": "Beispiel GmbH",
"address": "Musterstrasse 1, 10115 Berlin",
"dpoContact": "datenschutz@beispiel.de"
},
"processingActivities": [
{
"id": "pa-1",
"name": "Kundendatenverarbeitung",
"purpose": "Vertragserfuellung und Kundenservice",
"legalBasis": "Art. 6 Abs. 1 lit. b DSGVO",
"dataCategories": ["Kontaktdaten", "Vertragsdaten", "Zahlungsdaten"],
"dataSubjects": ["Kunden", "Interessenten"],
"recipients": ["Zahlungsdienstleister", "Versanddienstleister"],
"thirdCountryTransfers": {
"exists": false,
"countries": [],
"safeguards": null
},
"retentionPeriod": "10 Jahre nach Vertragsende (HGB)",
"technicalMeasures": ["Verschluesselung", "Zugriffskontrolle"]
}
],
"lastUpdated": "2026-02-04T12:00:00Z"
}
}
}`}
</CodeBlock>
<h2>SDK Integration</h2>
<CodeBlock language="typescript" filename="document-generation.ts">
{`import { getSDKBackendClient } from '@breakpilot/compliance-sdk'
const client = getSDKBackendClient()
// DSFA generieren
async function generateDSFA(useCaseId: string) {
const dsfa = await client.generateDSFA({
useCaseId,
includeRisks: true,
includeControls: true,
})
console.log('DSFA generiert:', dsfa.id)
console.log('Gesamtrisiko:', dsfa.conclusion.overallRisk)
return dsfa
}
// TOMs generieren
async function generateTOMs() {
const toms = await client.generateTOM({
categories: ['access_control', 'encryption'],
basedOnRisks: true,
})
console.log(\`\${toms.length} TOMs generiert\`)
return toms
}
// VVT generieren
async function generateVVT() {
const vvt = await client.generateVVT({
organizationInfo: {
name: 'Beispiel GmbH',
address: 'Musterstrasse 1',
dpoContact: 'dpo@beispiel.de',
},
})
console.log(\`VVT mit \${vvt.processingActivities.length} Verarbeitungen\`)
return vvt
}`}
</CodeBlock>
<InfoBox type="warning" title="Kosten">
Die Dokumentengenerierung verbraucht LLM-Tokens. Durchschnittliche Kosten:
DSFA ~5.000 Tokens, TOMs ~3.000 Tokens, VVT ~4.000 Tokens.
Enterprise-Kunden haben unbegrenzte Generierungen.
</InfoBox>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,239 @@
import Link from 'next/link'
import { DevPortalLayout, ApiEndpoint, InfoBox } from '@/components/developers/DevPortalLayout'
export default function ApiReferencePage() {
return (
<DevPortalLayout
title="API Reference"
description="Vollständige REST API Dokumentation"
>
<h2>Base URL</h2>
<p>
Alle API-Endpunkte sind unter folgender Basis-URL erreichbar:
</p>
<div className="bg-gray-100 p-4 rounded-lg font-mono text-sm my-4">
https://api.breakpilot.io/sdk/v1
</div>
<p>
Für Self-Hosted-Installationen verwenden Sie Ihre eigene Domain.
</p>
<h2>Authentifizierung</h2>
<p>
Alle API-Anfragen erfordern einen gültigen API Key im Header:
</p>
<div className="bg-gray-100 p-4 rounded-lg font-mono text-sm my-4">
Authorization: Bearer YOUR_API_KEY
</div>
<InfoBox type="info" title="Tenant-ID">
Die Tenant-ID wird aus dem API Key abgeleitet oder kann explizit
als Query-Parameter oder im Request-Body mitgegeben werden.
</InfoBox>
<h2>API Endpoints</h2>
<h3>State Management</h3>
<p>
Verwalten Sie den SDK-State für Ihren Tenant.
</p>
<ApiEndpoint
method="GET"
path="/state/{tenantId}"
description="Lädt den aktuellen SDK-State für einen Tenant"
/>
<ApiEndpoint
method="POST"
path="/state"
description="Speichert den SDK-State (mit Versionierung)"
/>
<ApiEndpoint
method="DELETE"
path="/state/{tenantId}"
description="Löscht den State für einen Tenant"
/>
<p>
<Link href="/developers/api/state" className="text-blue-600 hover:underline">
Vollständige State API Dokumentation
</Link>
</p>
<h3>RAG Search</h3>
<p>
Durchsuchen Sie den Compliance-Korpus (DSGVO, AI Act, NIS2).
</p>
<ApiEndpoint
method="GET"
path="/rag/search"
description="Semantische Suche im Legal Corpus"
/>
<ApiEndpoint
method="GET"
path="/rag/status"
description="Status des RAG-Systems und Corpus-Informationen"
/>
<p>
<Link href="/developers/api/rag" className="text-blue-600 hover:underline">
Vollständige RAG API Dokumentation
</Link>
</p>
<h3>Document Generation</h3>
<p>
Generieren Sie Compliance-Dokumente automatisch.
</p>
<ApiEndpoint
method="POST"
path="/generate/dsfa"
description="Generiert eine Datenschutz-Folgenabschätzung"
/>
<ApiEndpoint
method="POST"
path="/generate/tom"
description="Generiert technische und organisatorische Maßnahmen"
/>
<ApiEndpoint
method="POST"
path="/generate/vvt"
description="Generiert ein Verarbeitungsverzeichnis"
/>
<p>
<Link href="/developers/api/generate" className="text-blue-600 hover:underline">
Vollständige Generation API Dokumentation
</Link>
</p>
<h3>Export</h3>
<p>
Exportieren Sie den Compliance-Stand in verschiedenen Formaten.
</p>
<ApiEndpoint
method="GET"
path="/export"
description="Exportiert den State (JSON, PDF, ZIP)"
/>
<p>
<Link href="/developers/api/export" className="text-blue-600 hover:underline">
Vollständige Export API Dokumentation
</Link>
</p>
<h2>Response Format</h2>
<p>
Alle Responses folgen einem einheitlichen Format:
</p>
<h3>Erfolgreiche Response</h3>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg font-mono text-sm my-4">
{`{
"success": true,
"data": { ... },
"meta": {
"version": 1,
"timestamp": "2026-02-04T12:00:00Z"
}
}`}
</div>
<h3>Fehler Response</h3>
<div className="bg-gray-900 text-gray-100 p-4 rounded-lg font-mono text-sm my-4">
{`{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Tenant ID is required",
"details": { ... }
}
}`}
</div>
<h2>Error Codes</h2>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">HTTP Status</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Code</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3">400</td>
<td className="px-4 py-3 font-mono text-red-600">VALIDATION_ERROR</td>
<td className="px-4 py-3 text-gray-600">Ungültige Request-Daten</td>
</tr>
<tr>
<td className="px-4 py-3">401</td>
<td className="px-4 py-3 font-mono text-red-600">UNAUTHORIZED</td>
<td className="px-4 py-3 text-gray-600">Fehlender oder ungültiger API Key</td>
</tr>
<tr>
<td className="px-4 py-3">403</td>
<td className="px-4 py-3 font-mono text-red-600">FORBIDDEN</td>
<td className="px-4 py-3 text-gray-600">Keine Berechtigung für diese Ressource</td>
</tr>
<tr>
<td className="px-4 py-3">404</td>
<td className="px-4 py-3 font-mono text-red-600">NOT_FOUND</td>
<td className="px-4 py-3 text-gray-600">Ressource nicht gefunden</td>
</tr>
<tr>
<td className="px-4 py-3">409</td>
<td className="px-4 py-3 font-mono text-red-600">CONFLICT</td>
<td className="px-4 py-3 text-gray-600">Versions-Konflikt (Optimistic Locking)</td>
</tr>
<tr>
<td className="px-4 py-3">429</td>
<td className="px-4 py-3 font-mono text-red-600">RATE_LIMITED</td>
<td className="px-4 py-3 text-gray-600">Zu viele Anfragen</td>
</tr>
<tr>
<td className="px-4 py-3">500</td>
<td className="px-4 py-3 font-mono text-red-600">INTERNAL_ERROR</td>
<td className="px-4 py-3 text-gray-600">Interner Server-Fehler</td>
</tr>
</tbody>
</table>
</div>
<h2>Rate Limits</h2>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Plan</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Requests/Minute</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Requests/Tag</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3">Starter</td>
<td className="px-4 py-3">60</td>
<td className="px-4 py-3">10.000</td>
</tr>
<tr>
<td className="px-4 py-3">Professional</td>
<td className="px-4 py-3">300</td>
<td className="px-4 py-3">100.000</td>
</tr>
<tr>
<td className="px-4 py-3">Enterprise</td>
<td className="px-4 py-3">Unbegrenzt</td>
<td className="px-4 py-3">Unbegrenzt</td>
</tr>
</tbody>
</table>
</div>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,248 @@
import { DevPortalLayout, ApiEndpoint, CodeBlock, ParameterTable, InfoBox } from '@/components/developers/DevPortalLayout'
export default function RAGApiPage() {
return (
<DevPortalLayout
title="RAG API"
description="Semantische Suche im Legal Corpus (DSGVO, AI Act, NIS2)"
>
<h2>Uebersicht</h2>
<p>
Die RAG (Retrieval-Augmented Generation) API ermoeglicht semantische Suche
im Compliance-Korpus. Der Korpus enthaelt:
</p>
<ul>
<li>DSGVO (Datenschutz-Grundverordnung)</li>
<li>AI Act (EU KI-Verordnung)</li>
<li>NIS2 (Netzwerk- und Informationssicherheit)</li>
<li>ePrivacy-Verordnung</li>
<li>Bundesdatenschutzgesetz (BDSG)</li>
</ul>
<InfoBox type="info" title="Embedding-Modell">
Die Suche verwendet BGE-M3 Embeddings fuer praezise semantische Aehnlichkeit.
Die Vektoren werden in Qdrant gespeichert.
</InfoBox>
<h2>GET /rag/search</h2>
<p>Durchsucht den Legal Corpus semantisch.</p>
<h3>Query-Parameter</h3>
<ParameterTable
parameters={[
{
name: 'q',
type: 'string',
required: true,
description: 'Die Suchanfrage (z.B. "Einwilligung personenbezogene Daten")',
},
{
name: 'top_k',
type: 'number',
required: false,
description: 'Anzahl der Ergebnisse (default: 5, max: 20)',
},
{
name: 'corpus',
type: 'string',
required: false,
description: 'Einschraenkung auf bestimmten Corpus: dsgvo, ai_act, nis2, all (default: all)',
},
{
name: 'min_score',
type: 'number',
required: false,
description: 'Minimaler Relevanz-Score 0-1 (default: 0.5)',
},
]}
/>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X GET "https://api.breakpilot.io/sdk/v1/rag/search?q=Einwilligung%20DSGVO&top_k=5" \\
-H "Authorization: Bearer YOUR_API_KEY"`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"query": "Einwilligung DSGVO",
"results": [
{
"id": "dsgvo-art-7",
"title": "Art. 7 DSGVO - Bedingungen fuer die Einwilligung",
"content": "Beruht die Verarbeitung auf einer Einwilligung, muss der Verantwortliche nachweisen koennen, dass die betroffene Person in die Verarbeitung ihrer personenbezogenen Daten eingewilligt hat...",
"corpus": "dsgvo",
"article": "Art. 7",
"score": 0.92,
"metadata": {
"chapter": "II",
"section": "Einwilligung",
"url": "https://dsgvo-gesetz.de/art-7-dsgvo/"
}
},
{
"id": "dsgvo-art-6-1-a",
"title": "Art. 6 Abs. 1 lit. a DSGVO - Einwilligung als Rechtsgrundlage",
"content": "Die Verarbeitung ist nur rechtmaessig, wenn mindestens eine der nachstehenden Bedingungen erfuellt ist: a) Die betroffene Person hat ihre Einwilligung...",
"corpus": "dsgvo",
"article": "Art. 6",
"score": 0.88,
"metadata": {
"chapter": "II",
"section": "Rechtmaessigkeit",
"url": "https://dsgvo-gesetz.de/art-6-dsgvo/"
}
}
],
"total_results": 2,
"search_time_ms": 45
},
"meta": {
"corpus_version": "2026-01",
"embedding_model": "bge-m3"
}
}`}
</CodeBlock>
<h2>GET /rag/status</h2>
<p>Gibt Status-Informationen ueber das RAG-System zurueck.</p>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X GET "https://api.breakpilot.io/sdk/v1/rag/status" \\
-H "Authorization: Bearer YOUR_API_KEY"`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"status": "healthy",
"corpus": {
"dsgvo": {
"documents": 99,
"chunks": 1250,
"last_updated": "2026-01-15T00:00:00Z"
},
"ai_act": {
"documents": 89,
"chunks": 980,
"last_updated": "2026-01-20T00:00:00Z"
},
"nis2": {
"documents": 46,
"chunks": 520,
"last_updated": "2026-01-10T00:00:00Z"
}
},
"embedding_service": {
"status": "online",
"model": "bge-m3",
"dimension": 1024
},
"vector_db": {
"type": "qdrant",
"collections": 3,
"total_vectors": 2750
}
}
}`}
</CodeBlock>
<h2>SDK Integration</h2>
<p>
Verwenden Sie den SDK-Client fuer einfache RAG-Suche:
</p>
<CodeBlock language="typescript" filename="rag-search.ts">
{`import { getSDKBackendClient, isLegalQuery } from '@breakpilot/compliance-sdk'
const client = getSDKBackendClient()
// Pruefen ob die Query rechtliche Inhalte betrifft
if (isLegalQuery('Was ist eine Einwilligung?')) {
// RAG-Suche durchfuehren
const results = await client.search('Einwilligung DSGVO', 5)
results.forEach(result => {
console.log(\`[\${result.corpus}] \${result.title}\`)
console.log(\`Score: \${result.score}\`)
console.log(\`URL: \${result.metadata.url}\`)
console.log('---')
})
}`}
</CodeBlock>
<h2>Keyword-Erkennung</h2>
<p>
Die Funktion <code>isLegalQuery</code> erkennt automatisch rechtliche Anfragen:
</p>
<CodeBlock language="typescript" filename="keyword-detection.ts">
{`import { isLegalQuery } from '@breakpilot/compliance-sdk'
// Gibt true zurueck fuer:
isLegalQuery('DSGVO Art. 5') // true - Artikel-Referenz
isLegalQuery('Einwilligung') // true - DSGVO-Begriff
isLegalQuery('AI Act Hochrisiko') // true - AI Act Begriff
isLegalQuery('NIS2 Richtlinie') // true - NIS2 Referenz
isLegalQuery('personenbezogene Daten') // true - Datenschutz-Begriff
// Gibt false zurueck fuer:
isLegalQuery('Wie ist das Wetter?') // false - Keine rechtliche Anfrage
isLegalQuery('Programmiere mir X') // false - Technische Anfrage`}
</CodeBlock>
<h2>Beispiel: Command Bar Integration</h2>
<CodeBlock language="typescript" filename="command-bar-rag.tsx">
{`import { useState } from 'react'
import { getSDKBackendClient, isLegalQuery } from '@breakpilot/compliance-sdk'
function CommandBarSearch({ query }: { query: string }) {
const [results, setResults] = useState([])
const [loading, setLoading] = useState(false)
useEffect(() => {
if (query.length > 3 && isLegalQuery(query)) {
setLoading(true)
const client = getSDKBackendClient()
client.search(query, 3).then(data => {
setResults(data)
setLoading(false)
})
}
}, [query])
if (!isLegalQuery(query)) return null
return (
<div className="rag-results">
{loading ? (
<p>Suche im Legal Corpus...</p>
) : (
results.map(result => (
<div key={result.id} className="result-card">
<h4>{result.title}</h4>
<p>{result.content.slice(0, 200)}...</p>
<a href={result.metadata.url} target="_blank">
Volltext lesen
</a>
</div>
))
)}
</div>
)
}`}
</CodeBlock>
<InfoBox type="warning" title="Rate Limits">
Die RAG-Suche ist auf 100 Anfragen/Minute (Professional) bzw.
unbegrenzt (Enterprise) limitiert. Implementieren Sie Client-Side
Debouncing fuer Echtzeit-Suche.
</InfoBox>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,266 @@
import { DevPortalLayout, ApiEndpoint, CodeBlock, ParameterTable, InfoBox } from '@/components/developers/DevPortalLayout'
export default function StateApiPage() {
return (
<DevPortalLayout
title="State API"
description="Verwalten Sie den SDK-State für Ihren Tenant"
>
<h2>Übersicht</h2>
<p>
Die State API ermöglicht das Speichern und Abrufen des kompletten SDK-States.
Der State enthält alle Compliance-Daten: Use Cases, Risiken, Controls,
Checkpoints und mehr.
</p>
<InfoBox type="info" title="Versionierung">
Der State wird mit optimistischem Locking gespeichert. Bei jedem Speichern
wird die Version erhöht. Bei Konflikten erhalten Sie einen 409-Fehler.
</InfoBox>
<h2>GET /state/{'{tenantId}'}</h2>
<p>Lädt den aktuellen SDK-State für einen Tenant.</p>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X GET "https://api.breakpilot.io/sdk/v1/state/your-tenant-id" \\
-H "Authorization: Bearer YOUR_API_KEY"`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"version": "1.0.0",
"lastModified": "2026-02-04T12:00:00Z",
"tenantId": "your-tenant-id",
"userId": "user-123",
"subscription": "PROFESSIONAL",
"currentPhase": 1,
"currentStep": "use-case-workshop",
"completedSteps": ["use-case-workshop", "screening"],
"checkpoints": {
"CP-UC": {
"checkpointId": "CP-UC",
"passed": true,
"validatedAt": "2026-02-01T10:00:00Z",
"validatedBy": "user-123",
"errors": [],
"warnings": []
}
},
"useCases": [
{
"id": "uc-1",
"name": "KI-Kundenanalyse",
"description": "...",
"category": "Marketing",
"stepsCompleted": 5,
"assessmentResult": {
"riskLevel": "HIGH",
"dsfaRequired": true,
"aiActClassification": "LIMITED"
}
}
],
"risks": [...],
"controls": [...],
"dsfa": {...},
"toms": [...],
"vvt": [...]
},
"meta": {
"version": 5,
"etag": "W/\\"abc123\\""
}
}`}
</CodeBlock>
<h3>Response (404 Not Found)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "No state found for tenant your-tenant-id"
}
}`}
</CodeBlock>
<h2>POST /state</h2>
<p>Speichert den SDK-State. Unterstützt Versionierung und optimistisches Locking.</p>
<h3>Request Body</h3>
<ParameterTable
parameters={[
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Eindeutige Tenant-ID',
},
{
name: 'userId',
type: 'string',
required: false,
description: 'User-ID für Audit-Trail',
},
{
name: 'state',
type: 'SDKState',
required: true,
description: 'Der komplette SDK-State',
},
{
name: 'expectedVersion',
type: 'number',
required: false,
description: 'Erwartete Version für optimistisches Locking',
},
]}
/>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X POST "https://api.breakpilot.io/sdk/v1/state" \\
-H "Authorization: Bearer YOUR_API_KEY" \\
-H "Content-Type: application/json" \\
-H "If-Match: W/\\"abc123\\"" \\
-d '{
"tenantId": "your-tenant-id",
"userId": "user-123",
"state": {
"currentPhase": 1,
"currentStep": "risks",
"useCases": [...],
"risks": [...]
}
}'`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"tenantId": "your-tenant-id",
"version": 6,
"updatedAt": "2026-02-04T12:05:00Z"
},
"meta": {
"etag": "W/\\"def456\\""
}
}`}
</CodeBlock>
<h3>Response (409 Conflict)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": false,
"error": {
"code": "CONFLICT",
"message": "Version conflict: expected 5, but current is 6",
"details": {
"expectedVersion": 5,
"currentVersion": 6
}
}
}`}
</CodeBlock>
<InfoBox type="warning" title="Konfliktbehandlung">
Bei einem 409-Fehler sollten Sie den State erneut laden, Ihre Änderungen
mergen und erneut speichern.
</InfoBox>
<h2>DELETE /state/{'{tenantId}'}</h2>
<p>Löscht den kompletten State für einen Tenant.</p>
<h3>Request</h3>
<CodeBlock language="bash" filename="cURL">
{`curl -X DELETE "https://api.breakpilot.io/sdk/v1/state/your-tenant-id" \\
-H "Authorization: Bearer YOUR_API_KEY"`}
</CodeBlock>
<h3>Response (200 OK)</h3>
<CodeBlock language="json" filename="Response">
{`{
"success": true,
"data": {
"tenantId": "your-tenant-id",
"deleted": true
}
}`}
</CodeBlock>
<h2>State-Struktur</h2>
<p>Der SDKState enthält alle Compliance-Daten:</p>
<CodeBlock language="typescript" filename="types.ts">
{`interface SDKState {
// Metadata
version: string
lastModified: Date
// Tenant & User
tenantId: string
userId: string
subscription: 'FREE' | 'STARTER' | 'PROFESSIONAL' | 'ENTERPRISE'
// Progress
currentPhase: 1 | 2
currentStep: string
completedSteps: string[]
checkpoints: Record<string, CheckpointStatus>
// Phase 1 Data
useCases: UseCaseAssessment[]
activeUseCase: string | null
screening: ScreeningResult | null
modules: ServiceModule[]
requirements: Requirement[]
controls: Control[]
evidence: Evidence[]
checklist: ChecklistItem[]
risks: Risk[]
// Phase 2 Data
aiActClassification: AIActResult | null
obligations: Obligation[]
dsfa: DSFA | null
toms: TOM[]
retentionPolicies: RetentionPolicy[]
vvt: ProcessingActivity[]
documents: LegalDocument[]
cookieBanner: CookieBannerConfig | null
consents: ConsentRecord[]
dsrConfig: DSRConfig | null
escalationWorkflows: EscalationWorkflow[]
// UI State
preferences: UserPreferences
}`}
</CodeBlock>
<h2>Beispiel: SDK Integration</h2>
<CodeBlock language="typescript" filename="sdk-client.ts">
{`import { getSDKApiClient } from '@breakpilot/compliance-sdk'
const client = getSDKApiClient('your-tenant-id')
// State laden
const state = await client.getState()
console.log('Current step:', state.currentStep)
console.log('Use cases:', state.useCases.length)
// State speichern
await client.saveState({
...state,
currentStep: 'risks',
risks: [...state.risks, newRisk],
})`}
</CodeBlock>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,164 @@
import { DevPortalLayout, InfoBox } from '@/components/developers/DevPortalLayout'
export default function ChangelogPage() {
return (
<DevPortalLayout
title="Changelog"
description="Versionshistorie und Aenderungen des AI Compliance SDK"
>
<h2>Versionierung</h2>
<p>
Das SDK folgt Semantic Versioning (SemVer):
<code className="mx-1">MAJOR.MINOR.PATCH</code>
</p>
<ul>
<li><strong>MAJOR:</strong> Breaking Changes</li>
<li><strong>MINOR:</strong> Neue Features, abwaertskompatibel</li>
<li><strong>PATCH:</strong> Bugfixes</li>
</ul>
{/* Version 1.2.0 */}
<div className="mt-8 border-l-4 border-green-500 pl-4">
<div className="flex items-center gap-3 mb-2">
<span className="px-3 py-1 bg-green-100 text-green-800 rounded-full text-sm font-medium">
v1.2.0
</span>
<span className="text-slate-500 text-sm">2026-02-04</span>
<span className="px-2 py-0.5 bg-green-500 text-white rounded text-xs">Latest</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Neue Features</h3>
<ul className="list-disc list-inside text-slate-700 space-y-1 mb-4">
<li>Demo-Daten Seeding ueber API (nicht mehr hardcodiert)</li>
<li>Playwright E2E Tests fuer alle 19 SDK-Schritte</li>
<li>Command Bar RAG-Integration mit Live-Suche</li>
<li>Developer Portal mit API-Dokumentation</li>
<li>TOM-Katalog mit 20 vorkonfigurierten Massnahmen</li>
<li>VVT-Templates fuer gaengige Verarbeitungstaetigkeiten</li>
</ul>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Verbesserungen</h3>
<ul className="list-disc list-inside text-slate-700 space-y-1 mb-4">
<li>Performance-Optimierung beim State-Loading</li>
<li>Bessere TypeScript-Typen fuer alle Exports</li>
<li>Verbesserte Fehlerbehandlung bei API-Calls</li>
</ul>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Bugfixes</h3>
<ul className="list-disc list-inside text-slate-700 space-y-1">
<li>Fix: Checkpoint-Validierung bei leeren Arrays</li>
<li>Fix: Multi-Tab-Sync bei Safari</li>
<li>Fix: Export-Dateiname mit Sonderzeichen</li>
</ul>
</div>
{/* Version 1.1.0 */}
<div className="mt-8 border-l-4 border-blue-500 pl-4">
<div className="flex items-center gap-3 mb-2">
<span className="px-3 py-1 bg-blue-100 text-blue-800 rounded-full text-sm font-medium">
v1.1.0
</span>
<span className="text-slate-500 text-sm">2026-01-20</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Neue Features</h3>
<ul className="list-disc list-inside text-slate-700 space-y-1 mb-4">
<li>Backend-Sync mit PostgreSQL-Persistierung</li>
<li>SDK Backend (Go) mit RAG + LLM-Integration</li>
<li>Automatische DSFA-Generierung via Claude API</li>
<li>Export nach PDF, ZIP, JSON</li>
</ul>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Verbesserungen</h3>
<ul className="list-disc list-inside text-slate-700 space-y-1 mb-4">
<li>Offline-Support mit localStorage Fallback</li>
<li>Optimistic Locking fuer Konfliktbehandlung</li>
<li>BroadcastChannel fuer Multi-Tab-Sync</li>
</ul>
</div>
{/* Version 1.0.0 */}
<div className="mt-8 border-l-4 border-slate-400 pl-4">
<div className="flex items-center gap-3 mb-2">
<span className="px-3 py-1 bg-slate-100 text-slate-800 rounded-full text-sm font-medium">
v1.0.0
</span>
<span className="text-slate-500 text-sm">2026-01-01</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 mb-2">Initial Release</h3>
<ul className="list-disc list-inside text-slate-700 space-y-1 mb-4">
<li>SDKProvider mit React Context</li>
<li>useSDK Hook mit vollstaendigem State-Zugriff</li>
<li>19-Schritte Compliance-Workflow (Phase 1 + 2)</li>
<li>Checkpoint-Validierung</li>
<li>Risk Matrix mit Score-Berechnung</li>
<li>TypeScript-Support mit allen Types</li>
<li>Utility Functions fuer Navigation und Berechnung</li>
</ul>
</div>
{/* Breaking Changes Notice */}
<InfoBox type="warning" title="Upgrade-Hinweise">
<p className="mb-2">
Bei Major-Version-Updates (z.B. 1.x 2.x) koennen Breaking Changes auftreten.
Pruefen Sie die Migration Guides vor dem Upgrade.
</p>
<p>
Das SDK speichert die State-Version im localStorage. Bei inkompatiblen
Aenderungen wird automatisch eine Migration durchgefuehrt.
</p>
</InfoBox>
<h2>Geplante Features</h2>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Feature</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Version</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Status</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3">Multi-Tenant-Support</td>
<td className="px-4 py-3 font-mono">v1.3.0</td>
<td className="px-4 py-3"><span className="px-2 py-1 bg-yellow-100 text-yellow-800 rounded text-xs">In Entwicklung</span></td>
</tr>
<tr>
<td className="px-4 py-3">Workflow-Customization</td>
<td className="px-4 py-3 font-mono">v1.3.0</td>
<td className="px-4 py-3"><span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">Geplant</span></td>
</tr>
<tr>
<td className="px-4 py-3">Audit-Trail Export</td>
<td className="px-4 py-3 font-mono">v1.4.0</td>
<td className="px-4 py-3"><span className="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">Geplant</span></td>
</tr>
<tr>
<td className="px-4 py-3">White-Label Branding</td>
<td className="px-4 py-3 font-mono">v2.0.0</td>
<td className="px-4 py-3"><span className="px-2 py-1 bg-slate-100 text-slate-800 rounded text-xs">Roadmap</span></td>
</tr>
</tbody>
</table>
</div>
<h2>Feedback & Issues</h2>
<p>
Fuer Bug-Reports und Feature-Requests nutzen Sie bitte:
</p>
<ul>
<li>
<strong>GitHub Issues:</strong>{' '}
<code>github.com/breakpilot/compliance-sdk/issues</code>
</li>
<li>
<strong>Support:</strong>{' '}
<code>support@breakpilot.io</code>
</li>
</ul>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,203 @@
import Link from 'next/link'
import { DevPortalLayout, CodeBlock, InfoBox, ParameterTable } from '@/components/developers/DevPortalLayout'
export default function GettingStartedPage() {
return (
<DevPortalLayout
title="Quick Start"
description="Starten Sie in 5 Minuten mit dem AI Compliance SDK"
>
<h2>1. Installation</h2>
<p>
Installieren Sie das SDK über Ihren bevorzugten Paketmanager:
</p>
<CodeBlock language="bash" filename="Terminal">
{`npm install @breakpilot/compliance-sdk
# oder
yarn add @breakpilot/compliance-sdk
# oder
pnpm add @breakpilot/compliance-sdk`}
</CodeBlock>
<h2>2. API Key erhalten</h2>
<p>
Nach dem Abo-Abschluss erhalten Sie Ihren API Key im{' '}
<Link href="/settings" className="text-blue-600 hover:underline">
Einstellungsbereich
</Link>.
</p>
<InfoBox type="warning" title="Sicherheitshinweis">
Speichern Sie den API Key niemals im Frontend-Code. Verwenden Sie
Umgebungsvariablen auf dem Server.
</InfoBox>
<h2>3. Provider einrichten</h2>
<p>
Wrappen Sie Ihre App mit dem SDKProvider:
</p>
<CodeBlock language="typescript" filename="app/layout.tsx">
{`import { SDKProvider } from '@breakpilot/compliance-sdk'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="de">
<body>
<SDKProvider
tenantId={process.env.TENANT_ID}
apiKey={process.env.BREAKPILOT_API_KEY}
enableBackendSync={true}
>
{children}
</SDKProvider>
</body>
</html>
)
}`}
</CodeBlock>
<h3>Provider Props</h3>
<ParameterTable
parameters={[
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Ihre eindeutige Tenant-ID',
},
{
name: 'apiKey',
type: 'string',
required: false,
description: 'API Key für Backend-Sync (serverseitig)',
},
{
name: 'userId',
type: 'string',
required: false,
description: 'Optional: Benutzer-ID für Audit-Trail',
},
{
name: 'enableBackendSync',
type: 'boolean',
required: false,
description: 'Aktiviert Synchronisation mit dem Backend (default: false)',
},
]}
/>
<h2>4. SDK verwenden</h2>
<p>
Nutzen Sie den useSDK Hook in Ihren Komponenten:
</p>
<CodeBlock language="typescript" filename="components/Dashboard.tsx">
{`'use client'
import { useSDK } from '@breakpilot/compliance-sdk'
export function ComplianceDashboard() {
const {
state,
completionPercentage,
goToStep,
currentStep,
} = useSDK()
return (
<div className="p-6">
<h1 className="text-2xl font-bold">
Compliance Fortschritt: {completionPercentage}%
</h1>
<div className="mt-4">
<p>Aktueller Schritt: {currentStep?.name}</p>
<p>Phase: {state.currentPhase}</p>
<p>Use Cases: {state.useCases.length}</p>
</div>
<div className="mt-6 flex gap-4">
<button
onClick={() => goToStep('use-case-workshop')}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Use Case Workshop
</button>
<button
onClick={() => goToStep('risks')}
className="px-4 py-2 bg-blue-600 text-white rounded"
>
Risikoanalyse
</button>
</div>
</div>
)
}`}
</CodeBlock>
<h2>5. Erste Schritte im Workflow</h2>
<p>
Das SDK führt Sie durch einen 19-Schritte-Workflow in 2 Phasen:
</p>
<div className="my-6 not-prose">
<div className="grid grid-cols-2 gap-4">
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-semibold mb-2">Phase 1: Assessment</h4>
<ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
<li>Use Case Workshop</li>
<li>System Screening</li>
<li>Compliance Modules</li>
<li>Requirements</li>
<li>Controls</li>
<li>Evidence</li>
<li>Audit Checklist</li>
<li>Risk Matrix</li>
</ol>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-semibold mb-2">Phase 2: Dokumentation</h4>
<ol className="text-sm text-gray-600 space-y-1 list-decimal list-inside">
<li>AI Act Klassifizierung</li>
<li>Pflichtenübersicht</li>
<li>DSFA</li>
<li>TOMs</li>
<li>Löschfristen</li>
<li>VVT</li>
<li>Rechtliche Vorlagen</li>
<li>Cookie Banner</li>
<li>Einwilligungen</li>
<li>DSR Portal</li>
<li>Escalations</li>
</ol>
</div>
</div>
</div>
<h2>6. Nächste Schritte</h2>
<ul>
<li>
<Link href="/developers/sdk/configuration" className="text-blue-600 hover:underline">
SDK Konfiguration
</Link>
{' '}- Alle Konfigurationsoptionen
</li>
<li>
<Link href="/developers/api/state" className="text-blue-600 hover:underline">
State API
</Link>
{' '}- Verstehen Sie das State Management
</li>
<li>
<Link href="/developers/guides/phase1" className="text-blue-600 hover:underline">
Phase 1 Guide
</Link>
{' '}- Kompletter Workflow für das Assessment
</li>
</ul>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,227 @@
import Link from 'next/link'
import { DevPortalLayout, InfoBox } from '@/components/developers/DevPortalLayout'
export default function GuidesPage() {
return (
<DevPortalLayout
title="Entwickler-Guides"
description="Schritt-fuer-Schritt Anleitungen fuer die SDK-Integration"
>
<h2>Workflow-Guides</h2>
<p>
Das AI Compliance SDK fuehrt durch einen strukturierten 19-Schritte-Workflow
in zwei Phasen. Diese Guides erklaeren jeden Schritt im Detail.
</p>
<div className="grid grid-cols-1 md:grid-cols-2 gap-6 my-8">
<Link
href="/developers/guides/phase1"
className="block p-6 bg-blue-50 border border-blue-200 rounded-xl hover:border-blue-400 transition-colors"
>
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-blue-600 text-white rounded-xl flex items-center justify-center text-xl font-bold">
1
</div>
<div>
<h3 className="text-lg font-semibold text-blue-900">Phase 1: Assessment</h3>
<p className="text-sm text-blue-600">8 Schritte</p>
</div>
</div>
<p className="text-blue-800 text-sm">
Use Case Workshop, System Screening, Module-Auswahl, Requirements,
Controls, Evidence, Checkliste, Risk Matrix.
</p>
</Link>
<Link
href="/developers/guides/phase2"
className="block p-6 bg-green-50 border border-green-200 rounded-xl hover:border-green-400 transition-colors"
>
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 bg-green-600 text-white rounded-xl flex items-center justify-center text-xl font-bold">
2
</div>
<div>
<h3 className="text-lg font-semibold text-green-900">Phase 2: Dokumentation</h3>
<p className="text-sm text-green-600">11 Schritte</p>
</div>
</div>
<p className="text-green-800 text-sm">
AI Act Klassifizierung, Pflichten, DSFA, TOMs, Loeschfristen,
VVT, Rechtliche Vorlagen, Cookie Banner, DSR Portal.
</p>
</Link>
</div>
<h2>Workflow-Uebersicht</h2>
<div className="my-6 not-prose">
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200">
<h4 className="font-semibold mb-4 text-slate-900">Phase 1: Assessment (8 Schritte)</h4>
<ol className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">01</span>
<p className="font-medium">Use Case Workshop</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">02</span>
<p className="font-medium">System Screening</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">03</span>
<p className="font-medium">Compliance Modules</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">04</span>
<p className="font-medium">Requirements</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">05</span>
<p className="font-medium">Controls</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">06</span>
<p className="font-medium">Evidence</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">07</span>
<p className="font-medium">Audit Checklist</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-blue-600 font-mono">08</span>
<p className="font-medium">Risk Matrix</p>
</li>
</ol>
</div>
<div className="bg-slate-50 rounded-xl p-6 border border-slate-200 mt-4">
<h4 className="font-semibold mb-4 text-slate-900">Phase 2: Dokumentation (11 Schritte)</h4>
<ol className="grid grid-cols-2 md:grid-cols-4 gap-3 text-sm">
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">09</span>
<p className="font-medium">AI Act Klassifizierung</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">10</span>
<p className="font-medium">Pflichtenuebersicht</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">11</span>
<p className="font-medium">DSFA</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">12</span>
<p className="font-medium">TOMs</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">13</span>
<p className="font-medium">Loeschfristen</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">14</span>
<p className="font-medium">VVT</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">15</span>
<p className="font-medium">Rechtliche Vorlagen</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">16</span>
<p className="font-medium">Cookie Banner</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">17</span>
<p className="font-medium">Einwilligungen</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">18</span>
<p className="font-medium">DSR Portal</p>
</li>
<li className="p-3 bg-white rounded-lg border border-slate-200">
<span className="text-green-600 font-mono">19</span>
<p className="font-medium">Escalations</p>
</li>
</ol>
</div>
</div>
<h2>Checkpoints</h2>
<p>
Das SDK validiert den Fortschritt an definierten Checkpoints:
</p>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Checkpoint</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Nach Schritt</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Validierung</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3 font-mono text-blue-600">CP-UC</td>
<td className="px-4 py-3">Use Case Workshop</td>
<td className="px-4 py-3 text-gray-600">Mind. 1 Use Case angelegt</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono text-blue-600">CP-SCREEN</td>
<td className="px-4 py-3">System Screening</td>
<td className="px-4 py-3 text-gray-600">Screening abgeschlossen</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono text-blue-600">CP-CTRL</td>
<td className="px-4 py-3">Controls</td>
<td className="px-4 py-3 text-gray-600">Alle Requirements haben Controls</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono text-blue-600">CP-RISK</td>
<td className="px-4 py-3">Risk Matrix</td>
<td className="px-4 py-3 text-gray-600">Alle Risiken bewertet</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono text-green-600">CP-DSFA</td>
<td className="px-4 py-3">DSFA</td>
<td className="px-4 py-3 text-gray-600">DSFA generiert (falls erforderlich)</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono text-green-600">CP-TOM</td>
<td className="px-4 py-3">TOMs</td>
<td className="px-4 py-3 text-gray-600">Mind. 10 TOMs definiert</td>
</tr>
<tr>
<td className="px-4 py-3 font-mono text-green-600">CP-VVT</td>
<td className="px-4 py-3">VVT</td>
<td className="px-4 py-3 text-gray-600">VVT vollstaendig</td>
</tr>
</tbody>
</table>
</div>
<InfoBox type="info" title="Checkpoint-Navigation">
Nicht bestandene Checkpoints blockieren den Fortschritt zu spaetere Schritte.
Verwenden Sie <code>validateCheckpoint()</code> um den Status zu pruefen.
</InfoBox>
<h2>Best Practices</h2>
<ul>
<li>
<strong>Speichern Sie regelmaessig:</strong> Der State wird automatisch
im localStorage gespeichert, aber aktivieren Sie Backend-Sync fuer
persistente Speicherung.
</li>
<li>
<strong>Nutzen Sie die Command Bar:</strong> Cmd+K oeffnet schnelle
Navigation, Export und RAG-Suche.
</li>
<li>
<strong>Arbeiten Sie Use-Case-zentriert:</strong> Bearbeiten Sie
einen Use Case vollstaendig, bevor Sie zum naechsten wechseln.
</li>
<li>
<strong>Validieren Sie Checkpoints:</strong> Pruefen Sie vor dem
Phasenwechsel, ob alle Checkpoints bestanden sind.
</li>
</ul>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,391 @@
import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/developers/DevPortalLayout'
export default function Phase1GuidePage() {
return (
<DevPortalLayout
title="Phase 1: Assessment Guide"
description="Schritt-fuer-Schritt durch die Assessment-Phase"
>
<h2>Uebersicht Phase 1</h2>
<p>
Phase 1 umfasst die Erfassung und Bewertung Ihrer KI-Anwendungsfaelle.
Am Ende haben Sie eine vollstaendige Risikoanalyse und wissen, welche
Compliance-Dokumente Sie benoetigen.
</p>
<div className="my-6 p-4 bg-blue-50 border border-blue-200 rounded-xl">
<h3 className="text-lg font-semibold text-blue-900 mb-2">Phase 1 Schritte</h3>
<ol className="list-decimal list-inside text-blue-800 space-y-1">
<li>Use Case Workshop</li>
<li>System Screening</li>
<li>Compliance Modules</li>
<li>Requirements</li>
<li>Controls</li>
<li>Evidence</li>
<li>Audit Checklist</li>
<li>Risk Matrix</li>
</ol>
</div>
<h2>Schritt 1: Use Case Workshop</h2>
<p>
Erfassen Sie alle KI-Anwendungsfaelle in Ihrem Unternehmen.
</p>
<h3>Code-Beispiel</h3>
<CodeBlock language="typescript" filename="use-case-workshop.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function UseCaseForm() {
const { updateUseCase, state } = useSDK()
const handleCreateUseCase = async () => {
await updateUseCase({
id: \`uc-\${Date.now()}\`,
name: 'KI-gestuetzte Kundenanalyse',
description: 'Analyse von Kundenverhalten mittels ML',
category: 'Marketing',
department: 'Marketing & Sales',
dataTypes: ['Kundendaten', 'Verhaltensdaten', 'Transaktionen'],
aiCapabilities: ['Profiling', 'Vorhersage'],
stepsCompleted: 0,
})
}
return (
<div>
<h2>Use Cases: {state.useCases.length}</h2>
<button onClick={handleCreateUseCase}>
Use Case hinzufuegen
</button>
{state.useCases.map(uc => (
<div key={uc.id}>
<h3>{uc.name}</h3>
<p>{uc.description}</p>
</div>
))}
</div>
)
}`}
</CodeBlock>
<InfoBox type="info" title="Checkpoint CP-UC">
Nach dem Use Case Workshop muss mindestens ein Use Case angelegt sein,
um zum naechsten Schritt zu gelangen.
</InfoBox>
<h2>Schritt 2: System Screening</h2>
<p>
Das Screening bewertet jeden Use Case hinsichtlich Datenschutz und AI Act.
</p>
<h3>Code-Beispiel</h3>
<CodeBlock language="typescript" filename="screening.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function ScreeningView() {
const { state, dispatch } = useSDK()
const completeScreening = (useCaseId: string, result: ScreeningResult) => {
dispatch({
type: 'UPDATE_USE_CASE',
payload: {
id: useCaseId,
screeningResult: result,
// Ergebnis bestimmt weitere Pflichten
assessmentResult: {
riskLevel: result.aiActRisk,
dsfaRequired: result.dsfaRequired,
aiActClassification: result.aiActClassification,
},
},
})
}
// Screening-Fragen beantworten
const screeningQuestions = [
'Werden personenbezogene Daten verarbeitet?',
'Erfolgt automatisierte Entscheidungsfindung?',
'Werden besondere Datenkategorien verarbeitet?',
'Erfolgt Profiling?',
'Werden Daten in Drittlaender uebermittelt?',
]
return (
<div>
{screeningQuestions.map((question, i) => (
<label key={i} className="block">
<input type="checkbox" />
{question}
</label>
))}
</div>
)
}`}
</CodeBlock>
<h2>Schritt 3: Compliance Modules</h2>
<p>
Basierend auf dem Screening werden relevante Compliance-Module aktiviert.
</p>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Modul</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Aktiviert wenn</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3 font-medium">DSGVO Basis</td>
<td className="px-4 py-3 text-gray-600">Immer (personenbezogene Daten)</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium">DSFA</td>
<td className="px-4 py-3 text-gray-600">Hohes Risiko, Profiling, Art. 9 Daten</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium">AI Act</td>
<td className="px-4 py-3 text-gray-600">KI-basierte Entscheidungen</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium">NIS2</td>
<td className="px-4 py-3 text-gray-600">Kritische Infrastruktur</td>
</tr>
</tbody>
</table>
</div>
<h2>Schritt 4: Requirements</h2>
<p>
Fuer jedes aktivierte Modul werden spezifische Anforderungen generiert.
</p>
<CodeBlock language="typescript" filename="requirements.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function RequirementsView() {
const { state } = useSDK()
// Requirements nach Modul gruppieren
const byModule = state.requirements.reduce((acc, req) => {
const module = req.module || 'general'
if (!acc[module]) acc[module] = []
acc[module].push(req)
return acc
}, {})
return (
<div>
{Object.entries(byModule).map(([module, reqs]) => (
<div key={module}>
<h3>{module}</h3>
<ul>
{reqs.map(req => (
<li key={req.id}>
<strong>{req.title}</strong>
<p>{req.description}</p>
<span>Status: {req.status}</span>
</li>
))}
</ul>
</div>
))}
</div>
)
}`}
</CodeBlock>
<h2>Schritt 5: Controls</h2>
<p>
Definieren Sie Kontrollen fuer jede Anforderung.
</p>
<CodeBlock language="typescript" filename="controls.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function ControlsView() {
const { updateControl, state } = useSDK()
const addControl = (requirementId: string) => {
updateControl({
id: \`ctrl-\${Date.now()}\`,
requirementId,
title: 'Zugriffskontrolle implementieren',
description: 'Role-based access control fuer alle Datenzugaenge',
type: 'TECHNICAL',
status: 'PLANNED',
implementationDate: null,
owner: 'IT-Abteilung',
})
}
return (
<div>
<h2>Controls: {state.controls.length}</h2>
{state.requirements.map(req => (
<div key={req.id}>
<h3>{req.title}</h3>
<p>Controls: {state.controls.filter(c => c.requirementId === req.id).length}</p>
<button onClick={() => addControl(req.id)}>
Control hinzufuegen
</button>
</div>
))}
</div>
)
}`}
</CodeBlock>
<InfoBox type="warning" title="Checkpoint CP-CTRL">
Jede Requirement muss mindestens ein Control haben, bevor Sie
zur Evidence-Phase uebergehen koennen.
</InfoBox>
<h2>Schritt 6: Evidence</h2>
<p>
Dokumentieren Sie Nachweise fuer implementierte Controls.
</p>
<CodeBlock language="typescript" filename="evidence.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function EvidenceUpload({ controlId }: { controlId: string }) {
const { dispatch } = useSDK()
const addEvidence = (file: File) => {
dispatch({
type: 'ADD_EVIDENCE',
payload: {
id: \`ev-\${Date.now()}\`,
controlId,
title: file.name,
type: 'DOCUMENT',
uploadedAt: new Date().toISOString(),
fileType: file.type,
// In Produktion: Upload zu Storage
},
})
}
return (
<input
type="file"
onChange={(e) => e.target.files?.[0] && addEvidence(e.target.files[0])}
/>
)
}`}
</CodeBlock>
<h2>Schritt 7: Audit Checklist</h2>
<p>
Die Checkliste fasst alle Compliance-Punkte zusammen.
</p>
<h2>Schritt 8: Risk Matrix</h2>
<p>
Bewerten Sie alle identifizierten Risiken nach Likelihood und Impact.
</p>
<CodeBlock language="typescript" filename="risk-matrix.tsx">
{`import { useSDK, calculateRiskScore, getRiskSeverityFromScore } from '@breakpilot/compliance-sdk'
function RiskMatrix() {
const { addRisk, state } = useSDK()
const createRisk = () => {
const likelihood = 3 // 1-5
const impact = 4 // 1-5
const score = calculateRiskScore(likelihood, impact) // 12
const severity = getRiskSeverityFromScore(score) // 'HIGH'
addRisk({
id: \`risk-\${Date.now()}\`,
title: 'Unbefugter Datenzugriff',
description: 'Risiko durch unzureichende Zugriffskontrolle',
likelihood,
impact,
inherentScore: score,
severity,
category: 'Security',
mitigations: [],
residualScore: null,
})
}
return (
<div>
<h2>Risiken: {state.risks.length}</h2>
{/* 5x5 Matrix Visualisierung */}
<div className="grid grid-cols-5 gap-1">
{[5,4,3,2,1].map(likelihood => (
[1,2,3,4,5].map(impact => {
const score = likelihood * impact
const risksHere = state.risks.filter(
r => r.likelihood === likelihood && r.impact === impact
)
return (
<div
key={\`\${likelihood}-\${impact}\`}
className={\`p-2 \${score >= 15 ? 'bg-red-500' : score >= 8 ? 'bg-yellow-500' : 'bg-green-500'}\`}
>
{risksHere.length > 0 && (
<span className="text-white">{risksHere.length}</span>
)}
</div>
)
})
))}
</div>
<button onClick={createRisk}>Risiko hinzufuegen</button>
</div>
)
}`}
</CodeBlock>
<InfoBox type="success" title="Phase 1 abgeschlossen">
Nach erfolgreicher Bewertung aller Risiken koennen Sie zu Phase 2
uebergehen. Der Checkpoint CP-RISK validiert, dass alle Risiken
eine Severity-Bewertung haben.
</InfoBox>
<h2>Navigation nach Phase 2</h2>
<CodeBlock language="typescript" filename="phase-transition.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function PhaseTransition() {
const { validateCheckpoint, goToStep, phase1Completion } = useSDK()
const handleContinueToPhase2 = async () => {
// Alle Phase-1-Checkpoints pruefen
const cpRisk = await validateCheckpoint('CP-RISK')
if (cpRisk.passed) {
goToStep('ai-act-classification') // Erster Schritt Phase 2
} else {
console.error('Checkpoint nicht bestanden:', cpRisk.errors)
}
}
return (
<div>
<p>Phase 1 Fortschritt: {phase1Completion}%</p>
{phase1Completion === 100 && (
<button onClick={handleContinueToPhase2}>
Weiter zu Phase 2
</button>
)}
</div>
)
}`}
</CodeBlock>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,377 @@
import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/developers/DevPortalLayout'
export default function Phase2GuidePage() {
return (
<DevPortalLayout
title="Phase 2: Dokumentation Guide"
description="Schritt-fuer-Schritt durch die Dokumentations-Phase"
>
<h2>Uebersicht Phase 2</h2>
<p>
Phase 2 generiert alle erforderlichen Compliance-Dokumente basierend
auf dem Assessment aus Phase 1. Die Dokumente koennen exportiert und
fuer Audits verwendet werden.
</p>
<div className="my-6 p-4 bg-green-50 border border-green-200 rounded-xl">
<h3 className="text-lg font-semibold text-green-900 mb-2">Phase 2 Schritte</h3>
<ol className="list-decimal list-inside text-green-800 space-y-1">
<li>AI Act Klassifizierung</li>
<li>Pflichtenuebersicht</li>
<li>DSFA (Datenschutz-Folgenabschaetzung)</li>
<li>TOMs (Technische/Organisatorische Massnahmen)</li>
<li>Loeschfristen</li>
<li>VVT (Verarbeitungsverzeichnis)</li>
<li>Rechtliche Vorlagen</li>
<li>Cookie Banner</li>
<li>Einwilligungen</li>
<li>DSR Portal</li>
<li>Escalations</li>
</ol>
</div>
<h2>Schritt 9: AI Act Klassifizierung</h2>
<p>
Klassifizieren Sie jeden Use Case nach dem EU AI Act Risikosystem.
</p>
<div className="my-4 overflow-x-auto not-prose">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Risikostufe</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
<th className="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase">Pflichten</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200 text-sm">
<tr>
<td className="px-4 py-3 font-medium text-red-600">Verboten</td>
<td className="px-4 py-3 text-gray-600">Social Scoring, Manipulative KI</td>
<td className="px-4 py-3 text-gray-600">Nicht zulaessig</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium text-orange-600">Hochrisiko</td>
<td className="px-4 py-3 text-gray-600">Biometrie, Medizin, kritische Infrastruktur</td>
<td className="px-4 py-3 text-gray-600">Umfangreiche Dokumentation, Konformitaetsbewertung</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium text-yellow-600">Begrenzt</td>
<td className="px-4 py-3 text-gray-600">Chatbots, Empfehlungssysteme</td>
<td className="px-4 py-3 text-gray-600">Transparenzpflichten</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium text-green-600">Minimal</td>
<td className="px-4 py-3 text-gray-600">Spam-Filter, Spiele</td>
<td className="px-4 py-3 text-gray-600">Freiwillige Verhaltenskodizes</td>
</tr>
</tbody>
</table>
</div>
<CodeBlock language="typescript" filename="ai-act-classification.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
import type { AIActRiskCategory } from '@breakpilot/compliance-sdk'
function AIActClassification() {
const { state, dispatch } = useSDK()
const classifyUseCase = (useCaseId: string, classification: AIActRiskCategory) => {
dispatch({
type: 'UPDATE_USE_CASE',
payload: {
id: useCaseId,
assessmentResult: {
...state.useCases.find(uc => uc.id === useCaseId)?.assessmentResult,
aiActClassification: classification,
},
},
})
// Wenn Hochrisiko, zusaetzliche Pflichten aktivieren
if (classification === 'HIGH_RISK') {
dispatch({
type: 'SET_AI_ACT_RESULT',
payload: {
classification,
conformityRequired: true,
documentationRequired: true,
humanOversightRequired: true,
},
})
}
}
return (
<div>
{state.useCases.map(uc => (
<div key={uc.id}>
<h3>{uc.name}</h3>
<select
value={uc.assessmentResult?.aiActClassification || ''}
onChange={(e) => classifyUseCase(uc.id, e.target.value as AIActRiskCategory)}
>
<option value="">Bitte waehlen...</option>
<option value="PROHIBITED">Verboten</option>
<option value="HIGH_RISK">Hochrisiko</option>
<option value="LIMITED">Begrenzt</option>
<option value="MINIMAL">Minimal</option>
</select>
</div>
))}
</div>
)
}`}
</CodeBlock>
<h2>Schritt 10: Pflichtenuebersicht</h2>
<p>
Basierend auf der Klassifizierung werden alle anwendbaren Pflichten angezeigt.
</p>
<h2>Schritt 11: DSFA</h2>
<p>
Die Datenschutz-Folgenabschaetzung wird automatisch generiert.
</p>
<CodeBlock language="typescript" filename="dsfa.tsx">
{`import { useSDK, getSDKBackendClient } from '@breakpilot/compliance-sdk'
function DSFAGeneration() {
const { state, dispatch } = useSDK()
const [generating, setGenerating] = useState(false)
const generateDSFA = async () => {
setGenerating(true)
const client = getSDKBackendClient()
const dsfa = await client.generateDSFA({
useCases: state.useCases,
risks: state.risks,
controls: state.controls,
})
dispatch({
type: 'SET_DSFA',
payload: dsfa,
})
setGenerating(false)
}
// DSFA nur anzeigen wenn erforderlich
const dsfaRequired = state.useCases.some(
uc => uc.assessmentResult?.dsfaRequired
)
if (!dsfaRequired) {
return <p>Keine DSFA erforderlich fuer die aktuellen Use Cases.</p>
}
return (
<div>
{state.dsfa ? (
<div>
<h3>DSFA generiert</h3>
<p>Status: {state.dsfa.status}</p>
<p>Gesamtrisiko: {state.dsfa.conclusion?.overallRisk}</p>
{/* DSFA-Sektionen anzeigen */}
{Object.entries(state.dsfa.sections || {}).map(([key, section]) => (
<div key={key}>
<h4>{section.title}</h4>
<p>{section.content}</p>
</div>
))}
</div>
) : (
<button onClick={generateDSFA} disabled={generating}>
{generating ? 'Generiere DSFA...' : 'DSFA generieren'}
</button>
)}
</div>
)
}`}
</CodeBlock>
<InfoBox type="info" title="Checkpoint CP-DSFA">
Wenn eine DSFA erforderlich ist (basierend auf Screening), muss diese
generiert werden, bevor Sie fortfahren koennen.
</InfoBox>
<h2>Schritt 12: TOMs</h2>
<p>
Technische und Organisatorische Massnahmen nach Art. 32 DSGVO.
</p>
<CodeBlock language="typescript" filename="toms.tsx">
{`import { useSDK, getSDKBackendClient } from '@breakpilot/compliance-sdk'
function TOMsView() {
const { state, dispatch } = useSDK()
const generateTOMs = async () => {
const client = getSDKBackendClient()
const toms = await client.generateTOM({
risks: state.risks,
controls: state.controls,
})
dispatch({
type: 'SET_TOMS',
payload: toms,
})
}
const tomCategories = [
{ id: 'access_control', label: 'Zugangskontrolle' },
{ id: 'access_rights', label: 'Zugriffskontrolle' },
{ id: 'transfer_control', label: 'Weitergabekontrolle' },
{ id: 'input_control', label: 'Eingabekontrolle' },
{ id: 'availability', label: 'Verfuegbarkeitskontrolle' },
{ id: 'separation', label: 'Trennungsgebot' },
]
return (
<div>
<h2>TOMs: {state.toms.length}</h2>
{tomCategories.map(cat => {
const tomsInCategory = state.toms.filter(t => t.category === cat.id)
return (
<div key={cat.id}>
<h3>{cat.label} ({tomsInCategory.length})</h3>
<ul>
{tomsInCategory.map(tom => (
<li key={tom.id}>
<strong>{tom.title}</strong>
<p>{tom.description}</p>
<span>Status: {tom.implementationStatus}</span>
</li>
))}
</ul>
</div>
)
})}
<button onClick={generateTOMs}>TOMs generieren</button>
</div>
)
}`}
</CodeBlock>
<h2>Schritt 13: Loeschfristen</h2>
<p>
Definieren Sie Aufbewahrungsfristen fuer verschiedene Datenkategorien.
</p>
<h2>Schritt 14: VVT</h2>
<p>
Das Verarbeitungsverzeichnis nach Art. 30 DSGVO.
</p>
<CodeBlock language="typescript" filename="vvt.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function VVTView() {
const { state, dispatch } = useSDK()
const addProcessingActivity = () => {
dispatch({
type: 'ADD_PROCESSING_ACTIVITY',
payload: {
id: \`pa-\${Date.now()}\`,
name: 'Kundendatenverarbeitung',
purpose: 'Vertragserfuellung',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO',
dataCategories: ['Kontaktdaten', 'Vertragsdaten'],
dataSubjects: ['Kunden'],
recipients: [],
retentionPeriod: '10 Jahre',
technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle'],
},
})
}
return (
<div>
<h2>Verarbeitungstaetigkeiten: {state.vvt.length}</h2>
{state.vvt.map(activity => (
<div key={activity.id} className="border p-4 rounded mb-4">
<h3>{activity.name}</h3>
<p><strong>Zweck:</strong> {activity.purpose}</p>
<p><strong>Rechtsgrundlage:</strong> {activity.legalBasis}</p>
<p><strong>Datenkategorien:</strong> {activity.dataCategories.join(', ')}</p>
<p><strong>Betroffene:</strong> {activity.dataSubjects.join(', ')}</p>
<p><strong>Loeschfrist:</strong> {activity.retentionPeriod}</p>
</div>
))}
<button onClick={addProcessingActivity}>
Verarbeitungstaetigkeit hinzufuegen
</button>
</div>
)
}`}
</CodeBlock>
<h2>Schritt 15-19: Weitere Dokumentation</h2>
<p>
Die verbleibenden Schritte umfassen:
</p>
<ul>
<li><strong>Rechtliche Vorlagen:</strong> AGB, Datenschutzerklaerung, etc.</li>
<li><strong>Cookie Banner:</strong> Konfiguration fuer Cookie-Consent</li>
<li><strong>Einwilligungen:</strong> Consent-Management fuer Betroffene</li>
<li><strong>DSR Portal:</strong> Data Subject Request Handling</li>
<li><strong>Escalations:</strong> Eskalationspfade fuer Datenschutzvorfaelle</li>
</ul>
<h2>Export der Dokumentation</h2>
<CodeBlock language="typescript" filename="export-all.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function ExportAll() {
const { exportState, completionPercentage } = useSDK()
const handleExport = async (format: 'pdf' | 'zip' | 'json') => {
const blob = await exportState(format)
// Download ausloesen
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = \`compliance-export.\${format === 'json' ? 'json' : format}\`
a.click()
URL.revokeObjectURL(url)
}
return (
<div>
<h2>Compliance Fortschritt: {completionPercentage}%</h2>
<div className="flex gap-4 mt-4">
<button onClick={() => handleExport('pdf')}>
PDF Export
</button>
<button onClick={() => handleExport('zip')}>
ZIP Export (alle Dokumente)
</button>
<button onClick={() => handleExport('json')}>
JSON Backup
</button>
</div>
</div>
)
}`}
</CodeBlock>
<InfoBox type="success" title="Workflow abgeschlossen">
Nach Abschluss aller 19 Schritte haben Sie eine vollstaendige
Compliance-Dokumentation, die Sie fuer Audits und regulatorische
Anforderungen verwenden koennen.
</InfoBox>
</DevPortalLayout>
)
}

View File

@@ -0,0 +1,9 @@
import { DevPortalLayout } from '@/components/developers/DevPortalLayout'
export default function DevelopersLayout({
children,
}: {
children: React.ReactNode
}) {
return <>{children}</>
}

View File

@@ -0,0 +1,188 @@
import Link from 'next/link'
import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/developers/DevPortalLayout'
import { Zap, Code, Terminal, Book, ArrowRight } from 'lucide-react'
export default function DevelopersPage() {
return (
<DevPortalLayout
title="AI Compliance SDK"
description="Integrieren Sie Compliance-Automation in Ihre Anwendung"
>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 mb-12 not-prose">
<Link
href="/developers/getting-started"
className="group p-6 border border-gray-200 rounded-xl hover:border-blue-300 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center text-blue-600">
<Zap className="w-5 h-5" />
</div>
<h3 className="font-semibold text-gray-900">Quick Start</h3>
</div>
<p className="text-sm text-gray-600 mb-3">
Starten Sie in 5 Minuten mit dem AI Compliance SDK
</p>
<span className="text-sm text-blue-600 group-hover:underline flex items-center gap-1">
Jetzt starten <ArrowRight className="w-4 h-4" />
</span>
</Link>
<Link
href="/developers/api"
className="group p-6 border border-gray-200 rounded-xl hover:border-blue-300 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center text-green-600">
<Terminal className="w-5 h-5" />
</div>
<h3 className="font-semibold text-gray-900">API Reference</h3>
</div>
<p className="text-sm text-gray-600 mb-3">
Vollständige API-Dokumentation aller Endpoints
</p>
<span className="text-sm text-blue-600 group-hover:underline flex items-center gap-1">
API erkunden <ArrowRight className="w-4 h-4" />
</span>
</Link>
<Link
href="/developers/sdk"
className="group p-6 border border-gray-200 rounded-xl hover:border-blue-300 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-purple-100 flex items-center justify-center text-purple-600">
<Code className="w-5 h-5" />
</div>
<h3 className="font-semibold text-gray-900">SDK Documentation</h3>
</div>
<p className="text-sm text-gray-600 mb-3">
TypeScript SDK für React und Next.js
</p>
<span className="text-sm text-blue-600 group-hover:underline flex items-center gap-1">
Dokumentation lesen <ArrowRight className="w-4 h-4" />
</span>
</Link>
<Link
href="/developers/guides"
className="group p-6 border border-gray-200 rounded-xl hover:border-blue-300 hover:shadow-lg transition-all"
>
<div className="flex items-center gap-3 mb-3">
<div className="w-10 h-10 rounded-lg bg-orange-100 flex items-center justify-center text-orange-600">
<Book className="w-5 h-5" />
</div>
<h3 className="font-semibold text-gray-900">Guides</h3>
</div>
<p className="text-sm text-gray-600 mb-3">
Schritt-für-Schritt-Anleitungen und Best Practices
</p>
<span className="text-sm text-blue-600 group-hover:underline flex items-center gap-1">
Guides ansehen <ArrowRight className="w-4 h-4" />
</span>
</Link>
</div>
{/* Installation */}
<h2>Installation</h2>
<CodeBlock language="bash" filename="Terminal">
{`npm install @breakpilot/compliance-sdk
# oder
yarn add @breakpilot/compliance-sdk
# oder
pnpm add @breakpilot/compliance-sdk`}
</CodeBlock>
{/* Quick Example */}
<h2>Schnellstart-Beispiel</h2>
<CodeBlock language="typescript" filename="app.tsx">
{`import { SDKProvider, useSDK } from '@breakpilot/compliance-sdk'
function App() {
return (
<SDKProvider
tenantId="your-tenant-id"
apiKey={process.env.BREAKPILOT_API_KEY}
>
<ComplianceDashboard />
</SDKProvider>
)
}
function ComplianceDashboard() {
const { state, goToStep, completionPercentage } = useSDK()
return (
<div>
<h1>Compliance Status: {completionPercentage}%</h1>
<p>Aktueller Schritt: {state.currentStep}</p>
<button onClick={() => goToStep('risks')}>
Zur Risikoanalyse
</button>
</div>
)
}`}
</CodeBlock>
<InfoBox type="info" title="Voraussetzungen">
<ul className="list-disc list-inside space-y-1">
<li>Node.js 18 oder höher</li>
<li>React 18 oder höher</li>
<li>Breakpilot API Key (erhältlich nach Abo-Abschluss)</li>
</ul>
</InfoBox>
{/* Features */}
<h2>Hauptfunktionen</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4 not-prose">
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">19-Schritt-Workflow</h4>
<p className="text-sm text-gray-600">
Geführter Compliance-Prozess von Use Case bis DSR-Portal
</p>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">RAG-basierte Suche</h4>
<p className="text-sm text-gray-600">
Durchsuchen Sie DSGVO, AI Act, NIS2 mit semantischer Suche
</p>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">Dokumentengenerierung</h4>
<p className="text-sm text-gray-600">
Automatische Erstellung von DSFA, TOMs, VVT
</p>
</div>
<div className="p-4 border border-gray-200 rounded-lg">
<h4 className="font-medium text-gray-900 mb-2">Export</h4>
<p className="text-sm text-gray-600">
PDF, JSON, ZIP-Export für Audits und Dokumentation
</p>
</div>
</div>
{/* Next Steps */}
<h2>Nächste Schritte</h2>
<ol>
<li>
<Link href="/developers/getting-started" className="text-blue-600 hover:underline">
Quick Start Guide
</Link>
{' '}- Erste Integration in 5 Minuten
</li>
<li>
<Link href="/developers/api/state" className="text-blue-600 hover:underline">
State API
</Link>
{' '}- Verstehen Sie das State Management
</li>
<li>
<Link href="/developers/guides/phase1" className="text-blue-600 hover:underline">
Phase 1 Workflow
</Link>
{' '}- Durchlaufen Sie den Compliance-Prozess
</li>
</ol>
</DevPortalLayout>
)
}

Some files were not shown because too many files have changed in this diff Show More