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
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:
425
.claude/CLAUDE.md
Normal file
425
.claude/CLAUDE.md
Normal 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
|
||||
263
.claude/plans/embedding-service-separation.md
Normal file
263
.claude/plans/embedding-service-separation.md
Normal 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
|
||||
614
.claude/rules/abiturkorrektur.md
Normal file
614
.claude/rules/abiturkorrektur.md
Normal 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
|
||||
141
.claude/rules/compliance-checklist.md
Normal file
141
.claude/rules/compliance-checklist.md
Normal 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.
|
||||
91
.claude/rules/documentation.md
Normal file
91
.claude/rules/documentation.md
Normal 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)
|
||||
250
.claude/rules/experimental-dashboard.md
Normal file
250
.claude/rules/experimental-dashboard.md
Normal 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 |
|
||||
295
.claude/rules/multi-agent-architecture.md
Normal file
295
.claude/rules/multi-agent-architecture.md
Normal 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 |
|
||||
297
.claude/rules/night-scheduler.md
Normal file
297
.claude/rules/night-scheduler.md
Normal 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 |
|
||||
|
||||
99
.claude/rules/open-source-policy.md
Normal file
99
.claude/rules/open-source-policy.md
Normal 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
202
.claude/rules/testing.md
Normal 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)
|
||||
205
.claude/rules/vocab-worksheet.md
Normal file
205
.claude/rules/vocab-worksheet.md
Normal 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 |
|
||||
117
.claude/session-status-2026-01-25.md
Normal file
117
.claude/session-status-2026-01-25.md
Normal 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
36
.claude/settings.json
Normal 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/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
82
.claude/settings.local.json
Normal file
82
.claude/settings.local.json
Normal 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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
237
.claude/skills/debug-framework.md
Normal file
237
.claude/skills/debug-framework.md
Normal 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
31
.docker/build-ci-images.sh
Executable 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"
|
||||
51
.docker/python-ci.Dockerfile
Normal file
51
.docker/python-ci.Dockerfile
Normal 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
115
.env.dev
Normal 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
124
.env.example
Normal 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
113
.env.staging
Normal 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
132
.github/dependabot.yml
vendored
Normal 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
503
.github/workflows/ci.yml
vendored
Normal 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
222
.github/workflows/security.yml
vendored
Normal 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
244
.github/workflows/test.yml
vendored
Normal 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
186
.gitignore
vendored
Normal 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
77
.gitleaks.toml
Normal 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
152
.pre-commit-config.yaml
Normal 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
147
.semgrep.yml
Normal 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
66
.trivy.yaml
Normal 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
9
.trivyignore
Normal 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
132
.woodpecker/auto-fix.yml
Normal 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]
|
||||
37
.woodpecker/build-ci-image.yml
Normal file
37
.woodpecker/build-ci-image.yml
Normal 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
161
.woodpecker/integration.yml
Normal 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
669
.woodpecker/main.yml
Normal 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
314
.woodpecker/security.yml
Normal 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
|
||||
2029
AI_COMPLIANCE_SDK_IMPLEMENTATION_PLAN.md
Normal file
2029
AI_COMPLIANCE_SDK_IMPLEMENTATION_PLAN.md
Normal file
File diff suppressed because it is too large
Load Diff
566
BREAKPILOT_CONSENT_MANAGEMENT_PLAN.md
Normal file
566
BREAKPILOT_CONSENT_MANAGEMENT_PLAN.md
Normal 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
473
CONTENT_SERVICE_SETUP.md
Normal 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
427
IMPLEMENTATION_SUMMARY.md
Normal 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!**
|
||||
371
LICENSES/THIRD_PARTY_LICENSES.md
Normal file
371
LICENSES/THIRD_PARTY_LICENSES.md
Normal 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
95
MAC_MINI_SETUP.md
Normal 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
80
Makefile
Normal 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
794
POLICY_VAULT_OVERVIEW.md
Normal 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
|
||||
530
SOURCE_POLICY_IMPLEMENTATION_PLAN.md
Normal file
530
SOURCE_POLICY_IMPLEMENTATION_PLAN.md
Normal 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
44
admin-v2/.dockerignore
Normal 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
57
admin-v2/Dockerfile
Normal 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"]
|
||||
45
admin-v2/ai-compliance-sdk/Dockerfile
Normal file
45
admin-v2/ai-compliance-sdk/Dockerfile
Normal 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"]
|
||||
160
admin-v2/ai-compliance-sdk/cmd/server/main.go
Normal file
160
admin-v2/ai-compliance-sdk/cmd/server/main.go
Normal 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()
|
||||
}
|
||||
}
|
||||
42
admin-v2/ai-compliance-sdk/configs/config.yaml
Normal file
42
admin-v2/ai-compliance-sdk/configs/config.yaml
Normal 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
|
||||
11
admin-v2/ai-compliance-sdk/go.mod
Normal file
11
admin-v2/ai-compliance-sdk/go.mod
Normal 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
|
||||
)
|
||||
327
admin-v2/ai-compliance-sdk/internal/api/checkpoint.go
Normal file
327
admin-v2/ai-compliance-sdk/internal/api/checkpoint.go
Normal 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
|
||||
}
|
||||
365
admin-v2/ai-compliance-sdk/internal/api/generate.go
Normal file
365
admin-v2/ai-compliance-sdk/internal/api/generate.go
Normal 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()}
|
||||
`
|
||||
}
|
||||
182
admin-v2/ai-compliance-sdk/internal/api/rag.go
Normal file
182
admin-v2/ai-compliance-sdk/internal/api/rag.go
Normal 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
|
||||
}
|
||||
96
admin-v2/ai-compliance-sdk/internal/api/router.go
Normal file
96
admin-v2/ai-compliance-sdk/internal/api/router.go
Normal 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)
|
||||
}
|
||||
171
admin-v2/ai-compliance-sdk/internal/api/state.go
Normal file
171
admin-v2/ai-compliance-sdk/internal/api/state.go
Normal 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] + "\""
|
||||
}
|
||||
173
admin-v2/ai-compliance-sdk/internal/db/postgres.go
Normal file
173
admin-v2/ai-compliance-sdk/internal/db/postgres.go
Normal 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
|
||||
}
|
||||
384
admin-v2/ai-compliance-sdk/internal/llm/service.go
Normal file
384
admin-v2/ai-compliance-sdk/internal/llm/service.go
Normal 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
|
||||
}
|
||||
208
admin-v2/ai-compliance-sdk/internal/rag/service.go
Normal file
208
admin-v2/ai-compliance-sdk/internal/rag/service.go
Normal 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]
|
||||
}
|
||||
682
admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
Normal file
682
admin-v2/app/(admin)/ai/agents/[agentId]/page.tsx
Normal 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">
|
||||
← 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>
|
||||
)
|
||||
}
|
||||
779
admin-v2/app/(admin)/ai/agents/architecture/page.tsx
Normal file
779
admin-v2/app/(admin)/ai/agents/architecture/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
390
admin-v2/app/(admin)/ai/agents/page.tsx
Normal file
390
admin-v2/app/(admin)/ai/agents/page.tsx
Normal 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 →
|
||||
</Link>
|
||||
<Link
|
||||
href="/ai/agents/architecture#soul-files"
|
||||
className="text-sm font-medium text-teal-600 hover:text-teal-800"
|
||||
>
|
||||
SOUL-Files verstehen →
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
444
admin-v2/app/(admin)/ai/agents/sessions/page.tsx
Normal file
444
admin-v2/app/(admin)/ai/agents/sessions/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
491
admin-v2/app/(admin)/ai/agents/statistics/page.tsx
Normal file
491
admin-v2/app/(admin)/ai/agents/statistics/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
396
admin-v2/app/(admin)/ai/gpu/page.tsx
Normal file
396
admin-v2/app/(admin)/ai/gpu/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
503
admin-v2/app/(admin)/ai/llm-compare/page.tsx
Normal file
503
admin-v2/app/(admin)/ai/llm-compare/page.tsx
Normal 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 "Vergleich starten", 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>
|
||||
)
|
||||
}
|
||||
1604
admin-v2/app/(admin)/ai/magic-help/page.tsx
Normal file
1604
admin-v2/app/(admin)/ai/magic-help/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1638
admin-v2/app/(admin)/ai/ocr-compare/page.tsx
Normal file
1638
admin-v2/app/(admin)/ai/ocr-compare/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
987
admin-v2/app/(admin)/ai/ocr-labeling/page.tsx
Normal file
987
admin-v2/app/(admin)/ai/ocr-labeling/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
123
admin-v2/app/(admin)/ai/ocr-labeling/types.ts
Normal file
123
admin-v2/app/(admin)/ai/ocr-labeling/types.ts
Normal 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
|
||||
}
|
||||
53
admin-v2/app/(admin)/ai/page.tsx
Normal file
53
admin-v2/app/(admin)/ai/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
524
admin-v2/app/(admin)/ai/quality/page.tsx
Normal file
524
admin-v2/app/(admin)/ai/quality/page.tsx
Normal 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 += `®ulations=${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)}®ulation=${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>
|
||||
)
|
||||
}
|
||||
674
admin-v2/app/(admin)/ai/rag-pipeline/dsfa/page.tsx
Normal file
674
admin-v2/app/(admin)/ai/rag-pipeline/dsfa/page.tsx
Normal 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"
|
||||
>
|
||||
×
|
||||
</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>
|
||||
)
|
||||
}
|
||||
1468
admin-v2/app/(admin)/ai/rag-pipeline/page.tsx
Normal file
1468
admin-v2/app/(admin)/ai/rag-pipeline/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
3272
admin-v2/app/(admin)/ai/rag/page.tsx
Normal file
3272
admin-v2/app/(admin)/ai/rag/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
1526
admin-v2/app/(admin)/ai/test-quality/page.tsx
Normal file
1526
admin-v2/app/(admin)/ai/test-quality/page.tsx
Normal file
File diff suppressed because it is too large
Load Diff
52
admin-v2/app/(admin)/ai/test-quality/types.ts
Normal file
52
admin-v2/app/(admin)/ai/test-quality/types.ts
Normal 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'
|
||||
152
admin-v2/app/(admin)/architecture/page.tsx
Normal file
152
admin-v2/app/(admin)/architecture/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
991
admin-v2/app/(admin)/backlog/page.tsx
Normal file
991
admin-v2/app/(admin)/backlog/page.tsx
Normal 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 "Zuruecksetzen"
|
||||
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>
|
||||
)
|
||||
}
|
||||
912
admin-v2/app/(admin)/communication/alerts/page.tsx
Normal file
912
admin-v2/app/(admin)/communication/alerts/page.tsx
Normal 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} "{rule.conditions[0]?.value}"
|
||||
</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 "Inklusion"</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 "Werbung"</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 "new"</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 "\d{4}"</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>
|
||||
)
|
||||
}
|
||||
946
admin-v2/app/(admin)/communication/mail/page.tsx
Normal file
946
admin-v2/app/(admin)/communication/mail/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
421
admin-v2/app/(admin)/communication/mail/wizard/page.tsx
Normal file
421
admin-v2/app/(admin)/communication/mail/wizard/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
594
admin-v2/app/(admin)/communication/matrix/page.tsx
Normal file
594
admin-v2/app/(admin)/communication/matrix/page.tsx
Normal 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: "{intent.example}"
|
||||
</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>
|
||||
)
|
||||
}
|
||||
50
admin-v2/app/(admin)/communication/page.tsx
Normal file
50
admin-v2/app/(admin)/communication/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
635
admin-v2/app/(admin)/communication/video-chat/page.tsx
Normal file
635
admin-v2/app/(admin)/communication/video-chat/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
366
admin-v2/app/(admin)/communication/video-chat/wizard/page.tsx
Normal file
366
admin-v2/app/(admin)/communication/video-chat/wizard/page.tsx
Normal 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">
|
||||
← 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>
|
||||
)
|
||||
}
|
||||
12
admin-v2/app/(admin)/dashboard/catalog-manager/page.tsx
Normal file
12
admin-v2/app/(admin)/dashboard/catalog-manager/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
155
admin-v2/app/(admin)/dashboard/page.tsx
Normal file
155
admin-v2/app/(admin)/dashboard/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
271
admin-v2/app/(admin)/developers/api/export/page.tsx
Normal file
271
admin-v2/app/(admin)/developers/api/export/page.tsx
Normal 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§ions=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>
|
||||
)
|
||||
}
|
||||
381
admin-v2/app/(admin)/developers/api/generate/page.tsx
Normal file
381
admin-v2/app/(admin)/developers/api/generate/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
239
admin-v2/app/(admin)/developers/api/page.tsx
Normal file
239
admin-v2/app/(admin)/developers/api/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
248
admin-v2/app/(admin)/developers/api/rag/page.tsx
Normal file
248
admin-v2/app/(admin)/developers/api/rag/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
266
admin-v2/app/(admin)/developers/api/state/page.tsx
Normal file
266
admin-v2/app/(admin)/developers/api/state/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
164
admin-v2/app/(admin)/developers/changelog/page.tsx
Normal file
164
admin-v2/app/(admin)/developers/changelog/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
203
admin-v2/app/(admin)/developers/getting-started/page.tsx
Normal file
203
admin-v2/app/(admin)/developers/getting-started/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
227
admin-v2/app/(admin)/developers/guides/page.tsx
Normal file
227
admin-v2/app/(admin)/developers/guides/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
391
admin-v2/app/(admin)/developers/guides/phase1/page.tsx
Normal file
391
admin-v2/app/(admin)/developers/guides/phase1/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
377
admin-v2/app/(admin)/developers/guides/phase2/page.tsx
Normal file
377
admin-v2/app/(admin)/developers/guides/phase2/page.tsx
Normal 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>
|
||||
)
|
||||
}
|
||||
9
admin-v2/app/(admin)/developers/layout.tsx
Normal file
9
admin-v2/app/(admin)/developers/layout.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { DevPortalLayout } from '@/components/developers/DevPortalLayout'
|
||||
|
||||
export default function DevelopersLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode
|
||||
}) {
|
||||
return <>{children}</>
|
||||
}
|
||||
188
admin-v2/app/(admin)/developers/page.tsx
Normal file
188
admin-v2/app/(admin)/developers/page.tsx
Normal 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
Reference in New Issue
Block a user