Compare commits
6 Commits
ci
..
660295e218
| Author | SHA1 | Date | |
|---|---|---|---|
| 660295e218 | |||
| f28244753f | |||
| 1e68ccd4d0 | |||
| 3f7032260b | |||
| 83e32dc289 | |||
| baee45b861 |
+37
-86
@@ -1,57 +1,24 @@
|
||||
# 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
|
||||
## SSH-Verbindung (WICHTIG - IMMER ZUERST LESEN)
|
||||
|
||||
```bash
|
||||
# Verbindung zum Mac Mini im lokalen Netzwerk
|
||||
ssh macmini
|
||||
|
||||
# Projektverzeichnis auf Mac Mini
|
||||
# Projektverzeichnis
|
||||
cd /Users/benjaminadmin/Projekte/breakpilot-pwa
|
||||
|
||||
# Oder direkt (BEVORZUGT für einzelne Befehle):
|
||||
ssh macmini "<befehl>"
|
||||
# Oder direkt:
|
||||
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa && <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
|
||||
@@ -101,7 +68,6 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen:
|
||||
| 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)
|
||||
|
||||
@@ -125,28 +91,22 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen:
|
||||
| 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)
|
||||
### Studio URLs
|
||||
|
||||
| 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 |
|
||||
| URL | Beschreibung |
|
||||
|-----|--------------|
|
||||
| https://macmini/korrektur | Lehrer-Korrekturplattform |
|
||||
| https://macmini:8000/app | Dashboard (alte Version) |
|
||||
|
||||
### 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 |
|
||||
---
|
||||
| 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) |
|
||||
|
||||
---
|
||||
|
||||
@@ -255,8 +215,8 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen:
|
||||
- `night-scheduler`: FastAPI
|
||||
|
||||
### TypeScript/Next.js
|
||||
- `studio-v2`: Next.js 15, React, TailwindCSS
|
||||
- `admin-v2`: Next.js 15, React, TailwindCSS
|
||||
- `studio-v2`: Next.js 14, React, TailwindCSS
|
||||
- `admin-v2`: Next.js 14, React, TailwindCSS, shadcn/ui
|
||||
- `website`: Next.js 14
|
||||
|
||||
### Node.js
|
||||
@@ -275,13 +235,7 @@ breakpilot-pwa/
|
||||
│ ├── 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
|
||||
│ │ └── night-scheduler.md
|
||||
│ └── settings.json
|
||||
├── admin-v2/ # Admin Dashboard (Next.js)
|
||||
├── studio-v2/ # Lehrer-/Schüler-Studio (Next.js)
|
||||
@@ -336,45 +290,42 @@ mkdocs build
|
||||
|
||||
## Häufige Befehle
|
||||
|
||||
### Docker (via SSH auf Mac Mini)
|
||||
### Docker
|
||||
|
||||
```bash
|
||||
# Alle Services starten
|
||||
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml up -d"
|
||||
docker compose 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>"
|
||||
# Einzelnen Service neu bauen
|
||||
docker compose build --no-cache <service-name>
|
||||
docker compose 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>"
|
||||
docker compose logs -f <service-name>
|
||||
|
||||
# Status aller Container
|
||||
ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml ps"
|
||||
docker compose ps
|
||||
```
|
||||
|
||||
**WICHTIG:** Docker-Pfad auf Mac Mini ist `/usr/local/bin/docker` (nicht im Standard-PATH bei SSH).
|
||||
|
||||
### Tests (via SSH)
|
||||
### Tests
|
||||
|
||||
```bash
|
||||
# Go Tests (Consent Service)
|
||||
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa/consent-service && go test -v ./..."
|
||||
cd consent-service && go test -v ./...
|
||||
|
||||
# Python Tests
|
||||
ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa/backend && source venv/bin/activate && pytest -v"
|
||||
cd backend && source venv/bin/activate && pytest -v
|
||||
|
||||
# Mit Coverage
|
||||
pytest --cov=. --cov-report=html
|
||||
```
|
||||
|
||||
### Git
|
||||
### Git (via Gitea)
|
||||
|
||||
```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"
|
||||
# Remote ist localhost weil Gitea im Container läuft
|
||||
git remote -v
|
||||
# origin http://localhost:3003/pilotadmin/breakpilot-pwa.git
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
@@ -1,263 +0,0 @@
|
||||
# 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
|
||||
@@ -1,614 +0,0 @@
|
||||
# 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
|
||||
@@ -1,91 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,250 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,295 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,202 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,205 +0,0 @@
|
||||
# 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 |
|
||||
@@ -1,117 +0,0 @@
|
||||
# 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
|
||||
@@ -1,36 +0,0 @@
|
||||
{
|
||||
"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/**)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
{
|
||||
"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:*)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
#!/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"
|
||||
@@ -1,51 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,115 +0,0 @@
|
||||
# ============================================
|
||||
# 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
@@ -1,124 +0,0 @@
|
||||
# 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
@@ -1,113 +0,0 @@
|
||||
# ============================================
|
||||
# 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
|
||||
@@ -1,132 +0,0 @@
|
||||
# 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):"
|
||||
@@ -1,503 +0,0 @@
|
||||
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
|
||||
@@ -1,222 +0,0 @@
|
||||
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
|
||||
@@ -1,244 +0,0 @@
|
||||
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
@@ -1,186 +0,0 @@
|
||||
# ============================================
|
||||
# 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
|
||||
@@ -1,77 +0,0 @@
|
||||
# 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''',
|
||||
]
|
||||
@@ -1,152 +0,0 @@
|
||||
# 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
@@ -1,147 +0,0 @@
|
||||
# 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
@@ -1,66 +0,0 @@
|
||||
# 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
|
||||
@@ -1,9 +0,0 @@
|
||||
# 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)
|
||||
@@ -1,132 +0,0 @@
|
||||
# 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]
|
||||
@@ -1,37 +0,0 @@
|
||||
# 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!"
|
||||
@@ -1,161 +0,0 @@
|
||||
# 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
|
||||
@@ -1,669 +0,0 @@
|
||||
# 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
|
||||
@@ -1,314 +0,0 @@
|
||||
# Woodpecker CI Security Pipeline
|
||||
# Dedizierte Security-Scans fuer DevSecOps
|
||||
#
|
||||
# Laeuft taeglich via Cron und bei jedem PR
|
||||
|
||||
when:
|
||||
- event: cron
|
||||
cron: "0 3 * * *" # Taeglich um 3:00 Uhr
|
||||
- event: pull_request
|
||||
|
||||
clone:
|
||||
git:
|
||||
image: woodpeckerci/plugin-git
|
||||
settings:
|
||||
depth: 1
|
||||
extra_hosts:
|
||||
- macmini:192.168.178.100
|
||||
|
||||
steps:
|
||||
# ========================================
|
||||
# Static Analysis
|
||||
# ========================================
|
||||
|
||||
semgrep-scan:
|
||||
image: returntocorp/semgrep:latest
|
||||
commands:
|
||||
- semgrep scan --config auto --json -o semgrep-results.json . || true
|
||||
- |
|
||||
if [ -f semgrep-results.json ]; then
|
||||
echo "=== Semgrep Findings ==="
|
||||
cat semgrep-results.json | head -100
|
||||
fi
|
||||
when:
|
||||
event: [pull_request, cron]
|
||||
|
||||
bandit-python:
|
||||
image: python:3.12-slim
|
||||
commands:
|
||||
- pip install --quiet bandit
|
||||
- bandit -r backend/ -f json -o bandit-results.json || true
|
||||
- |
|
||||
if [ -f bandit-results.json ]; then
|
||||
echo "=== Bandit Findings ==="
|
||||
cat bandit-results.json | head -50
|
||||
fi
|
||||
when:
|
||||
event: [pull_request, cron]
|
||||
|
||||
gosec-go:
|
||||
image: securego/gosec:latest
|
||||
commands:
|
||||
- gosec -fmt json -out gosec-consent.json ./consent-service/... || true
|
||||
- gosec -fmt json -out gosec-billing.json ./billing-service/... || true
|
||||
- echo "Go Security Scan abgeschlossen"
|
||||
when:
|
||||
event: [pull_request, cron]
|
||||
|
||||
# ========================================
|
||||
# Secrets Detection
|
||||
# ========================================
|
||||
|
||||
gitleaks-scan:
|
||||
image: zricethezav/gitleaks:latest
|
||||
commands:
|
||||
- gitleaks detect --source . --report-format json --report-path gitleaks-report.json || true
|
||||
- |
|
||||
if [ -s gitleaks-report.json ]; then
|
||||
echo "=== WARNUNG: Potentielle Secrets gefunden ==="
|
||||
cat gitleaks-report.json
|
||||
else
|
||||
echo "Keine Secrets gefunden"
|
||||
fi
|
||||
|
||||
trufflehog-scan:
|
||||
image: trufflesecurity/trufflehog:latest
|
||||
commands:
|
||||
- trufflehog filesystem . --json > trufflehog-results.json 2>&1 || true
|
||||
- echo "TruffleHog Scan abgeschlossen"
|
||||
|
||||
# ========================================
|
||||
# Dependency Vulnerabilities
|
||||
# ========================================
|
||||
|
||||
npm-audit:
|
||||
image: node:20-alpine
|
||||
commands:
|
||||
- cd website && npm audit --json > ../npm-audit-website.json || true
|
||||
- cd ../studio-v2 && npm audit --json > ../npm-audit-studio.json || true
|
||||
- cd ../admin-v2 && npm audit --json > ../npm-audit-admin.json || true
|
||||
- echo "NPM Audit abgeschlossen"
|
||||
when:
|
||||
event: [pull_request, cron]
|
||||
|
||||
pip-audit:
|
||||
image: python:3.12-slim
|
||||
commands:
|
||||
- pip install --quiet pip-audit
|
||||
- pip-audit -r backend/requirements.txt --format json -o pip-audit-backend.json || true
|
||||
- pip-audit -r voice-service/requirements.txt --format json -o pip-audit-voice.json || true
|
||||
- echo "Pip Audit abgeschlossen"
|
||||
when:
|
||||
event: [pull_request, cron]
|
||||
|
||||
go-vulncheck:
|
||||
image: golang:1.21-alpine
|
||||
commands:
|
||||
- go install golang.org/x/vuln/cmd/govulncheck@latest
|
||||
- cd consent-service && govulncheck ./... || true
|
||||
- cd ../billing-service && govulncheck ./... || true
|
||||
- echo "Go Vulncheck abgeschlossen"
|
||||
when:
|
||||
event: [pull_request, cron]
|
||||
|
||||
# ========================================
|
||||
# Container Security
|
||||
# ========================================
|
||||
|
||||
trivy-filesystem:
|
||||
image: aquasec/trivy:latest
|
||||
commands:
|
||||
- trivy fs --severity HIGH,CRITICAL --format json -o trivy-fs.json . || true
|
||||
- echo "Trivy Filesystem Scan abgeschlossen"
|
||||
when:
|
||||
event: cron
|
||||
|
||||
# ========================================
|
||||
# SBOM Generation (taeglich)
|
||||
# ========================================
|
||||
|
||||
daily-sbom:
|
||||
image: anchore/syft:latest
|
||||
commands:
|
||||
- mkdir -p sbom-reports
|
||||
- syft dir:. -o cyclonedx-json > sbom-reports/sbom-full-$(date +%Y%m%d).json
|
||||
- echo "SBOM generiert"
|
||||
when:
|
||||
event: cron
|
||||
|
||||
# ========================================
|
||||
# AUTO-FIX: Dependency Vulnerabilities
|
||||
# Laeuft nur bei Cron (nightly), nicht bei PRs
|
||||
# ========================================
|
||||
|
||||
auto-fix-npm:
|
||||
image: node:20-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- |
|
||||
echo "=== Auto-Fix: NPM Dependencies ==="
|
||||
FIXES_APPLIED=0
|
||||
|
||||
for dir in website studio-v2 admin-v2 h5p-service; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/package.json" ]; then
|
||||
echo "Pruefe $dir..."
|
||||
cd $dir
|
||||
|
||||
# Speichere Hash vor Fix
|
||||
BEFORE=$(md5sum package-lock.json 2>/dev/null || echo "none")
|
||||
|
||||
# npm audit fix (ohne --force fuer sichere Updates)
|
||||
npm audit fix --package-lock-only 2>/dev/null || true
|
||||
|
||||
# Pruefe ob Aenderungen
|
||||
AFTER=$(md5sum package-lock.json 2>/dev/null || echo "none")
|
||||
if [ "$BEFORE" != "$AFTER" ]; then
|
||||
echo " -> Fixes angewendet in $dir"
|
||||
FIXES_APPLIED=$((FIXES_APPLIED + 1))
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
|
||||
echo "NPM Auto-Fix abgeschlossen: $FIXES_APPLIED Projekte aktualisiert"
|
||||
echo "NPM_FIXES=$FIXES_APPLIED" >> /tmp/autofix-results.env
|
||||
when:
|
||||
event: cron
|
||||
|
||||
auto-fix-python:
|
||||
image: python:3.12-slim
|
||||
commands:
|
||||
- apt-get update && apt-get install -y git
|
||||
- pip install --quiet pip-audit
|
||||
- |
|
||||
echo "=== Auto-Fix: Python Dependencies ==="
|
||||
FIXES_APPLIED=0
|
||||
|
||||
for reqfile in backend/requirements.txt voice-service/requirements.txt klausur-service/backend/requirements.txt; do
|
||||
if [ -f "$reqfile" ]; then
|
||||
echo "Pruefe $reqfile..."
|
||||
DIR=$(dirname $reqfile)
|
||||
|
||||
# pip-audit mit --fix (aktualisiert requirements.txt)
|
||||
pip-audit -r $reqfile --fix 2>/dev/null || true
|
||||
|
||||
# Pruefe ob requirements.txt geaendert wurde
|
||||
if git diff --quiet $reqfile 2>/dev/null; then
|
||||
echo " -> Keine Aenderungen in $reqfile"
|
||||
else
|
||||
echo " -> Fixes angewendet in $reqfile"
|
||||
FIXES_APPLIED=$((FIXES_APPLIED + 1))
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Python Auto-Fix abgeschlossen: $FIXES_APPLIED Dateien aktualisiert"
|
||||
echo "PYTHON_FIXES=$FIXES_APPLIED" >> /tmp/autofix-results.env
|
||||
when:
|
||||
event: cron
|
||||
|
||||
auto-fix-go:
|
||||
image: golang:1.21-alpine
|
||||
commands:
|
||||
- apk add --no-cache git
|
||||
- |
|
||||
echo "=== Auto-Fix: Go Dependencies ==="
|
||||
FIXES_APPLIED=0
|
||||
|
||||
for dir in consent-service billing-service school-service edu-search ai-compliance-sdk; do
|
||||
if [ -d "$dir" ] && [ -f "$dir/go.mod" ]; then
|
||||
echo "Pruefe $dir..."
|
||||
cd $dir
|
||||
|
||||
# Go mod tidy und update
|
||||
go get -u ./... 2>/dev/null || true
|
||||
go mod tidy 2>/dev/null || true
|
||||
|
||||
# Pruefe ob go.mod/go.sum geaendert wurden
|
||||
if git diff --quiet go.mod go.sum 2>/dev/null; then
|
||||
echo " -> Keine Aenderungen in $dir"
|
||||
else
|
||||
echo " -> Updates angewendet in $dir"
|
||||
FIXES_APPLIED=$((FIXES_APPLIED + 1))
|
||||
fi
|
||||
|
||||
cd ..
|
||||
fi
|
||||
done
|
||||
|
||||
echo "Go Auto-Fix abgeschlossen: $FIXES_APPLIED Module aktualisiert"
|
||||
echo "GO_FIXES=$FIXES_APPLIED" >> /tmp/autofix-results.env
|
||||
when:
|
||||
event: cron
|
||||
|
||||
# ========================================
|
||||
# Commit & Push Auto-Fixes
|
||||
# ========================================
|
||||
|
||||
commit-security-fixes:
|
||||
image: alpine/git:latest
|
||||
commands:
|
||||
- |
|
||||
echo "=== Commit Security Fixes ==="
|
||||
|
||||
# Git konfigurieren
|
||||
git config --global user.email "security-bot@breakpilot.de"
|
||||
git config --global user.name "Security Bot"
|
||||
git config --global --add safe.directory /woodpecker/src
|
||||
|
||||
# Pruefe ob es Aenderungen gibt
|
||||
if git diff --quiet && git diff --cached --quiet; then
|
||||
echo "Keine Security-Fixes zum Committen"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Zeige was geaendert wurde
|
||||
echo "Geaenderte Dateien:"
|
||||
git status --short
|
||||
|
||||
# Stage alle relevanten Dateien
|
||||
git add -A \
|
||||
*/package-lock.json \
|
||||
*/requirements.txt \
|
||||
*/go.mod \
|
||||
*/go.sum \
|
||||
2>/dev/null || true
|
||||
|
||||
# Commit erstellen
|
||||
TIMESTAMP=$(date +%Y-%m-%d)
|
||||
git commit -m "fix(security): auto-fix vulnerable dependencies [$TIMESTAMP]
|
||||
|
||||
Automatische Sicherheitsupdates durch CI/CD Pipeline:
|
||||
- npm audit fix fuer Node.js Projekte
|
||||
- pip-audit --fix fuer Python Projekte
|
||||
- go get -u fuer Go Module
|
||||
|
||||
Co-Authored-By: Security Bot <security-bot@breakpilot.de>" || echo "Nichts zu committen"
|
||||
|
||||
# Push zum Repository
|
||||
git push origin HEAD:main || echo "Push fehlgeschlagen - manueller Review erforderlich"
|
||||
|
||||
echo "Security-Fixes committed und gepusht"
|
||||
when:
|
||||
event: cron
|
||||
status: success
|
||||
|
||||
# ========================================
|
||||
# Report to Dashboard
|
||||
# ========================================
|
||||
|
||||
update-security-dashboard:
|
||||
image: curlimages/curl:latest
|
||||
commands:
|
||||
- |
|
||||
curl -X POST "http://backend:8000/api/security/scan-results" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{
|
||||
\"scan_type\": \"daily\",
|
||||
\"timestamp\": \"$(date -Iseconds)\",
|
||||
\"tools\": [\"semgrep\", \"bandit\", \"gosec\", \"gitleaks\", \"trivy\"]
|
||||
}" || true
|
||||
when:
|
||||
status: [success, failure]
|
||||
event: cron
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,566 +0,0 @@
|
||||
# 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*
|
||||
@@ -1,473 +0,0 @@
|
||||
# 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
|
||||
@@ -1,427 +0,0 @@
|
||||
# 🎓 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!**
|
||||
@@ -1,371 +0,0 @@
|
||||
# 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*
|
||||
@@ -1,95 +0,0 @@
|
||||
# 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
|
||||
@@ -1,80 +0,0 @@
|
||||
# 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"
|
||||
@@ -1,794 +0,0 @@
|
||||
# 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
|
||||
@@ -1,530 +0,0 @@
|
||||
# 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)
|
||||
```
|
||||
@@ -6,39 +6,3 @@ 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
|
||||
|
||||
@@ -16,13 +16,11 @@ COPY . .
|
||||
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
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
# 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"]
|
||||
@@ -1,160 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
||||
@@ -1,42 +0,0 @@
|
||||
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
|
||||
@@ -1,11 +0,0 @@
|
||||
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
|
||||
)
|
||||
@@ -1,327 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,365 +0,0 @@
|
||||
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()}
|
||||
`
|
||||
}
|
||||
@@ -1,182 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,96 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@@ -1,171 +0,0 @@
|
||||
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] + "\""
|
||||
}
|
||||
@@ -1,173 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,384 +0,0 @@
|
||||
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
|
||||
}
|
||||
@@ -1,208 +0,0 @@
|
||||
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]
|
||||
}
|
||||
@@ -273,52 +273,6 @@ Dein Ziel ist die rechtzeitige Erkennung und Kommunikation relevanter Ereignisse
|
||||
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',
|
||||
|
||||
@@ -94,19 +94,6 @@ const mockAgents: AgentConfig[] = [
|
||||
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()
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -1,396 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -12,7 +12,6 @@
|
||||
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar'
|
||||
|
||||
interface LLMResponse {
|
||||
provider: string
|
||||
@@ -211,24 +210,21 @@ export default function LLMComparePage() {
|
||||
{/* 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."
|
||||
purpose="Vergleichen Sie Antworten verschiedener KI-Provider (OpenAI, Claude, Self-hosted) fuer Qualitaetssicherung. Optimieren Sie Parameter und System Prompts fuer beste Ergebnisse."
|
||||
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' },
|
||||
{ name: 'RAG Management', href: '/ai/rag', description: 'Training Data verwalten' },
|
||||
{ name: 'GPU Infrastruktur', href: '/infrastructure/gpu', description: 'GPU-Ressourcen' },
|
||||
{ name: 'OCR-Labeling', href: '/ai/ocr-labeling', description: 'Handschrift-Training' },
|
||||
]}
|
||||
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">
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,987 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
@@ -1,674 +0,0 @@
|
||||
'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>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -14,7 +14,6 @@
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
import { AIToolsSidebarResponsive } from '@/components/ai/AIToolsSidebar'
|
||||
import type { TestRun, BQASMetrics, TrendData, TabType } from './types'
|
||||
|
||||
// API Configuration - Use internal proxy to avoid CORS issues
|
||||
@@ -1430,17 +1429,14 @@ export default function TestQualityPage() {
|
||||
databases: ['Qdrant', 'PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'Provider-Vergleich' },
|
||||
{ name: 'GPU Infrastruktur', href: '/ai/gpu', description: 'GPU-Ressourcen verwalten' },
|
||||
{ name: 'CI/CD Scheduler', href: '/infrastructure/ci-cd', description: 'Automatische Test-Planung' },
|
||||
{ name: 'RAG Management', href: '/ai/rag', description: 'Training Data & RAG Pipelines' },
|
||||
{ name: 'LLM Vergleich', href: '/ai/llm-compare', description: 'Provider-Vergleich' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* KI-Werkzeuge Sidebar */}
|
||||
<AIToolsSidebarResponsive currentTool="test-quality" />
|
||||
|
||||
{error && (
|
||||
<div className="p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center gap-3">
|
||||
<svg className="w-5 h-5 flex-shrink-0" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
|
||||
@@ -30,7 +30,7 @@ export default function ArchitecturePage() {
|
||||
databases: ['PostgreSQL', 'Qdrant']
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Compliance Hub', href: '/sdk/compliance-hub', description: 'Compliance-Module' },
|
||||
{ name: 'Compliance Hub', href: '/compliance/hub', description: 'Compliance-Module' },
|
||||
{ name: 'AI Hub', href: '/ai', description: 'KI-Module' },
|
||||
]}
|
||||
/>
|
||||
@@ -99,51 +99,47 @@ export default function ArchitecturePage() {
|
||||
<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">
|
||||
<div className="flex items-center gap-3 p-3 border border-slate-200 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">
|
||||
<div className="flex items-center gap-3 p-3 border border-slate-200 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>
|
||||
<span className="text-slate-700">Compliance Hub migriert</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center gap-3 p-3 border border-slate-200 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>
|
||||
<span className="text-slate-700">Consent Verwaltung migriert</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-3 border border-green-200 bg-green-50 rounded-lg">
|
||||
<div className="flex items-center gap-3 p-3 border border-slate-200 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 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">DSR-Modul migrieren</span>
|
||||
<span className="text-xs text-yellow-600 ml-auto">Prioritaet: Hoch</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-slate-700">Cookie-Kategorien 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>
|
||||
<span className="text-slate-700">KI-Module migrieren (LLM Compare, OCR, RAG)</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">Infrastruktur-Module migrieren</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">Alle Module getestet und deployed</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 und dokumentiert</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,831 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* EU-AI-Act Risk Classification Page
|
||||
*
|
||||
* Self-assessment and documentation of AI risk categories according to EU AI Act.
|
||||
* Provides module-by-module risk assessment, warning lines, and exportable documentation.
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// =============================================================================
|
||||
// TYPES
|
||||
// =============================================================================
|
||||
|
||||
type RiskLevel = 'unacceptable' | 'high' | 'limited' | 'minimal'
|
||||
|
||||
interface ModuleAssessment {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
riskLevel: RiskLevel
|
||||
justification: string
|
||||
humanInLoop: boolean
|
||||
transparencyMeasures: string[]
|
||||
aiActArticle: string
|
||||
}
|
||||
|
||||
interface WarningLine {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
wouldTrigger: RiskLevel
|
||||
currentStatus: 'safe' | 'approaching' | 'violated'
|
||||
recommendation: string
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// DATA - Breakpilot Module Assessments
|
||||
// =============================================================================
|
||||
|
||||
const MODULE_ASSESSMENTS: ModuleAssessment[] = [
|
||||
{
|
||||
id: 'text-suggestions',
|
||||
name: 'Textvorschlaege / Formulierhilfen',
|
||||
description: 'KI-generierte Textvorschlaege fuer Gutachten und Feedback',
|
||||
riskLevel: 'minimal',
|
||||
justification: 'Reine Assistenzfunktion ohne Entscheidungswirkung. Lehrer editieren und finalisieren alle Texte.',
|
||||
humanInLoop: true,
|
||||
transparencyMeasures: ['KI-Label auf generierten Texten', 'Editierbare Vorschlaege'],
|
||||
aiActArticle: 'Art. 69 (Freiwillige Verhaltenskodizes)',
|
||||
},
|
||||
{
|
||||
id: 'rag-sources',
|
||||
name: 'RAG-basierte Quellenanzeige',
|
||||
description: 'Retrieval Augmented Generation fuer Lehrplan- und Erwartungshorizont-Referenzen',
|
||||
riskLevel: 'minimal',
|
||||
justification: 'Zitierende Referenzfunktion. Zeigt nur offizielle Quellen an, trifft keine Entscheidungen.',
|
||||
humanInLoop: true,
|
||||
transparencyMeasures: ['Quellenangaben', 'Direkte Links zu Originaldokumenten'],
|
||||
aiActArticle: 'Art. 69 (Freiwillige Verhaltenskodizes)',
|
||||
},
|
||||
{
|
||||
id: 'correction-suggestions',
|
||||
name: 'Korrektur-Vorschlaege',
|
||||
description: 'KI-Vorschlaege fuer Bewertungskriterien und Punktevergabe',
|
||||
riskLevel: 'limited',
|
||||
justification: 'Vorschlaege ohne bindende Wirkung. Lehrkraft behaelt vollstaendige Entscheidungshoheit.',
|
||||
humanInLoop: true,
|
||||
transparencyMeasures: [
|
||||
'Klare Kennzeichnung als KI-Vorschlag',
|
||||
'Begruendung fuer jeden Vorschlag',
|
||||
'Einfache Ueberschreibung moeglich',
|
||||
],
|
||||
aiActArticle: 'Art. 52 (Transparenzpflichten)',
|
||||
},
|
||||
{
|
||||
id: 'eh-matching',
|
||||
name: 'Erwartungshorizont-Abgleich',
|
||||
description: 'Automatischer Abgleich von Schuelerantworten mit Erwartungshorizonten',
|
||||
riskLevel: 'limited',
|
||||
justification: 'Empfehlung, keine Entscheidung. Zeigt Uebereinstimmungen auf, bewertet nicht eigenstaendig.',
|
||||
humanInLoop: true,
|
||||
transparencyMeasures: [
|
||||
'Visualisierung der Matching-Logik',
|
||||
'Confidence-Score angezeigt',
|
||||
'Manuelle Korrektur jederzeit moeglich',
|
||||
],
|
||||
aiActArticle: 'Art. 52 (Transparenzpflichten)',
|
||||
},
|
||||
{
|
||||
id: 'report-drafts',
|
||||
name: 'Zeugnis-Textentwurf',
|
||||
description: 'KI-generierte Entwuerfe fuer Zeugnistexte und Beurteilungen',
|
||||
riskLevel: 'limited',
|
||||
justification: 'Entwurf, der von der Lehrkraft finalisiert wird. Keine automatische Uebernahme.',
|
||||
humanInLoop: true,
|
||||
transparencyMeasures: [
|
||||
'Entwurf-Wasserzeichen',
|
||||
'Pflicht zur manuellen Freigabe',
|
||||
'Vollstaendig editierbar',
|
||||
],
|
||||
aiActArticle: 'Art. 52 (Transparenzpflichten)',
|
||||
},
|
||||
{
|
||||
id: 'edu-search',
|
||||
name: 'Bildungssuche (edu-search)',
|
||||
description: 'Semantische Suche in Lehrplaenen und Bildungsmaterialien',
|
||||
riskLevel: 'minimal',
|
||||
justification: 'Informationsretrieval ohne Bewertungsfunktion. Reine Suchfunktion.',
|
||||
humanInLoop: true,
|
||||
transparencyMeasures: ['Quellenangaben', 'Ranking-Transparenz'],
|
||||
aiActArticle: 'Art. 69 (Freiwillige Verhaltenskodizes)',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// DATA - Warning Lines (What we must never build)
|
||||
// =============================================================================
|
||||
|
||||
const WARNING_LINES: WarningLine[] = [
|
||||
{
|
||||
id: 'auto-grading',
|
||||
title: 'Automatische Notenvergabe',
|
||||
description: 'KI berechnet und vergibt Noten ohne menschliche Pruefung',
|
||||
wouldTrigger: 'high',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Noten immer als Vorschlag, nie als finale Entscheidung',
|
||||
},
|
||||
{
|
||||
id: 'student-classification',
|
||||
title: 'Schuelerklassifikation',
|
||||
description: 'Automatische Einteilung in Leistungsgruppen (leistungsstark/schwach)',
|
||||
wouldTrigger: 'high',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Keine automatische Kategorisierung von Schuelern implementieren',
|
||||
},
|
||||
{
|
||||
id: 'promotion-decisions',
|
||||
title: 'Versetzungsentscheidungen',
|
||||
description: 'Automatisierte Logik fuer Versetzung/Nichtversetzung',
|
||||
wouldTrigger: 'high',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Versetzungsentscheidungen ausschliesslich bei Lehrkraeften belassen',
|
||||
},
|
||||
{
|
||||
id: 'unreviewed-assessments',
|
||||
title: 'Ungeprueft freigegebene Bewertungen',
|
||||
description: 'KI-Bewertungen ohne menschliche Kontrolle an Schueler/Eltern',
|
||||
wouldTrigger: 'high',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Immer manuellen Freigabe-Schritt vor Veroeffentlichung',
|
||||
},
|
||||
{
|
||||
id: 'behavioral-profiling',
|
||||
title: 'Verhaltensprofilierung',
|
||||
description: 'Erstellung von Persoenlichkeits- oder Verhaltensprofilen von Schuelern',
|
||||
wouldTrigger: 'unacceptable',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Keine Verhaltensanalyse oder Profiling implementieren',
|
||||
},
|
||||
{
|
||||
id: 'algorithmic-optimization',
|
||||
title: 'Algorithmische Schuloptimierung',
|
||||
description: 'KI optimiert Schulentscheidungen (Klassenzuteilung, Ressourcen)',
|
||||
wouldTrigger: 'high',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Schulorganisatorische Entscheidungen bei Menschen belassen',
|
||||
},
|
||||
{
|
||||
id: 'auto-accept',
|
||||
title: 'Auto-Accept Funktionen',
|
||||
description: 'Ein-Klick-Uebernahme von KI-Vorschlaegen ohne Pruefung',
|
||||
wouldTrigger: 'high',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Immer bewusste Bestaetigungsschritte einbauen',
|
||||
},
|
||||
{
|
||||
id: 'emotion-detection',
|
||||
title: 'Emotionserkennung',
|
||||
description: 'Analyse von Emotionen oder psychischem Zustand von Schuelern',
|
||||
wouldTrigger: 'unacceptable',
|
||||
currentStatus: 'safe',
|
||||
recommendation: 'Keine biometrische oder emotionale Analyse',
|
||||
},
|
||||
]
|
||||
|
||||
// =============================================================================
|
||||
// HELPER FUNCTIONS
|
||||
// =============================================================================
|
||||
|
||||
const getRiskLevelInfo = (level: RiskLevel) => {
|
||||
switch (level) {
|
||||
case 'unacceptable':
|
||||
return {
|
||||
label: 'Unzulaessig',
|
||||
color: 'bg-black text-white',
|
||||
borderColor: 'border-black',
|
||||
description: 'Verboten nach EU-AI-Act',
|
||||
}
|
||||
case 'high':
|
||||
return {
|
||||
label: 'Hoch',
|
||||
color: 'bg-red-600 text-white',
|
||||
borderColor: 'border-red-600',
|
||||
description: 'Strenge Anforderungen, Konformitaetsbewertung',
|
||||
}
|
||||
case 'limited':
|
||||
return {
|
||||
label: 'Begrenzt',
|
||||
color: 'bg-amber-500 text-white',
|
||||
borderColor: 'border-amber-500',
|
||||
description: 'Transparenzpflichten',
|
||||
}
|
||||
case 'minimal':
|
||||
return {
|
||||
label: 'Minimal',
|
||||
color: 'bg-green-600 text-white',
|
||||
borderColor: 'border-green-600',
|
||||
description: 'Freiwillige Verhaltenskodizes',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusInfo = (status: 'safe' | 'approaching' | 'violated') => {
|
||||
switch (status) {
|
||||
case 'safe':
|
||||
return { label: 'Sicher', color: 'bg-green-100 text-green-700', icon: '✓' }
|
||||
case 'approaching':
|
||||
return { label: 'Annaehernd', color: 'bg-amber-100 text-amber-700', icon: '⚠' }
|
||||
case 'violated':
|
||||
return { label: 'Verletzt', color: 'bg-red-100 text-red-700', icon: '✗' }
|
||||
}
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
// COMPONENT
|
||||
// =============================================================================
|
||||
|
||||
export default function AIActClassificationPage() {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'modules' | 'warnings' | 'documentation'>('overview')
|
||||
const [expandedModule, setExpandedModule] = useState<string | null>(null)
|
||||
|
||||
// Calculate statistics
|
||||
const stats = {
|
||||
totalModules: MODULE_ASSESSMENTS.length,
|
||||
minimalRisk: MODULE_ASSESSMENTS.filter((m) => m.riskLevel === 'minimal').length,
|
||||
limitedRisk: MODULE_ASSESSMENTS.filter((m) => m.riskLevel === 'limited').length,
|
||||
highRisk: MODULE_ASSESSMENTS.filter((m) => m.riskLevel === 'high').length,
|
||||
humanInLoop: MODULE_ASSESSMENTS.filter((m) => m.humanInLoop).length,
|
||||
warningsTotal: WARNING_LINES.length,
|
||||
warningsSafe: WARNING_LINES.filter((w) => w.currentStatus === 'safe').length,
|
||||
}
|
||||
|
||||
const generateMemo = () => {
|
||||
const date = new Date().toLocaleDateString('de-DE', {
|
||||
year: 'numeric',
|
||||
month: 'long',
|
||||
day: 'numeric',
|
||||
})
|
||||
|
||||
const memo = `
|
||||
================================================================================
|
||||
EU-AI-ACT RISIKOKLASSIFIZIERUNG - BREAKPILOT
|
||||
================================================================================
|
||||
Erstellungsdatum: ${date}
|
||||
Version: 1.0
|
||||
Verantwortlich: Breakpilot GmbH
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
1. ZUSAMMENFASSUNG
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
System: Breakpilot KI-Assistenzsystem fuer Bildung
|
||||
Gesamtrisikokategorie: LIMITED RISK (Art. 52 EU-AI-Act)
|
||||
|
||||
Begruendung:
|
||||
- KI liefert ausschliesslich Vorschlaege und Entwuerfe
|
||||
- Kein automatisiertes Entscheiden ueber Schueler
|
||||
- Mensch-in-the-Loop ist technisch erzwungen
|
||||
- Keine Schuelerklassifikation oder Profiling
|
||||
- Alle paedagogischen Entscheidungen verbleiben bei Lehrkraeften
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
2. MODUL-BEWERTUNG
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
${MODULE_ASSESSMENTS.map(
|
||||
(m) => `
|
||||
${m.name}
|
||||
Risikostufe: ${getRiskLevelInfo(m.riskLevel).label.toUpperCase()}
|
||||
Begruendung: ${m.justification}
|
||||
Human-in-Loop: ${m.humanInLoop ? 'JA' : 'NEIN'}
|
||||
AI-Act Artikel: ${m.aiActArticle}
|
||||
`
|
||||
).join('')}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
3. TRANSPARENZMASSNAHMEN
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Alle KI-generierten Inhalte sind:
|
||||
- Klar als KI-Vorschlag gekennzeichnet
|
||||
- Vollstaendig editierbar durch die Lehrkraft
|
||||
- Mit Quellenangaben versehen (wo zutreffend)
|
||||
- Erst nach manueller Freigabe wirksam
|
||||
|
||||
Zusaetzliche UI-Hinweise:
|
||||
- "Dieser Text wurde durch KI vorgeschlagen"
|
||||
- "Endverantwortung liegt bei der Lehrkraft"
|
||||
- Confidence-Scores wo relevant
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
4. HUMAN-IN-THE-LOOP GARANTIEN
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Technisch erzwungene Massnahmen:
|
||||
- Kein Auto-Accept fuer KI-Vorschlaege
|
||||
- Kein 1-Click-Bewerten
|
||||
- Pflicht-Bestaetigung vor Freigabe
|
||||
- Audit-Trail aller Aenderungen
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
5. WARNLINIEN (NICHT IMPLEMENTIEREN)
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
${WARNING_LINES.map(
|
||||
(w) => `
|
||||
[${getStatusInfo(w.currentStatus).icon}] ${w.title}
|
||||
Wuerde ausloesen: ${getRiskLevelInfo(w.wouldTrigger).label}
|
||||
Status: ${getStatusInfo(w.currentStatus).label}
|
||||
`
|
||||
).join('')}
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
6. RECHTLICHE EINORDNUNG
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
Breakpilot faellt NICHT unter die High-Risk Kategorie des EU-AI-Act, da:
|
||||
|
||||
1. Keine automatisierten Entscheidungen ueber natuerliche Personen
|
||||
2. Keine Bewertung von Schuelern ohne menschliche Kontrolle
|
||||
3. Keine Zugangs- oder Selektionsentscheidungen
|
||||
4. Reine Assistenzfunktion mit Human-in-the-Loop
|
||||
|
||||
Die Transparenzpflichten nach Art. 52 werden durch entsprechende
|
||||
UI-Kennzeichnungen und Nutzerinformationen erfuellt.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
7. MANAGEMENT-STATEMENT
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
"Breakpilot ist ein KI-Assistenzsystem mit begrenztem Risiko gemaess EU-AI-Act.
|
||||
Die KI trifft keine Entscheidungen, sondern unterstuetzt Lehrkraefte transparent
|
||||
und nachvollziehbar. Alle paedagogischen und rechtlichen Entscheidungen
|
||||
verbleiben beim Menschen."
|
||||
|
||||
================================================================================
|
||||
Dieses Dokument dient der internen Compliance-Dokumentation und kann
|
||||
Auditoren auf Anfrage vorgelegt werden.
|
||||
================================================================================
|
||||
`
|
||||
return memo
|
||||
}
|
||||
|
||||
const downloadMemo = () => {
|
||||
const memo = generateMemo()
|
||||
const blob = new Blob([memo], { type: 'text/plain;charset=utf-8' })
|
||||
const url = URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `breakpilot-ai-act-klassifizierung-${new Date().toISOString().split('T')[0]}.txt`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
document.body.removeChild(a)
|
||||
URL.revokeObjectURL(url)
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'overview', name: 'Uebersicht', icon: '📊' },
|
||||
{ id: 'modules', name: 'Module', icon: '🧩' },
|
||||
{ id: 'warnings', name: 'Warnlinien', icon: '⚠️' },
|
||||
{ id: 'documentation', name: 'Dokumentation', icon: '📄' },
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50">
|
||||
{/* Header */}
|
||||
<div className="bg-white border-b border-slate-200">
|
||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-blue-600 to-indigo-700 flex items-center justify-center">
|
||||
<span className="text-2xl">🤖</span>
|
||||
</div>
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">EU-AI-Act Klassifizierung</h1>
|
||||
<p className="text-slate-600">Risikoklassifizierung und Compliance-Dokumentation</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="max-w-7xl mx-auto px-6 py-6">
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="KI-Risikoklassifizierung nach EU-AI-Act"
|
||||
purpose="Selbstbewertung und Dokumentation der Risikokategorien aller KI-Module gemaess EU-AI-Act. Definiert Warnlinien fuer Features, die nicht implementiert werden duerfen."
|
||||
audience={['Management', 'DSB', 'Compliance Officer', 'Auditor', 'Investoren']}
|
||||
gdprArticles={['EU-AI-Act Art. 52', 'EU-AI-Act Art. 69', 'EU-AI-Act Anhang III']}
|
||||
/>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 mt-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500">Module gesamt</div>
|
||||
<div className="text-2xl font-bold text-slate-900">{stats.totalModules}</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500">Minimal Risk</div>
|
||||
<div className="text-2xl font-bold text-green-600">{stats.minimalRisk}</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500">Limited Risk</div>
|
||||
<div className="text-2xl font-bold text-amber-600">{stats.limitedRisk}</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-sm text-slate-500">Human-in-Loop</div>
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.humanInLoop}/{stats.totalModules}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Classification Banner */}
|
||||
<div className="mt-6 bg-gradient-to-r from-amber-50 to-amber-100 border border-amber-200 rounded-xl p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-amber-200 flex items-center justify-center flex-shrink-0">
|
||||
<span className="text-3xl">⚖️</span>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-amber-900">Gesamtklassifizierung: LIMITED RISK</h2>
|
||||
<p className="text-amber-800 mt-1">
|
||||
Breakpilot ist ein KI-Assistenzsystem mit <strong>begrenztem Risiko</strong> gemaess EU-AI-Act (Art. 52).
|
||||
Es gelten Transparenzpflichten, aber keine Konformitaetsbewertung.
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2 mt-3">
|
||||
<span className="px-3 py-1 bg-amber-200 text-amber-800 rounded-full text-sm font-medium">
|
||||
Art. 52 Transparenz
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-green-200 text-green-800 rounded-full text-sm font-medium">
|
||||
Human-in-the-Loop
|
||||
</span>
|
||||
<span className="px-3 py-1 bg-blue-200 text-blue-800 rounded-full text-sm font-medium">
|
||||
Assistiv, nicht autonom
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 mt-6 border-b border-slate-200">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||||
className={`px-4 py-3 text-sm font-medium border-b-2 transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'border-purple-600 text-purple-600'
|
||||
: 'border-transparent text-slate-600 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
<span className="mr-2">{tab.icon}</span>
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
<div className="mt-6">
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
{/* Risk Level Pyramid */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">EU-AI-Act Risikopyramide</h3>
|
||||
<div className="space-y-3">
|
||||
{/* Unacceptable */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-32 text-right">
|
||||
<span className="px-3 py-1 bg-black text-white text-xs rounded font-medium">
|
||||
Unzulaessig
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 h-8 bg-slate-100 rounded relative overflow-hidden">
|
||||
<div className="absolute inset-y-0 left-0 w-0 bg-black rounded" />
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs text-slate-500">
|
||||
0 Module - Social Scoring, Manipulation verboten
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* High */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-32 text-right">
|
||||
<span className="px-3 py-1 bg-red-600 text-white text-xs rounded font-medium">
|
||||
Hoch
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 h-8 bg-slate-100 rounded relative overflow-hidden">
|
||||
<div className="absolute inset-y-0 left-0 w-0 bg-red-600 rounded" />
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs text-slate-500">
|
||||
0 Module - Keine automatischen Entscheidungen
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Limited */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-32 text-right">
|
||||
<span className="px-3 py-1 bg-amber-500 text-white text-xs rounded font-medium">
|
||||
Begrenzt
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 h-8 bg-slate-100 rounded relative overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-amber-500 rounded transition-all"
|
||||
style={{ width: `${(stats.limitedRisk / stats.totalModules) * 100}%` }}
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs font-medium text-slate-700">
|
||||
{stats.limitedRisk} Module - Transparenzpflichten
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{/* Minimal */}
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-32 text-right">
|
||||
<span className="px-3 py-1 bg-green-600 text-white text-xs rounded font-medium">
|
||||
Minimal
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex-1 h-8 bg-slate-100 rounded relative overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-y-0 left-0 bg-green-600 rounded transition-all"
|
||||
style={{ width: `${(stats.minimalRisk / stats.totalModules) * 100}%` }}
|
||||
/>
|
||||
<span className="absolute inset-0 flex items-center justify-center text-xs font-medium text-slate-700">
|
||||
{stats.minimalRisk} Module - Freiwillige Kodizes
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Key Arguments */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Kernargumente fuer Limited Risk</h3>
|
||||
<div className="grid md:grid-cols-2 gap-4">
|
||||
<div className="flex items-start gap-3 p-4 bg-green-50 rounded-lg">
|
||||
<span className="text-xl">✓</span>
|
||||
<div>
|
||||
<div className="font-medium text-green-900">Assistiv, nicht autonom</div>
|
||||
<div className="text-sm text-green-700">KI liefert Vorschlaege, keine Entscheidungen</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-green-50 rounded-lg">
|
||||
<span className="text-xl">✓</span>
|
||||
<div>
|
||||
<div className="font-medium text-green-900">Human-in-the-Loop</div>
|
||||
<div className="text-sm text-green-700">Lehrkraft hat immer das letzte Wort</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-green-50 rounded-lg">
|
||||
<span className="text-xl">✓</span>
|
||||
<div>
|
||||
<div className="font-medium text-green-900">Keine Schuelerklassifikation</div>
|
||||
<div className="text-sm text-green-700">Keine Kategorisierung oder Profiling</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3 p-4 bg-green-50 rounded-lg">
|
||||
<span className="text-xl">✓</span>
|
||||
<div>
|
||||
<div className="font-medium text-green-900">Transparente Kennzeichnung</div>
|
||||
<div className="text-sm text-green-700">KI-Inhalte sind klar markiert</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Management Statement */}
|
||||
<div className="bg-gradient-to-br from-slate-800 to-slate-900 rounded-xl p-6 text-white">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-3xl">💬</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-lg">Management-Statement (Pitch-faehig)</h3>
|
||||
<blockquote className="mt-3 text-slate-300 italic border-l-4 border-amber-500 pl-4">
|
||||
“Breakpilot ist ein KI-Assistenzsystem mit begrenztem Risiko gemaess EU-AI-Act.
|
||||
Die KI trifft keine Entscheidungen, sondern unterstuetzt Lehrkraefte transparent und nachvollziehbar.
|
||||
Alle paedagogischen und rechtlichen Entscheidungen verbleiben beim Menschen.”
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Modules Tab */}
|
||||
{activeTab === 'modules' && (
|
||||
<div className="space-y-4">
|
||||
<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 px-4 py-3 text-xs font-medium text-slate-500 uppercase">Modul</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Risikostufe</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">Human-in-Loop</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-slate-500 uppercase">AI-Act Artikel</th>
|
||||
<th className="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-100">
|
||||
{MODULE_ASSESSMENTS.map((module) => {
|
||||
const riskInfo = getRiskLevelInfo(module.riskLevel)
|
||||
const isExpanded = expandedModule === module.id
|
||||
return (
|
||||
<>
|
||||
<tr key={module.id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<div className="font-medium text-slate-800">{module.name}</div>
|
||||
<div className="text-sm text-slate-500">{module.description}</div>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className={`px-3 py-1 rounded text-xs font-medium ${riskInfo.color}`}>
|
||||
{riskInfo.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
{module.humanInLoop ? (
|
||||
<span className="text-green-600 font-medium">✓ Ja</span>
|
||||
) : (
|
||||
<span className="text-red-600 font-medium">✗ Nein</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-sm text-slate-600">{module.aiActArticle}</td>
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setExpandedModule(isExpanded ? null : module.id)}
|
||||
className="text-purple-600 hover:text-purple-800 text-sm"
|
||||
>
|
||||
{isExpanded ? 'Weniger' : 'Details'}
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
{isExpanded && (
|
||||
<tr key={`${module.id}-details`}>
|
||||
<td colSpan={5} className="px-4 py-4 bg-slate-50">
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-500 uppercase mb-1">Begruendung</div>
|
||||
<div className="text-sm text-slate-700">{module.justification}</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="text-xs font-medium text-slate-500 uppercase mb-1">
|
||||
Transparenzmassnahmen
|
||||
</div>
|
||||
<ul className="text-sm text-slate-700 list-disc list-inside">
|
||||
{module.transparencyMeasures.map((measure, i) => (
|
||||
<li key={i}>{measure}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
)}
|
||||
</>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Warnings Tab */}
|
||||
{activeTab === 'warnings' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<span className="text-3xl">🚫</span>
|
||||
<div>
|
||||
<h3 className="font-semibold text-red-900">
|
||||
Warnlinien: Was wir NIEMALS bauen duerfen
|
||||
</h3>
|
||||
<p className="text-red-700 mt-1">
|
||||
Diese Features wuerden Breakpilot in die High-Risk oder Unzulaessig-Kategorie verschieben.
|
||||
Sie sind explizit von der Roadmap ausgeschlossen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-4">
|
||||
{WARNING_LINES.map((warning) => {
|
||||
const statusInfo = getStatusInfo(warning.currentStatus)
|
||||
const riskInfo = getRiskLevelInfo(warning.wouldTrigger)
|
||||
return (
|
||||
<div
|
||||
key={warning.id}
|
||||
className={`bg-white rounded-xl border-2 ${
|
||||
warning.currentStatus === 'safe'
|
||||
? 'border-green-200'
|
||||
: warning.currentStatus === 'approaching'
|
||||
? 'border-amber-300'
|
||||
: 'border-red-400'
|
||||
} p-4`}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<div
|
||||
className={`w-10 h-10 rounded-full flex items-center justify-center text-lg ${statusInfo.color}`}
|
||||
>
|
||||
{statusInfo.icon}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-semibold text-slate-900">{warning.title}</h4>
|
||||
<p className="text-sm text-slate-600 mt-1">{warning.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs text-slate-500">Wuerde ausloesen:</span>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${riskInfo.color}`}>
|
||||
{riskInfo.label}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-3 pl-13 ml-13">
|
||||
<div className="flex items-center gap-2 text-sm">
|
||||
<span className="text-slate-500">Empfehlung:</span>
|
||||
<span className="text-slate-700">{warning.recommendation}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* Safe Zone Indicator */}
|
||||
<div className="bg-green-50 border border-green-200 rounded-xl p-6">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-16 h-16 rounded-full bg-green-200 flex items-center justify-center">
|
||||
<span className="text-3xl">✓</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-green-900">
|
||||
Alle Warnlinien eingehalten: {stats.warningsSafe}/{stats.warningsTotal}
|
||||
</h3>
|
||||
<p className="text-green-700 mt-1">
|
||||
Breakpilot befindet sich sicher im Limited/Minimal Risk Bereich des EU-AI-Act.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documentation Tab */}
|
||||
{activeTab === 'documentation' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900">Klassifizierungs-Memo exportieren</h3>
|
||||
<p className="text-slate-600 mt-1">
|
||||
Generiert ein vollstaendiges Compliance-Dokument zur Vorlage bei Auditoren oder Investoren.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={downloadMemo}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors 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 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
Als TXT herunterladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Preview */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Vorschau</h3>
|
||||
<pre className="bg-slate-900 text-slate-100 p-4 rounded-lg overflow-x-auto text-xs font-mono whitespace-pre-wrap max-h-96 overflow-y-auto">
|
||||
{generateMemo()}
|
||||
</pre>
|
||||
</div>
|
||||
|
||||
{/* Human-in-the-Loop Documentation */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Human-in-the-Loop Nachweis</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<h4 className="font-medium text-blue-900">Technische Massnahmen</h4>
|
||||
<ul className="mt-2 text-sm text-blue-800 space-y-1">
|
||||
<li>• Kein Auto-Accept Button fuer KI-Vorschlaege</li>
|
||||
<li>• Mindestens 2 Klicks fuer Uebernahme erforderlich</li>
|
||||
<li>• Alle KI-Outputs sind editierbar</li>
|
||||
<li>• Pflicht-Review vor Freigabe an Schueler/Eltern</li>
|
||||
<li>• Audit-Trail dokumentiert alle menschlichen Eingriffe</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="p-4 bg-blue-50 rounded-lg">
|
||||
<h4 className="font-medium text-blue-900">UI-Kennzeichnungen</h4>
|
||||
<ul className="mt-2 text-sm text-blue-800 space-y-1">
|
||||
<li>• “KI-Vorschlag” Label auf allen generierten Inhalten</li>
|
||||
<li>• “Endverantwortung liegt bei der Lehrkraft” Hinweis</li>
|
||||
<li>• Confidence-Scores wo relevant</li>
|
||||
<li>• Quellenangaben fuer RAG-basierte Inhalte</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,775 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Audit Checklist Page - 476+ Requirements Interactive Checklist
|
||||
*
|
||||
* Features:
|
||||
* - Session management (create, start, complete)
|
||||
* - Paginated checklist with search & filters
|
||||
* - Sign-off workflow with digital signatures
|
||||
* - Progress tracking with statistics
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// Types
|
||||
interface AuditSession {
|
||||
id: string
|
||||
name: string
|
||||
auditor_name: string
|
||||
auditor_email?: string
|
||||
auditor_organization?: string
|
||||
status: 'draft' | 'in_progress' | 'completed' | 'archived'
|
||||
regulation_ids?: string[]
|
||||
total_items: number
|
||||
completed_items: number
|
||||
compliant_count: number
|
||||
non_compliant_count: number
|
||||
completion_percentage: number
|
||||
created_at: string
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
interface ChecklistItem {
|
||||
requirement_id: string
|
||||
regulation_code: string
|
||||
article: string
|
||||
paragraph?: string
|
||||
title: string
|
||||
description?: string
|
||||
current_result: string
|
||||
notes?: string
|
||||
is_signed: boolean
|
||||
signed_at?: string
|
||||
signed_by?: string
|
||||
evidence_count: number
|
||||
controls_mapped: number
|
||||
implementation_status: string
|
||||
priority: number
|
||||
}
|
||||
|
||||
interface AuditStatistics {
|
||||
total: number
|
||||
compliant: number
|
||||
compliant_with_notes: number
|
||||
non_compliant: number
|
||||
not_applicable: number
|
||||
pending: number
|
||||
completion_percentage: number
|
||||
}
|
||||
|
||||
// Haupt-/Nebenabweichungen aus ISMS
|
||||
interface FindingsData {
|
||||
major_count: number // Hauptabweichungen (blockiert Zertifizierung)
|
||||
minor_count: number // Nebenabweichungen (erfordert CAPA)
|
||||
ofi_count: number // Verbesserungspotenziale
|
||||
total: number
|
||||
open_majors: number // Offene Hauptabweichungen
|
||||
open_minors: number // Offene Nebenabweichungen
|
||||
}
|
||||
|
||||
const RESULT_COLORS: Record<string, { bg: string; text: string; label: string }> = {
|
||||
compliant: { bg: 'bg-green-100', text: 'text-green-700', label: 'Konform' },
|
||||
compliant_notes: { bg: 'bg-green-50', text: 'text-green-600', label: 'Konform (mit Anm.)' },
|
||||
non_compliant: { bg: 'bg-red-100', text: 'text-red-700', label: 'Nicht konform' },
|
||||
not_applicable: { bg: 'bg-slate-100', text: 'text-slate-600', label: 'N/A' },
|
||||
pending: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Ausstehend' },
|
||||
}
|
||||
|
||||
export default function AuditChecklistPage() {
|
||||
const [sessions, setSessions] = useState<AuditSession[]>([])
|
||||
const [selectedSession, setSelectedSession] = useState<AuditSession | null>(null)
|
||||
const [checklist, setChecklist] = useState<ChecklistItem[]>([])
|
||||
const [statistics, setStatistics] = useState<AuditStatistics | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [checklistLoading, setChecklistLoading] = useState(false)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [findings, setFindings] = useState<FindingsData | null>(null)
|
||||
|
||||
// Filters
|
||||
const [search, setSearch] = useState('')
|
||||
const [statusFilter, setStatusFilter] = useState<string>('')
|
||||
const [regulationFilter, setRegulationFilter] = useState<string>('')
|
||||
const [page, setPage] = useState(1)
|
||||
const [totalPages, setTotalPages] = useState(1)
|
||||
|
||||
// Modal states
|
||||
const [showCreateModal, setShowCreateModal] = useState(false)
|
||||
const [showSignOffModal, setShowSignOffModal] = useState(false)
|
||||
const [selectedItem, setSelectedItem] = useState<ChecklistItem | null>(null)
|
||||
|
||||
// New session form
|
||||
const [newSession, setNewSession] = useState({
|
||||
name: '',
|
||||
auditor_name: '',
|
||||
auditor_email: '',
|
||||
auditor_organization: '',
|
||||
regulation_codes: [] as string[],
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
loadSessions()
|
||||
loadFindings()
|
||||
}, [])
|
||||
|
||||
const loadFindings = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/compliance/isms/findings/summary')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setFindings(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load findings:', err)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedSession) {
|
||||
loadChecklist()
|
||||
}
|
||||
}, [selectedSession, page, statusFilter, regulationFilter, search])
|
||||
|
||||
const loadSessions = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/admin/audit/sessions')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setSessions(data)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load sessions:', err)
|
||||
setError('Sessions konnten nicht geladen werden')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadChecklist = async () => {
|
||||
if (!selectedSession) return
|
||||
setChecklistLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({
|
||||
page: page.toString(),
|
||||
page_size: '50',
|
||||
})
|
||||
if (statusFilter) params.set('status_filter', statusFilter)
|
||||
if (regulationFilter) params.set('regulation_filter', regulationFilter)
|
||||
if (search) params.set('search', search)
|
||||
|
||||
const res = await fetch(`/api/admin/compliance/audit/checklist/${selectedSession.id}?${params}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setChecklist(data.items || [])
|
||||
setStatistics(data.statistics)
|
||||
setTotalPages(data.pagination?.total_pages || 1)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load checklist:', err)
|
||||
} finally {
|
||||
setChecklistLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const createSession = async () => {
|
||||
try {
|
||||
const res = await fetch('/api/admin/audit/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newSession),
|
||||
})
|
||||
if (res.ok) {
|
||||
const session = await res.json()
|
||||
setSessions([session, ...sessions])
|
||||
setSelectedSession(session)
|
||||
setShowCreateModal(false)
|
||||
setNewSession({
|
||||
name: '',
|
||||
auditor_name: '',
|
||||
auditor_email: '',
|
||||
auditor_organization: '',
|
||||
regulation_codes: [],
|
||||
})
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to create session:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const startSession = async (sessionId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/audit/sessions/${sessionId}/start`, {
|
||||
method: 'PUT',
|
||||
})
|
||||
if (res.ok) {
|
||||
loadSessions()
|
||||
if (selectedSession?.id === sessionId) {
|
||||
setSelectedSession({ ...selectedSession, status: 'in_progress' })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to start session:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const completeSession = async (sessionId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/audit/sessions/${sessionId}/complete`, {
|
||||
method: 'PUT',
|
||||
})
|
||||
if (res.ok) {
|
||||
loadSessions()
|
||||
if (selectedSession?.id === sessionId) {
|
||||
setSelectedSession({ ...selectedSession, status: 'completed' })
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to complete session:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const signOffItem = async (result: string, notes: string, sign: boolean) => {
|
||||
if (!selectedSession || !selectedItem) return
|
||||
try {
|
||||
const res = await fetch(
|
||||
`/api/admin/compliance/audit/checklist/${selectedSession.id}/items/${selectedItem.requirement_id}`,
|
||||
{
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ result, notes, sign }),
|
||||
}
|
||||
)
|
||||
if (res.ok) {
|
||||
loadChecklist()
|
||||
loadSessions()
|
||||
setShowSignOffModal(false)
|
||||
setSelectedItem(null)
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to sign off:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const downloadPdf = async (sessionId: string) => {
|
||||
window.open(`/api/admin/audit/sessions/${sessionId}/pdf`, '_blank')
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PagePurpose
|
||||
title="Audit Checkliste"
|
||||
purpose="Interaktive Checkliste mit 476+ Compliance-Anforderungen aus DSGVO, AI Act, CRA und BSI TR-03161. Erstellen Sie Audit-Sessions, bewerten Sie Anforderungen und generieren Sie Audit-Reports mit digitalen Signaturen."
|
||||
audience={['Auditor', 'DSB', 'Compliance Officer']}
|
||||
gdprArticles={['Art. 5 (Rechenschaftspflicht)', 'Art. 30 (Verzeichnis)', 'Art. 32 (Sicherheit)']}
|
||||
architecture={{
|
||||
services: ['Python Backend', 'PostgreSQL'],
|
||||
databases: ['compliance_audit_sessions', 'compliance_audit_signoffs', 'compliance_requirements'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Compliance Hub', href: '/compliance/hub', description: 'Uebersicht & Dashboard' },
|
||||
{ name: 'Audit Report', href: '/compliance/audit-report', description: 'PDF-Reports generieren' },
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Massnahmen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
|
||||
<p className="text-red-700">{error}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Haupt-/Nebenabweichungen Uebersicht */}
|
||||
{findings && (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Audit Findings (ISMS)</h2>
|
||||
<span className={`px-3 py-1 text-sm rounded-full ${
|
||||
findings.open_majors > 0
|
||||
? 'bg-red-100 text-red-700'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{findings.open_majors > 0 ? 'Zertifizierung blockiert' : 'Zertifizierungsfaehig'}
|
||||
</span>
|
||||
</div>
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4">
|
||||
<div className="text-center p-4 bg-red-50 rounded-lg border border-red-200">
|
||||
<p className="text-3xl font-bold text-red-700">{findings.major_count}</p>
|
||||
<p className="text-sm text-red-600 font-medium">Hauptabweichungen</p>
|
||||
<p className="text-xs text-red-500 mt-1">(MAJOR)</p>
|
||||
{findings.open_majors > 0 && (
|
||||
<p className="text-xs text-red-700 mt-2 font-medium">
|
||||
{findings.open_majors} offen
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center p-4 bg-orange-50 rounded-lg border border-orange-200">
|
||||
<p className="text-3xl font-bold text-orange-700">{findings.minor_count}</p>
|
||||
<p className="text-sm text-orange-600 font-medium">Nebenabweichungen</p>
|
||||
<p className="text-xs text-orange-500 mt-1">(MINOR)</p>
|
||||
{findings.open_minors > 0 && (
|
||||
<p className="text-xs text-orange-700 mt-2 font-medium">
|
||||
{findings.open_minors} offen
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-center p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<p className="text-3xl font-bold text-blue-700">{findings.ofi_count}</p>
|
||||
<p className="text-sm text-blue-600 font-medium">Verbesserungen</p>
|
||||
<p className="text-xs text-blue-500 mt-1">(OFI)</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<p className="text-3xl font-bold text-slate-700">{findings.total}</p>
|
||||
<p className="text-sm text-slate-600 font-medium">Gesamt Findings</p>
|
||||
</div>
|
||||
<div className="text-center p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||
<div className="flex flex-col items-center">
|
||||
<svg className={`w-8 h-8 ${findings.open_majors === 0 ? 'text-green-500' : 'text-red-500'}`} fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
{findings.open_majors === 0 ? (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
|
||||
) : (
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
)}
|
||||
</svg>
|
||||
<p className="text-sm text-purple-600 font-medium mt-2">Zertifizierung</p>
|
||||
<p className={`text-xs mt-1 font-medium ${findings.open_majors === 0 ? 'text-green-600' : 'text-red-600'}`}>
|
||||
{findings.open_majors === 0 ? 'Moeglich' : 'Blockiert'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-xs text-slate-600">
|
||||
<strong>Hauptabweichung (MAJOR):</strong> Signifikante Abweichung von Anforderungen - blockiert Zertifizierung bis zur Behebung.{' '}
|
||||
<strong>Nebenabweichung (MINOR):</strong> Kleinere Abweichung - erfordert CAPA (Corrective Action) innerhalb 90 Tagen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
|
||||
{/* Sessions Sidebar */}
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Audit Sessions</h2>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="p-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 4v16m8-8H4" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
<p>Keine Sessions vorhanden</p>
|
||||
<button
|
||||
onClick={() => setShowCreateModal(true)}
|
||||
className="mt-2 text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Erste Session erstellen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
onClick={() => setSelectedSession(session)}
|
||||
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedSession?.id === session.id
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
session.status === 'completed' ? 'bg-green-100 text-green-700' :
|
||||
session.status === 'in_progress' ? 'bg-blue-100 text-blue-700' :
|
||||
session.status === 'archived' ? 'bg-slate-100 text-slate-700' :
|
||||
'bg-yellow-100 text-yellow-700'
|
||||
}`}>
|
||||
{session.status === 'completed' ? 'Abgeschlossen' :
|
||||
session.status === 'in_progress' ? 'In Bearbeitung' :
|
||||
session.status === 'archived' ? 'Archiviert' : 'Entwurf'}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500">{session.completion_percentage.toFixed(0)}%</span>
|
||||
</div>
|
||||
<h3 className="font-medium text-slate-900 truncate">{session.name}</h3>
|
||||
<p className="text-sm text-slate-500">{session.auditor_name}</p>
|
||||
<div className="mt-2 h-1.5 bg-slate-200 rounded-full overflow-hidden">
|
||||
<div
|
||||
className="h-full bg-purple-500"
|
||||
style={{ width: `${session.completion_percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Checklist Content */}
|
||||
<div className="lg:col-span-3 space-y-4">
|
||||
{!selectedSession ? (
|
||||
<div className="bg-white rounded-xl shadow-sm border 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={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<h3 className="text-lg font-medium text-slate-900">Waehlen Sie eine Session</h3>
|
||||
<p className="text-slate-500 mt-2">Waehlen Sie eine Audit-Session aus der Liste oder erstellen Sie eine neue.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Session Header */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-xl font-semibold text-slate-900">{selectedSession.name}</h2>
|
||||
<p className="text-slate-500">{selectedSession.auditor_name} {selectedSession.auditor_organization && `- ${selectedSession.auditor_organization}`}</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
{selectedSession.status === 'draft' && (
|
||||
<button
|
||||
onClick={() => startSession(selectedSession.id)}
|
||||
className="px-4 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Starten
|
||||
</button>
|
||||
)}
|
||||
{selectedSession.status === 'in_progress' && (
|
||||
<button
|
||||
onClick={() => completeSession(selectedSession.id)}
|
||||
className="px-4 py-2 bg-green-600 text-white rounded-lg hover:bg-green-700"
|
||||
>
|
||||
Abschliessen
|
||||
</button>
|
||||
)}
|
||||
{selectedSession.status === 'completed' && (
|
||||
<button
|
||||
onClick={() => downloadPdf(selectedSession.id)}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
PDF Export
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
{statistics && (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="text-center p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-slate-900">{statistics.total}</p>
|
||||
<p className="text-xs text-slate-500">Gesamt</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-green-700">{statistics.compliant + statistics.compliant_with_notes}</p>
|
||||
<p className="text-xs text-green-600">Konform</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-red-700">{statistics.non_compliant}</p>
|
||||
<p className="text-xs text-red-600">Nicht konform</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-slate-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-slate-700">{statistics.not_applicable}</p>
|
||||
<p className="text-xs text-slate-500">N/A</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-yellow-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-yellow-700">{statistics.pending}</p>
|
||||
<p className="text-xs text-yellow-600">Ausstehend</p>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-purple-50 rounded-lg">
|
||||
<p className="text-2xl font-bold text-purple-700">{statistics.completion_percentage.toFixed(0)}%</p>
|
||||
<p className="text-xs text-purple-600">Fortschritt</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
<div className="flex gap-4">
|
||||
<input
|
||||
type="text"
|
||||
value={search}
|
||||
onChange={(e) => { setSearch(e.target.value); setPage(1) }}
|
||||
placeholder="Suche..."
|
||||
className="flex-1 px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => { setStatusFilter(e.target.value); setPage(1) }}
|
||||
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Alle Status</option>
|
||||
<option value="pending">Ausstehend</option>
|
||||
<option value="compliant">Konform</option>
|
||||
<option value="non_compliant">Nicht konform</option>
|
||||
<option value="not_applicable">N/A</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Checklist Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
{checklistLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
) : checklist.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
Keine Eintraege gefunden
|
||||
</div>
|
||||
) : (
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Regulation</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Artikel</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Titel</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Controls</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Status</th>
|
||||
<th className="px-4 py-3 text-center text-xs font-medium text-slate-500 uppercase">Aktion</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-slate-200">
|
||||
{checklist.map((item) => {
|
||||
const resultConfig = RESULT_COLORS[item.current_result] || RESULT_COLORS.pending
|
||||
return (
|
||||
<tr key={item.requirement_id} className="hover:bg-slate-50">
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-mono text-sm text-purple-600">{item.regulation_code}</span>
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<span className="font-medium">{item.article}</span>
|
||||
{item.paragraph && <span className="text-slate-500 text-sm"> {item.paragraph}</span>}
|
||||
</td>
|
||||
<td className="px-4 py-3">
|
||||
<p className="text-sm text-slate-900 line-clamp-2">{item.title}</p>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
item.controls_mapped > 0 ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500'
|
||||
}`}>
|
||||
{item.controls_mapped}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${resultConfig.bg} ${resultConfig.text}`}>
|
||||
{resultConfig.label}
|
||||
</span>
|
||||
{item.is_signed && (
|
||||
<svg className="w-4 h-4 text-green-600 inline-block ml-1" 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>
|
||||
)}
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<button
|
||||
onClick={() => { setSelectedItem(item); setShowSignOffModal(true) }}
|
||||
className="px-3 py-1 text-sm bg-purple-100 text-purple-700 rounded hover:bg-purple-200"
|
||||
disabled={selectedSession.status !== 'in_progress' && selectedSession.status !== 'draft'}
|
||||
>
|
||||
Bewerten
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
)}
|
||||
|
||||
{/* Pagination */}
|
||||
{totalPages > 1 && (
|
||||
<div className="px-4 py-3 border-t flex items-center justify-between">
|
||||
<button
|
||||
onClick={() => setPage(Math.max(1, page - 1))}
|
||||
disabled={page === 1}
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
>
|
||||
Zurueck
|
||||
</button>
|
||||
<span className="text-sm text-slate-500">
|
||||
Seite {page} von {totalPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPage(Math.min(totalPages, page + 1))}
|
||||
disabled={page === totalPages}
|
||||
className="px-3 py-1 border rounded disabled:opacity-50"
|
||||
>
|
||||
Weiter
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Create Session Modal */}
|
||||
{showCreateModal && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl p-6 w-full max-w-md">
|
||||
<h3 className="text-lg font-semibold mb-4">Neue Audit Session</h3>
|
||||
<div className="space-y-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({ ...newSession, name: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
placeholder="z.B. Q1 2026 DSGVO Audit"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Auditor Name</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newSession.auditor_name}
|
||||
onChange={(e) => setNewSession({ ...newSession, auditor_name: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
placeholder="Dr. Max Mustermann"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Organisation (optional)</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newSession.auditor_organization}
|
||||
onChange={(e) => setNewSession({ ...newSession, auditor_organization: e.target.value })}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
placeholder="TÜV Rheinland"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button
|
||||
onClick={() => setShowCreateModal(false)}
|
||||
className="px-4 py-2 border rounded-lg"
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={createSession}
|
||||
disabled={!newSession.name || !newSession.auditor_name}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
Erstellen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Sign Off Modal */}
|
||||
{showSignOffModal && selectedItem && (
|
||||
<SignOffModal
|
||||
item={selectedItem}
|
||||
onClose={() => { setShowSignOffModal(false); setSelectedItem(null) }}
|
||||
onSignOff={signOffItem}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Sign Off Modal Component
|
||||
function SignOffModal({
|
||||
item,
|
||||
onClose,
|
||||
onSignOff,
|
||||
}: {
|
||||
item: ChecklistItem
|
||||
onClose: () => void
|
||||
onSignOff: (result: string, notes: string, sign: boolean) => void
|
||||
}) {
|
||||
const [result, setResult] = useState(item.current_result === 'pending' ? '' : item.current_result)
|
||||
const [notes, setNotes] = useState(item.notes || '')
|
||||
const [sign, setSign] = useState(false)
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl p-6 w-full max-w-lg">
|
||||
<h3 className="text-lg font-semibold mb-2">Anforderung bewerten</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
{item.regulation_code} {item.article}: {item.title}
|
||||
</p>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Bewertung</label>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{[
|
||||
{ value: 'compliant', label: 'Konform', color: 'green' },
|
||||
{ value: 'compliant_notes', label: 'Konform (mit Anm.)', color: 'green' },
|
||||
{ value: 'non_compliant', label: 'Nicht konform', color: 'red' },
|
||||
{ value: 'not_applicable', label: 'Nicht anwendbar', color: 'slate' },
|
||||
].map((opt) => (
|
||||
<button
|
||||
key={opt.value}
|
||||
onClick={() => setResult(opt.value)}
|
||||
className={`p-3 rounded-lg border-2 text-left transition-colors ${
|
||||
result === opt.value
|
||||
? opt.color === 'green' ? 'border-green-500 bg-green-50' :
|
||||
opt.color === 'red' ? 'border-red-500 bg-red-50' :
|
||||
'border-slate-500 bg-slate-50'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<span className="font-medium">{opt.label}</span>
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Anmerkungen</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
className="w-full px-3 py-2 border rounded-lg"
|
||||
rows={3}
|
||||
placeholder="Optionale Anmerkungen..."
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
id="sign"
|
||||
checked={sign}
|
||||
onChange={(e) => setSign(e.target.checked)}
|
||||
className="w-4 h-4 rounded"
|
||||
/>
|
||||
<label htmlFor="sign" className="text-sm text-slate-700">
|
||||
Digitale Signatur erstellen (SHA-256)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<button onClick={onClose} className="px-4 py-2 border rounded-lg">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => onSignOff(result, notes, sign)}
|
||||
disabled={!result}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,705 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Audit Report Management Page
|
||||
*
|
||||
* Create and manage GDPR audit sessions with PDF report generation.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface AuditSession {
|
||||
id: string
|
||||
name: string
|
||||
description?: string
|
||||
auditor_name: string
|
||||
auditor_email?: string
|
||||
auditor_organization?: string
|
||||
status: 'draft' | 'in_progress' | 'completed' | 'archived'
|
||||
regulation_ids?: string[]
|
||||
total_items: number
|
||||
completed_items: number
|
||||
compliant_count: number
|
||||
non_compliant_count: number
|
||||
completion_percentage: number
|
||||
created_at: string
|
||||
started_at?: string
|
||||
completed_at?: string
|
||||
}
|
||||
|
||||
// Available regulations for filtering
|
||||
const REGULATIONS = [
|
||||
{ code: 'GDPR', name: 'DSGVO / GDPR', description: 'EU-Datenschutzgrundverordnung' },
|
||||
{ code: 'BDSG', name: 'BDSG', description: 'Bundesdatenschutzgesetz' },
|
||||
{ code: 'TTDSG', name: 'TTDSG', description: 'Telekommunikation-Telemedien-Datenschutz' },
|
||||
]
|
||||
|
||||
export default function AuditReportPage() {
|
||||
const [sessions, setSessions] = useState<AuditSession[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [activeTab, setActiveTab] = useState<'sessions' | 'new' | 'export'>('sessions')
|
||||
|
||||
// New session form
|
||||
const [newSession, setNewSession] = useState({
|
||||
name: '',
|
||||
description: '',
|
||||
auditor_name: '',
|
||||
auditor_email: '',
|
||||
auditor_organization: '',
|
||||
regulation_codes: [] as string[],
|
||||
})
|
||||
const [creating, setCreating] = useState(false)
|
||||
|
||||
// PDF generation
|
||||
const [generatingPdf, setGeneratingPdf] = useState<string | null>(null)
|
||||
const [pdfLanguage, setPdfLanguage] = useState<'de' | 'en'>('de')
|
||||
|
||||
// Status filter
|
||||
const [statusFilter, setStatusFilter] = useState<string>('all')
|
||||
|
||||
useEffect(() => {
|
||||
fetchSessions()
|
||||
}, [statusFilter])
|
||||
|
||||
const fetchSessions = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const params = statusFilter !== 'all' ? `?status=${statusFilter}` : ''
|
||||
const res = await fetch(`/api/admin/audit/sessions${params}`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Laden der Audit-Sessions')
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
setSessions(data.sessions || [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const createSession = async () => {
|
||||
if (!newSession.name || !newSession.auditor_name) {
|
||||
setError('Name und Auditor-Name sind Pflichtfelder')
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
setCreating(true)
|
||||
const res = await fetch('/api/admin/audit/sessions', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(newSession),
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Erstellen der Session')
|
||||
}
|
||||
|
||||
// Reset form and refresh
|
||||
setNewSession({
|
||||
name: '',
|
||||
description: '',
|
||||
auditor_name: '',
|
||||
auditor_email: '',
|
||||
auditor_organization: '',
|
||||
regulation_codes: [],
|
||||
})
|
||||
setActiveTab('sessions')
|
||||
fetchSessions()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setCreating(false)
|
||||
}
|
||||
}
|
||||
|
||||
const startSession = async (sessionId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/audit/sessions/${sessionId}/start`, {
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Starten der Session')
|
||||
}
|
||||
|
||||
fetchSessions()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
}
|
||||
}
|
||||
|
||||
const completeSession = async (sessionId: string) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/audit/sessions/${sessionId}/complete`, {
|
||||
method: 'PUT',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Abschliessen der Session')
|
||||
}
|
||||
|
||||
fetchSessions()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
}
|
||||
}
|
||||
|
||||
const deleteSession = async (sessionId: string) => {
|
||||
if (!confirm('Session wirklich loeschen?')) return
|
||||
|
||||
try {
|
||||
const res = await fetch(`/api/admin/audit/sessions/${sessionId}`, {
|
||||
method: 'DELETE',
|
||||
})
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Loeschen der Session')
|
||||
}
|
||||
|
||||
fetchSessions()
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
}
|
||||
}
|
||||
|
||||
const downloadPdf = async (sessionId: string) => {
|
||||
try {
|
||||
setGeneratingPdf(sessionId)
|
||||
const res = await fetch(
|
||||
`/api/admin/audit/sessions/${sessionId}/pdf?language=${pdfLanguage}`
|
||||
)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler bei der PDF-Generierung')
|
||||
}
|
||||
|
||||
// Download the PDF
|
||||
const blob = await res.blob()
|
||||
const url = window.URL.createObjectURL(blob)
|
||||
const a = document.createElement('a')
|
||||
a.href = url
|
||||
a.download = `audit-report-${sessionId}.pdf`
|
||||
document.body.appendChild(a)
|
||||
a.click()
|
||||
window.URL.revokeObjectURL(url)
|
||||
document.body.removeChild(a)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
} finally {
|
||||
setGeneratingPdf(null)
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
const styles: Record<string, string> = {
|
||||
draft: 'bg-slate-100 text-slate-700',
|
||||
in_progress: 'bg-blue-100 text-blue-700',
|
||||
completed: 'bg-green-100 text-green-700',
|
||||
archived: 'bg-purple-100 text-purple-700',
|
||||
}
|
||||
const labels: Record<string, string> = {
|
||||
draft: 'Entwurf',
|
||||
in_progress: 'In Bearbeitung',
|
||||
completed: 'Abgeschlossen',
|
||||
archived: 'Archiviert',
|
||||
}
|
||||
return (
|
||||
<span className={`px-2 py-1 rounded-full text-xs font-medium ${styles[status] || ''}`}>
|
||||
{labels[status] || status}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
|
||||
const getComplianceColor = (percentage: number) => {
|
||||
if (percentage >= 80) return 'text-green-600'
|
||||
if (percentage >= 50) return 'text-yellow-600'
|
||||
return 'text-red-600'
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Audit Report"
|
||||
purpose="Erstellen und verwalten Sie DSGVO-Audit-Sessions. Generieren Sie PDF-Berichte fuer Auditoren und Aufsichtsbehoerden mit vollstaendiger Checkliste, Sign-Off-Status und digitalen Signaturen."
|
||||
audience={['DSB', 'Auditor', 'Compliance Officer']}
|
||||
gdprArticles={[
|
||||
'Art. 5 (Rechenschaftspflicht)',
|
||||
'Art. 24 (Verantwortung des Verantwortlichen)',
|
||||
'Art. 39 (Aufgaben des DSB)',
|
||||
]}
|
||||
architecture={{
|
||||
services: ['backend (Python)', 'ReportLab PDF'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'DSMS', href: '/compliance/dsms', description: 'Uebersicht Datenschutz-Management' },
|
||||
{ name: 'Einwilligungen', href: '/compliance/einwilligungen', description: 'Consent-Tracking' },
|
||||
{ name: 'VVT', href: '/compliance/vvt', description: 'Verarbeitungsverzeichnis' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
|
||||
<span>{error}</span>
|
||||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 mb-6">
|
||||
<button
|
||||
onClick={() => setActiveTab('sessions')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeTab === 'sessions'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Audit-Sessions
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('new')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeTab === 'new'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
+ Neues Audit
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setActiveTab('export')}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors ${
|
||||
activeTab === 'export'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Export-Optionen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sessions Tab */}
|
||||
{activeTab === 'sessions' && (
|
||||
<div>
|
||||
{/* Filter */}
|
||||
<div className="flex items-center gap-4 mb-4">
|
||||
<label className="text-sm text-slate-600">Status:</label>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-200 rounded-lg text-sm"
|
||||
>
|
||||
<option value="all">Alle</option>
|
||||
<option value="draft">Entwurf</option>
|
||||
<option value="in_progress">In Bearbeitung</option>
|
||||
<option value="completed">Abgeschlossen</option>
|
||||
<option value="archived">Archiviert</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={fetchSessions}
|
||||
className="px-3 py-2 text-sm text-purple-600 hover:text-purple-700"
|
||||
>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Sessions List */}
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-slate-500">Lade Audit-Sessions...</div>
|
||||
) : sessions.length === 0 ? (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-8 text-center">
|
||||
<svg className="w-12 h-12 mx-auto text-slate-300 mb-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>
|
||||
<h3 className="text-lg font-medium text-slate-700 mb-2">Keine Audit-Sessions vorhanden</h3>
|
||||
<p className="text-sm text-slate-500 mb-4">
|
||||
Erstellen Sie ein neues Audit, um mit der DSGVO-Pruefung zu beginnen.
|
||||
</p>
|
||||
<button
|
||||
onClick={() => setActiveTab('new')}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Neues Audit erstellen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{sessions.map((session) => (
|
||||
<div
|
||||
key={session.id}
|
||||
className="bg-white rounded-xl border border-slate-200 p-6"
|
||||
>
|
||||
<div className="flex items-start justify-between mb-4">
|
||||
<div>
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-slate-900">{session.name}</h3>
|
||||
{getStatusBadge(session.status)}
|
||||
</div>
|
||||
{session.description && (
|
||||
<p className="text-sm text-slate-500 mt-1">{session.description}</p>
|
||||
)}
|
||||
<div className="flex items-center gap-4 mt-2 text-xs text-slate-500">
|
||||
<span>Auditor: {session.auditor_name}</span>
|
||||
{session.auditor_organization && (
|
||||
<span>| {session.auditor_organization}</span>
|
||||
)}
|
||||
<span>| Erstellt: {new Date(session.created_at).toLocaleDateString('de-DE')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`text-2xl font-bold ${getComplianceColor(session.completion_percentage)}`}>
|
||||
{session.completion_percentage}%
|
||||
</div>
|
||||
<div className="text-xs text-slate-500">
|
||||
{session.completed_items} / {session.total_items} Punkte
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Progress Bar */}
|
||||
<div className="h-2 bg-slate-100 rounded-full overflow-hidden mb-4">
|
||||
<div
|
||||
className={`h-full transition-all ${
|
||||
session.completion_percentage >= 80
|
||||
? 'bg-green-500'
|
||||
: session.completion_percentage >= 50
|
||||
? 'bg-yellow-500'
|
||||
: 'bg-red-500'
|
||||
}`}
|
||||
style={{ width: `${session.completion_percentage}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-3 gap-4 mb-4 text-sm">
|
||||
<div className="text-center p-3 bg-green-50 rounded-lg">
|
||||
<div className="font-semibold text-green-700">{session.compliant_count}</div>
|
||||
<div className="text-xs text-green-600">Konform</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-red-50 rounded-lg">
|
||||
<div className="font-semibold text-red-700">{session.non_compliant_count}</div>
|
||||
<div className="text-xs text-red-600">Nicht Konform</div>
|
||||
</div>
|
||||
<div className="text-center p-3 bg-slate-50 rounded-lg">
|
||||
<div className="font-semibold text-slate-700">
|
||||
{session.total_items - session.completed_items}
|
||||
</div>
|
||||
<div className="text-xs text-slate-600">Ausstehend</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions */}
|
||||
<div className="flex items-center gap-2 pt-4 border-t border-slate-100">
|
||||
{session.status === 'draft' && (
|
||||
<button
|
||||
onClick={() => startSession(session.id)}
|
||||
className="px-3 py-2 bg-blue-600 text-white text-sm rounded-lg hover:bg-blue-700"
|
||||
>
|
||||
Audit starten
|
||||
</button>
|
||||
)}
|
||||
{session.status === 'in_progress' && (
|
||||
<button
|
||||
onClick={() => completeSession(session.id)}
|
||||
className="px-3 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700"
|
||||
>
|
||||
Abschliessen
|
||||
</button>
|
||||
)}
|
||||
{(session.status === 'completed' || session.status === 'in_progress') && (
|
||||
<button
|
||||
onClick={() => downloadPdf(session.id)}
|
||||
disabled={generatingPdf === session.id}
|
||||
className="px-3 py-2 bg-purple-600 text-white text-sm rounded-lg hover:bg-purple-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{generatingPdf === session.id ? (
|
||||
<>
|
||||
<svg className="w-4 h-4 animate-spin" 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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
Generiere PDF...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 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>
|
||||
PDF-Report
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
)}
|
||||
{(session.status === 'draft' || session.status === 'archived') && (
|
||||
<button
|
||||
onClick={() => deleteSession(session.id)}
|
||||
className="px-3 py-2 text-red-600 text-sm hover:text-red-700"
|
||||
>
|
||||
Loeschen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* New Session Tab */}
|
||||
{activeTab === 'new' && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Neues Audit erstellen</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{/* Name */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Audit-Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newSession.name}
|
||||
onChange={(e) => setNewSession({ ...newSession, name: e.target.value })}
|
||||
placeholder="z.B. DSGVO Jahresaudit 2026"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Description */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Beschreibung
|
||||
</label>
|
||||
<textarea
|
||||
value={newSession.description}
|
||||
onChange={(e) => setNewSession({ ...newSession, description: e.target.value })}
|
||||
rows={3}
|
||||
placeholder="Optionale Beschreibung des Audit-Umfangs"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Auditor Info */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Auditor Name *
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newSession.auditor_name}
|
||||
onChange={(e) => setNewSession({ ...newSession, auditor_name: e.target.value })}
|
||||
placeholder="Name des Auditors"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
E-Mail
|
||||
</label>
|
||||
<input
|
||||
type="email"
|
||||
value={newSession.auditor_email}
|
||||
onChange={(e) => setNewSession({ ...newSession, auditor_email: e.target.value })}
|
||||
placeholder="auditor@example.com"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">
|
||||
Organisation
|
||||
</label>
|
||||
<input
|
||||
type="text"
|
||||
value={newSession.auditor_organization}
|
||||
onChange={(e) => setNewSession({ ...newSession, auditor_organization: e.target.value })}
|
||||
placeholder="z.B. TUeV, Aufsichtsbehoerde"
|
||||
className="w-full px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Regulations */}
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Zu pruefende Regelwerke
|
||||
</label>
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-3">
|
||||
{REGULATIONS.map((reg) => (
|
||||
<label
|
||||
key={reg.code}
|
||||
className={`flex items-center gap-3 p-3 border rounded-lg cursor-pointer transition-colors ${
|
||||
newSession.regulation_codes.includes(reg.code)
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={newSession.regulation_codes.includes(reg.code)}
|
||||
onChange={(e) => {
|
||||
if (e.target.checked) {
|
||||
setNewSession({
|
||||
...newSession,
|
||||
regulation_codes: [...newSession.regulation_codes, reg.code],
|
||||
})
|
||||
} else {
|
||||
setNewSession({
|
||||
...newSession,
|
||||
regulation_codes: newSession.regulation_codes.filter((c) => c !== reg.code),
|
||||
})
|
||||
}
|
||||
}}
|
||||
className="w-4 h-4 text-purple-600"
|
||||
/>
|
||||
<div>
|
||||
<div className="font-medium text-slate-800">{reg.name}</div>
|
||||
<div className="text-xs text-slate-500">{reg.description}</div>
|
||||
</div>
|
||||
</label>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Submit */}
|
||||
<div className="pt-4 border-t border-slate-100">
|
||||
<button
|
||||
onClick={createSession}
|
||||
disabled={creating}
|
||||
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 flex items-center gap-2"
|
||||
>
|
||||
{creating ? (
|
||||
<>
|
||||
<svg className="w-5 h-5 animate-spin" 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 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
|
||||
</svg>
|
||||
Erstelle...
|
||||
</>
|
||||
) : (
|
||||
'Audit-Session erstellen'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Export Options Tab */}
|
||||
{activeTab === 'export' && (
|
||||
<div className="space-y-6">
|
||||
{/* PDF Language Settings */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">PDF-Export Einstellungen</h3>
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Sprache</label>
|
||||
<div className="flex gap-3">
|
||||
<label className={`flex items-center gap-2 px-4 py-2 border rounded-lg cursor-pointer ${pdfLanguage === 'de' ? 'border-purple-500 bg-purple-50' : 'border-slate-200'}`}>
|
||||
<input
|
||||
type="radio"
|
||||
checked={pdfLanguage === 'de'}
|
||||
onChange={() => setPdfLanguage('de')}
|
||||
className="w-4 h-4 text-purple-600"
|
||||
/>
|
||||
<span>Deutsch</span>
|
||||
</label>
|
||||
<label className={`flex items-center gap-2 px-4 py-2 border rounded-lg cursor-pointer ${pdfLanguage === 'en' ? 'border-purple-500 bg-purple-50' : 'border-slate-200'}`}>
|
||||
<input
|
||||
type="radio"
|
||||
checked={pdfLanguage === 'en'}
|
||||
onChange={() => setPdfLanguage('en')}
|
||||
className="w-4 h-4 text-purple-600"
|
||||
/>
|
||||
<span>English</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Export Types Info */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Verfuegbare Export-Formate</h3>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-4 p-4 bg-slate-50 rounded-lg">
|
||||
<div className="w-10 h-10 rounded-lg bg-red-100 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-800">PDF Audit Report</h4>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
Vollstaendiger Audit-Bericht mit Deckblatt, Executive Summary, Checkliste und digitalen Signaturen.
|
||||
Ideal fuer Aufsichtsbehoerden und offizielle Dokumentation.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 p-4 bg-slate-50 rounded-lg">
|
||||
<div className="w-10 h-10 rounded-lg bg-green-100 flex items-center justify-center flex-shrink-0">
|
||||
<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 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-8l-4-4m0 0L8 8m4-4v12" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-800">ZIP Export-Paket</h4>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
Komplettes Export-Paket mit Regelwerken, Controls, Nachweisen und interaktivem HTML-Index.
|
||||
Fuer externe Auditoren zur detaillierten Pruefung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-start gap-4 p-4 bg-slate-50 rounded-lg">
|
||||
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-blue-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>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-800">Compliance Report (JSON)</h4>
|
||||
<p className="text-sm text-slate-600 mt-1">
|
||||
Strukturierter Bericht mit Statistiken, Trends und Empfehlungen.
|
||||
Fuer Integration in andere Systeme und Dashboards.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tip */}
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<svg className="w-5 h-5 text-purple-600 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-medium text-purple-800">Tipp</h4>
|
||||
<p className="text-sm text-purple-700 mt-1">
|
||||
Der PDF-Report enthaelt SHA-256-Signaturen fuer alle Sign-Offs. Diese koennen zur Integritaetspruefung
|
||||
verwendet werden und belegen, dass die Bewertungen nicht nachtraeglich veraendert wurden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+74
-54
@@ -7,11 +7,12 @@
|
||||
* - Documents (AGB, Privacy, etc.)
|
||||
* - Document Versions
|
||||
* - Email Templates
|
||||
* - GDPR Processes (Art. 15-21)
|
||||
* - Statistics
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// API Proxy URL (avoids CORS issues)
|
||||
const API_BASE = '/api/admin/consent'
|
||||
@@ -39,7 +40,7 @@ interface Version {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export default function ConsentAdminPage() {
|
||||
export default function ConsentPage() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('documents')
|
||||
const [documents, setDocuments] = useState<Document[]>([])
|
||||
const [versions, setVersions] = useState<Version[]>([])
|
||||
@@ -120,25 +121,25 @@ export default function ConsentAdminPage() {
|
||||
const emailTemplates = [
|
||||
// Onboarding
|
||||
{ name: 'Willkommens-E-Mail', key: 'welcome', category: 'onboarding', description: 'Wird bei Registrierung versendet' },
|
||||
{ name: 'E-Mail Bestätigung', key: 'email_verification', category: 'onboarding', description: 'Bestätigungslink für E-Mail-Adresse' },
|
||||
{ name: 'Konto aktiviert', key: 'account_activated', category: 'onboarding', description: 'Bestätigung der Kontoaktivierung' },
|
||||
{ name: 'E-Mail Bestaetigung', key: 'email_verification', category: 'onboarding', description: 'Bestaetigungslink fuer E-Mail-Adresse' },
|
||||
{ name: 'Konto aktiviert', key: 'account_activated', category: 'onboarding', description: 'Bestaetigung der Kontoaktivierung' },
|
||||
// Security
|
||||
{ name: 'Passwort zurücksetzen', key: 'password_reset', category: 'security', description: 'Link zum Zurücksetzen des Passworts' },
|
||||
{ name: 'Passwort geändert', key: 'password_changed', category: 'security', description: 'Bestätigung der Passwortänderung' },
|
||||
{ name: 'Neue Anmeldung', key: 'new_login', category: 'security', description: 'Benachrichtigung über Anmeldung von neuem Gerät' },
|
||||
{ name: '2FA aktiviert', key: '2fa_enabled', category: 'security', description: 'Bestätigung der 2FA-Aktivierung' },
|
||||
{ name: 'Passwort zuruecksetzen', key: 'password_reset', category: 'security', description: 'Link zum Zuruecksetzen des Passworts' },
|
||||
{ name: 'Passwort geaendert', key: 'password_changed', category: 'security', description: 'Bestaetigung der Passwortaenderung' },
|
||||
{ name: 'Neue Anmeldung', key: 'new_login', category: 'security', description: 'Benachrichtigung ueber Anmeldung von neuem Geraet' },
|
||||
{ name: '2FA aktiviert', key: '2fa_enabled', category: 'security', description: 'Bestaetigung der 2FA-Aktivierung' },
|
||||
// Consent & Legal
|
||||
{ name: 'Neue Dokumentversion', key: 'new_document_version', category: 'consent', description: 'Info über neue Dokumentversion zur Zustimmung' },
|
||||
{ name: 'Zustimmung erteilt', key: 'consent_given', category: 'consent', description: 'Bestätigung der erteilten Zustimmung' },
|
||||
{ name: 'Zustimmung widerrufen', key: 'consent_withdrawn', category: 'consent', description: 'Bestätigung des Widerrufs' },
|
||||
{ name: 'Neue Dokumentversion', key: 'new_document_version', category: 'consent', description: 'Info ueber neue Dokumentversion zur Zustimmung' },
|
||||
{ name: 'Zustimmung erteilt', key: 'consent_given', category: 'consent', description: 'Bestaetigung der erteilten Zustimmung' },
|
||||
{ name: 'Zustimmung widerrufen', key: 'consent_withdrawn', category: 'consent', description: 'Bestaetigung des Widerrufs' },
|
||||
// Data Subject Rights (GDPR)
|
||||
{ name: 'DSR Anfrage erhalten', key: 'dsr_received', category: 'gdpr', description: 'Bestätigung des Eingangs einer DSGVO-Anfrage' },
|
||||
{ name: 'Datenexport bereit', key: 'export_ready', category: 'gdpr', description: 'Benachrichtigung über fertigen Datenexport' },
|
||||
{ name: 'Daten gelöscht', key: 'data_deleted', category: 'gdpr', description: 'Bestätigung der Datenlöschung' },
|
||||
{ name: 'Daten berichtigt', key: 'data_rectified', category: 'gdpr', description: 'Bestätigung der Datenberichtigung' },
|
||||
{ name: 'DSR Anfrage erhalten', key: 'dsr_received', category: 'gdpr', description: 'Bestaetigung des Eingangs einer DSGVO-Anfrage' },
|
||||
{ name: 'Datenexport bereit', key: 'export_ready', category: 'gdpr', description: 'Benachrichtigung ueber fertigen Datenexport' },
|
||||
{ name: 'Daten geloescht', key: 'data_deleted', category: 'gdpr', description: 'Bestaetigung der Datenloeschung' },
|
||||
{ name: 'Daten berichtigt', key: 'data_rectified', category: 'gdpr', description: 'Bestaetigung der Datenberichtigung' },
|
||||
// Account Lifecycle
|
||||
{ name: 'Konto deaktiviert', key: 'account_deactivated', category: 'lifecycle', description: 'Konto wurde deaktiviert' },
|
||||
{ name: 'Konto gelöscht', key: 'account_deleted', category: 'lifecycle', description: 'Bestätigung der Kontolöschung' },
|
||||
{ name: 'Konto geloescht', key: 'account_deleted', category: 'lifecycle', description: 'Bestaetigung der Kontoloeschung' },
|
||||
]
|
||||
|
||||
// GDPR Article 15-21 Processes
|
||||
@@ -146,8 +147,8 @@ export default function ConsentAdminPage() {
|
||||
{
|
||||
article: '15',
|
||||
title: 'Auskunftsrecht',
|
||||
description: 'Recht auf Bestätigung und Auskunft über verarbeitete personenbezogene Daten',
|
||||
actions: ['Datenexport generieren', 'Verarbeitungszwecke auflisten', 'Empfänger auflisten'],
|
||||
description: 'Recht auf Bestaetigung und Auskunft ueber verarbeitete personenbezogene Daten',
|
||||
actions: ['Datenexport generieren', 'Verarbeitungszwecke auflisten', 'Empfaenger auflisten'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
@@ -155,39 +156,39 @@ export default function ConsentAdminPage() {
|
||||
article: '16',
|
||||
title: 'Recht auf Berichtigung',
|
||||
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
|
||||
actions: ['Daten bearbeiten', 'Änderungshistorie führen', 'Benachrichtigung senden'],
|
||||
actions: ['Daten bearbeiten', 'Aenderungshistorie fuehren', 'Benachrichtigung senden'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '17',
|
||||
title: 'Recht auf Löschung ("Vergessenwerden")',
|
||||
description: 'Recht auf Löschung personenbezogener Daten unter bestimmten Voraussetzungen',
|
||||
actions: ['Löschantrag prüfen', 'Daten löschen', 'Aufbewahrungsfristen prüfen', 'Löschbestätigung senden'],
|
||||
title: 'Recht auf Loeschung ("Vergessenwerden")',
|
||||
description: 'Recht auf Loeschung personenbezogener Daten unter bestimmten Voraussetzungen',
|
||||
actions: ['Loeschantrag pruefen', 'Daten loeschen', 'Aufbewahrungsfristen pruefen', 'Loeschbestaetigung senden'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '18',
|
||||
title: 'Recht auf Einschränkung der Verarbeitung',
|
||||
description: 'Recht auf Markierung von Daten zur eingeschränkten Verarbeitung',
|
||||
actions: ['Daten markieren', 'Verarbeitung einschränken', 'Benachrichtigung bei Aufhebung'],
|
||||
title: 'Recht auf Einschraenkung der Verarbeitung',
|
||||
description: 'Recht auf Markierung von Daten zur eingeschraenkten Verarbeitung',
|
||||
actions: ['Daten markieren', 'Verarbeitung einschraenken', 'Benachrichtigung bei Aufhebung'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '19',
|
||||
title: 'Mitteilungspflicht',
|
||||
description: 'Pflicht zur Mitteilung von Berichtigung, Löschung oder Einschränkung an Empfänger',
|
||||
actions: ['Empfänger ermitteln', 'Mitteilungen versenden', 'Protokollierung'],
|
||||
sla: 'Unverzüglich',
|
||||
description: 'Pflicht zur Mitteilung von Berichtigung, Loeschung oder Einschraenkung an Empfaenger',
|
||||
actions: ['Empfaenger ermitteln', 'Mitteilungen versenden', 'Protokollierung'],
|
||||
sla: 'Unverzueglich',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '20',
|
||||
title: 'Recht auf Datenübertragbarkeit',
|
||||
title: 'Recht auf Datenuebertragbarkeit',
|
||||
description: 'Recht auf Erhalt der Daten in strukturiertem, maschinenlesbarem Format',
|
||||
actions: ['JSON/CSV Export', 'API-Schnittstelle', 'Direkte Übertragung'],
|
||||
actions: ['JSON/CSV Export', 'API-Schnittstelle', 'Direkte Uebertragung'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
@@ -196,7 +197,7 @@ export default function ConsentAdminPage() {
|
||||
title: 'Widerspruchsrecht',
|
||||
description: 'Recht auf Widerspruch gegen Verarbeitung aus berechtigtem Interesse oder Direktwerbung',
|
||||
actions: ['Widerspruch erfassen', 'Verarbeitung stoppen', 'Marketing-Opt-out'],
|
||||
sla: 'Unverzüglich',
|
||||
sla: 'Unverzueglich',
|
||||
status: 'active'
|
||||
},
|
||||
]
|
||||
@@ -210,7 +211,26 @@ export default function ConsentAdminPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<AdminLayout title="Consent Verwaltung" description="Rechtliche Dokumente & Versionen">
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Consent Verwaltung"
|
||||
purpose="Verwalten Sie rechtliche Dokumente (AGB, Datenschutz, Cookie-Richtlinien) und deren Versionen. Jede Einwilligung eines Benutzers basiert auf diesen Dokumenten und muss nachvollziehbar sein."
|
||||
audience={['DSB', 'Entwickler', 'Compliance Officer']}
|
||||
gdprArticles={['Art. 7 (Einwilligung)', 'Art. 13/14 (Informationspflichten)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'DSR-Verwaltung', href: '/compliance/dsr', description: 'Datenschutzanfragen bearbeiten' },
|
||||
{ name: 'DSGVO-Audit', href: '/compliance/audit', description: 'Audit-Dokumentation erstellen' },
|
||||
{ name: 'Workflow', href: '/compliance/workflow', description: 'Freigabe-Prozesse konfigurieren' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
/>
|
||||
|
||||
{/* Token Input */}
|
||||
{!authToken && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6">
|
||||
@@ -257,7 +277,7 @@ export default function ConsentAdminPage() {
|
||||
onClick={() => setError(null)}
|
||||
className="ml-4 text-red-500 hover:text-red-700"
|
||||
>
|
||||
✕
|
||||
X
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
@@ -268,7 +288,7 @@ export default function ConsentAdminPage() {
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Dokumente verwalten</h2>
|
||||
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium">
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
|
||||
+ Neues Dokument
|
||||
</button>
|
||||
</div>
|
||||
@@ -318,7 +338,7 @@ export default function ConsentAdminPage() {
|
||||
setSelectedDocument(doc.id)
|
||||
setActiveTab('versions')
|
||||
}}
|
||||
className="text-primary-600 hover:text-primary-700 text-sm font-medium mr-3"
|
||||
className="text-purple-600 hover:text-purple-700 text-sm font-medium mr-3"
|
||||
>
|
||||
Versionen
|
||||
</button>
|
||||
@@ -346,7 +366,7 @@ export default function ConsentAdminPage() {
|
||||
onChange={(e) => setSelectedDocument(e.target.value)}
|
||||
className="px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
>
|
||||
<option value="">Dokument auswählen...</option>
|
||||
<option value="">Dokument auswaehlen...</option>
|
||||
{documents.map((doc) => (
|
||||
<option key={doc.id} value={doc.id}>
|
||||
{doc.name}
|
||||
@@ -355,7 +375,7 @@ export default function ConsentAdminPage() {
|
||||
</select>
|
||||
</div>
|
||||
{selectedDocument && (
|
||||
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium">
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
|
||||
+ Neue Version
|
||||
</button>
|
||||
)}
|
||||
@@ -363,7 +383,7 @@ export default function ConsentAdminPage() {
|
||||
|
||||
{!selectedDocument ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
Bitte wählen Sie ein Dokument aus
|
||||
Bitte waehlen Sie ein Dokument aus
|
||||
</div>
|
||||
) : loading ? (
|
||||
<div className="text-center py-12 text-slate-500">Lade Versionen...</div>
|
||||
@@ -376,7 +396,7 @@ export default function ConsentAdminPage() {
|
||||
{versions.map((version) => (
|
||||
<div
|
||||
key={version.id}
|
||||
className="border border-slate-200 rounded-lg p-4 hover:border-primary-300 transition-colors"
|
||||
className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
@@ -408,7 +428,7 @@ export default function ConsentAdminPage() {
|
||||
</button>
|
||||
{version.status === 'draft' && (
|
||||
<button className="px-3 py-1.5 text-sm text-white bg-green-600 hover:bg-green-700 rounded-lg">
|
||||
Veröffentlichen
|
||||
Veroeffentlichen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
@@ -426,9 +446,9 @@ export default function ConsentAdminPage() {
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">E-Mail Vorlagen</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">16 Lifecycle-Vorlagen für automatisierte Kommunikation</p>
|
||||
<p className="text-sm text-slate-500 mt-1">16 Lifecycle-Vorlagen fuer automatisierte Kommunikation</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium">
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
|
||||
+ Neue Vorlage
|
||||
</button>
|
||||
</div>
|
||||
@@ -456,15 +476,15 @@ export default function ConsentAdminPage() {
|
||||
.map((template) => (
|
||||
<div
|
||||
key={template.key}
|
||||
className="border border-slate-200 rounded-lg p-4 flex items-center justify-between hover:border-primary-300 transition-colors bg-white"
|
||||
className="border border-slate-200 rounded-lg p-4 flex items-center justify-between hover:border-purple-300 transition-colors bg-white"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${category.color} flex items-center justify-center text-lg`}>
|
||||
{category.key === 'onboarding' && '👋'}
|
||||
{category.key === 'security' && '🔒'}
|
||||
{category.key === 'consent' && '✓'}
|
||||
{category.key === 'gdpr' && '📋'}
|
||||
{category.key === 'lifecycle' && '🔄'}
|
||||
{category.key === 'onboarding' && ''}
|
||||
{category.key === 'security' && ''}
|
||||
{category.key === 'consent' && ''}
|
||||
{category.key === 'gdpr' && ''}
|
||||
{category.key === 'lifecycle' && ''}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-900">{template.name}</h4>
|
||||
@@ -496,7 +516,7 @@ export default function ConsentAdminPage() {
|
||||
<h2 className="text-lg font-semibold text-slate-900">DSGVO Betroffenenrechte</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">Artikel 15-21 Prozesse und Vorlagen</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 transition-colors text-sm font-medium">
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 transition-colors text-sm font-medium">
|
||||
+ DSR Anfrage erstellen
|
||||
</button>
|
||||
</div>
|
||||
@@ -504,7 +524,7 @@ export default function ConsentAdminPage() {
|
||||
{/* Info Banner */}
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-lg p-4 mb-6">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="text-2xl">⚖️</span>
|
||||
<span className="text-2xl">*</span>
|
||||
<div>
|
||||
<h4 className="font-medium text-purple-900">Data Subject Rights (DSR)</h4>
|
||||
<p className="text-sm text-purple-700 mt-1">
|
||||
@@ -570,7 +590,7 @@ export default function ConsentAdminPage() {
|
||||
|
||||
{/* DSR Request Statistics */}
|
||||
<div className="mt-8 pt-6 border-t border-slate-200">
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-4">DSR Übersicht</h3>
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-4">DSR Uebersicht</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-slate-900">0</div>
|
||||
@@ -586,7 +606,7 @@ export default function ConsentAdminPage() {
|
||||
</div>
|
||||
<div className="bg-red-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-red-700">0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Überfällig</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Ueberfaellig</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -616,13 +636,13 @@ export default function ConsentAdminPage() {
|
||||
<div className="border border-slate-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Zustimmungsrate nach Dokument</h3>
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Noch keine Daten verfügbar
|
||||
Noch keine Daten verfuegbar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+174
-77
@@ -4,15 +4,15 @@
|
||||
* Control Catalogue Page
|
||||
*
|
||||
* Features:
|
||||
* - List all controls with filters
|
||||
* - Control detail view
|
||||
* - Status update / Review
|
||||
* - List all 44+ controls with filters
|
||||
* - Domain-based organization (9 domains)
|
||||
* - Status update / Review workflow
|
||||
* - Evidence linking
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface Control {
|
||||
id: string
|
||||
@@ -58,12 +58,12 @@ const DOMAIN_COLORS: Record<string, string> = {
|
||||
aud: 'bg-indigo-100 text-indigo-700',
|
||||
}
|
||||
|
||||
const STATUS_STYLES: Record<string, { bg: string; text: string; icon: string }> = {
|
||||
pass: { bg: 'bg-green-100', text: 'text-green-700', icon: 'M5 13l4 4L19 7' },
|
||||
partial: { bg: 'bg-yellow-100', text: 'text-yellow-700', icon: 'M12 8v4m0 4h.01' },
|
||||
fail: { bg: 'bg-red-100', text: 'text-red-700', icon: 'M6 18L18 6M6 6l12 12' },
|
||||
planned: { bg: 'bg-slate-100', text: 'text-slate-700', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z' },
|
||||
'n/a': { bg: 'bg-slate-100', text: 'text-slate-500', icon: 'M20 12H4' },
|
||||
const STATUS_STYLES: Record<string, { bg: string; text: string; icon: string; label: string }> = {
|
||||
pass: { bg: 'bg-green-100', text: 'text-green-700', icon: 'M5 13l4 4L19 7', label: 'Bestanden' },
|
||||
partial: { bg: 'bg-yellow-100', text: 'text-yellow-700', icon: 'M12 8v4m0 4h.01', label: 'Teilweise' },
|
||||
fail: { bg: 'bg-red-100', text: 'text-red-700', icon: 'M6 18L18 6M6 6l12 12', label: 'Nicht bestanden' },
|
||||
planned: { bg: 'bg-slate-100', text: 'text-slate-700', icon: 'M12 8v4l3 3m6-3a9 9 0 11-18 0 9 9 0 0118 0z', label: 'Geplant' },
|
||||
'n/a': { bg: 'bg-slate-100', text: 'text-slate-500', icon: 'M20 12H4', label: 'Nicht anwendbar' },
|
||||
}
|
||||
|
||||
export default function ControlsPage() {
|
||||
@@ -76,8 +76,7 @@ export default function ControlsPage() {
|
||||
const [reviewModalOpen, setReviewModalOpen] = useState(false)
|
||||
const [reviewStatus, setReviewStatus] = useState('pass')
|
||||
const [reviewNotes, setReviewNotes] = useState('')
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
loadControls()
|
||||
@@ -91,7 +90,7 @@ export default function ControlsPage() {
|
||||
if (filterStatus) params.append('status', filterStatus)
|
||||
if (searchTerm) params.append('search', searchTerm)
|
||||
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/controls?${params}`)
|
||||
const res = await fetch(`/api/admin/compliance/controls?${params}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setControls(data.controls || [])
|
||||
@@ -117,8 +116,9 @@ export default function ControlsPage() {
|
||||
const submitReview = async () => {
|
||||
if (!selectedControl) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/controls/${selectedControl.control_id}/review`, {
|
||||
const res = await fetch(`/api/admin/compliance/controls/${selectedControl.control_id}/review`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -136,6 +136,8 @@ export default function ControlsPage() {
|
||||
} catch (error) {
|
||||
console.error('Review failed:', error)
|
||||
alert('Fehler beim Speichern')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -157,21 +159,88 @@ export default function ControlsPage() {
|
||||
return days
|
||||
}
|
||||
|
||||
// Statistics
|
||||
const stats = {
|
||||
total: controls.length,
|
||||
pass: controls.filter(c => c.status === 'pass').length,
|
||||
partial: controls.filter(c => c.status === 'partial').length,
|
||||
fail: controls.filter(c => c.status === 'fail').length,
|
||||
planned: controls.filter(c => c.status === 'planned').length,
|
||||
automated: controls.filter(c => c.is_automated).length,
|
||||
overdue: controls.filter(c => {
|
||||
if (!c.next_review_at) return false
|
||||
return new Date(c.next_review_at) < new Date()
|
||||
}).length,
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout title="Control Catalogue" description="Technische & organisatorische Controls">
|
||||
{/* Header Actions */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-6">
|
||||
<div className="min-h-screen bg-slate-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Control Catalogue</h1>
|
||||
<p className="text-slate-600">Technische & organisatorische Massnahmen</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/compliance"
|
||||
className="text-sm text-slate-500 hover:text-slate-700 flex items-center gap-1"
|
||||
href="/compliance/hub"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Zurueck zum Dashboard
|
||||
Compliance Hub
|
||||
</Link>
|
||||
<div className="flex-1" />
|
||||
<span className="text-sm text-slate-500">{filteredControls.length} Controls</span>
|
||||
</div>
|
||||
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Control Catalogue"
|
||||
purpose="Der Control-Katalog dokumentiert alle technischen und organisatorischen Massnahmen (TOMs) zur Einhaltung von ISO 27001, DSGVO, AI Act und BSI TR-03161. Jede Massnahme wird regelmaessig reviewed und mit Nachweisen verknuepft."
|
||||
audience={['CISO', 'DSB', 'Compliance Officer', 'Entwickler']}
|
||||
gdprArticles={['Art. 32 (Sicherheit)', 'Art. 25 (Privacy by Design)']}
|
||||
architecture={{
|
||||
services: ['Python Backend (FastAPI)', 'compliance_controls Modul'],
|
||||
databases: ['PostgreSQL (compliance_controls Table)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Evidence', href: '/compliance/evidence', description: 'Nachweise zu Controls verwalten' },
|
||||
{ name: 'Audit Checklist', href: '/compliance/audit-checklist', description: 'Anforderungen pruefen' },
|
||||
{ name: 'Risks', href: '/compliance/risks', description: 'Risiko-Matrix verwalten' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<p className="text-sm text-slate-500">Gesamt</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{stats.total}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-green-200">
|
||||
<p className="text-sm text-green-600">Bestanden</p>
|
||||
<p className="text-2xl font-bold text-green-700">{stats.pass}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-yellow-200">
|
||||
<p className="text-sm text-yellow-600">Teilweise</p>
|
||||
<p className="text-2xl font-bold text-yellow-700">{stats.partial}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-red-200">
|
||||
<p className="text-sm text-red-600">Nicht bestanden</p>
|
||||
<p className="text-2xl font-bold text-red-700">{stats.fail}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<p className="text-sm text-slate-500">Geplant</p>
|
||||
<p className="text-2xl font-bold text-slate-700">{stats.planned}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-blue-200">
|
||||
<p className="text-sm text-blue-600">Automatisiert</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{stats.automated}</p>
|
||||
</div>
|
||||
<div className={`bg-white rounded-xl p-4 border ${stats.overdue > 0 ? 'border-red-300 bg-red-50' : 'border-slate-200'}`}>
|
||||
<p className="text-sm text-slate-500">Review faellig</p>
|
||||
<p className={`text-2xl font-bold ${stats.overdue > 0 ? 'text-red-600' : 'text-slate-700'}`}>
|
||||
{stats.overdue}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
@@ -180,7 +249,7 @@ export default function ControlsPage() {
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Control suchen..."
|
||||
placeholder="Control suchen (ID, Titel, Beschreibung)..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
onKeyDown={(e) => e.key === 'Enter' && handleSearch()}
|
||||
@@ -205,11 +274,9 @@ export default function ControlsPage() {
|
||||
className="px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
<option value="">Alle Status</option>
|
||||
<option value="pass">Bestanden</option>
|
||||
<option value="partial">Teilweise</option>
|
||||
<option value="fail">Nicht bestanden</option>
|
||||
<option value="planned">Geplant</option>
|
||||
<option value="n/a">Nicht anwendbar</option>
|
||||
{Object.entries(STATUS_STYLES).map(([key, style]) => (
|
||||
<option key={key} value={key}>{style.label}</option>
|
||||
))}
|
||||
</select>
|
||||
|
||||
<button
|
||||
@@ -226,11 +293,24 @@ export default function ControlsPage() {
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
) : filteredControls.length === 0 ? (
|
||||
<div className="bg-white rounded-xl shadow-sm border 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 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
|
||||
</svg>
|
||||
<p className="text-slate-500">Keine Controls gefunden</p>
|
||||
<p className="text-sm text-slate-400 mt-1">
|
||||
Versuchen Sie andere Filter oder laden Sie die Compliance-Daten im Hub
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className="px-4 py-3 border-b bg-slate-50 flex justify-between items-center">
|
||||
<span className="text-sm text-slate-500">{filteredControls.length} Controls</span>
|
||||
</div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<thead className="bg-slate-50 border-b">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Domain</th>
|
||||
@@ -270,7 +350,7 @@ export default function ControlsPage() {
|
||||
<svg className="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={statusStyle.icon} />
|
||||
</svg>
|
||||
{control.status}
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
@@ -287,7 +367,7 @@ export default function ControlsPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
<Link
|
||||
href={`/admin/compliance/evidence?control=${control.control_id}`}
|
||||
href={`/compliance/evidence?control=${control.control_id}`}
|
||||
className="text-primary-600 hover:text-primary-700 font-medium"
|
||||
>
|
||||
{control.evidence_count || 0}
|
||||
@@ -295,8 +375,16 @@ export default function ControlsPage() {
|
||||
</td>
|
||||
<td className="px-4 py-3 text-center">
|
||||
{daysUntilReview !== null ? (
|
||||
<span className={`text-sm ${daysUntilReview < 0 ? 'text-red-600 font-medium' : daysUntilReview < 14 ? 'text-yellow-600' : 'text-slate-500'}`}>
|
||||
{daysUntilReview < 0 ? `${Math.abs(daysUntilReview)}d ueberfaellig` : `${daysUntilReview}d`}
|
||||
<span className={`text-sm ${
|
||||
daysUntilReview < 0
|
||||
? 'text-red-600 font-medium'
|
||||
: daysUntilReview < 14
|
||||
? 'text-yellow-600'
|
||||
: 'text-slate-500'
|
||||
}`}>
|
||||
{daysUntilReview < 0
|
||||
? `${Math.abs(daysUntilReview)}d ueberfaellig`
|
||||
: `${daysUntilReview}d`}
|
||||
</span>
|
||||
) : (
|
||||
<span className="text-slate-400 text-sm">-</span>
|
||||
@@ -321,67 +409,76 @@ export default function ControlsPage() {
|
||||
|
||||
{/* Review Modal */}
|
||||
{reviewModalOpen && selectedControl && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">
|
||||
Control Review: {selectedControl.control_id}
|
||||
</h3>
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-6 border-b">
|
||||
<h3 className="text-lg font-semibold text-slate-900">
|
||||
Control Review
|
||||
</h3>
|
||||
<p className="text-sm text-slate-500 font-mono">{selectedControl.control_id}</p>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<p className="text-sm text-slate-500 mb-2">{selectedControl.title}</p>
|
||||
<div className="p-3 bg-slate-50 rounded-lg text-sm">
|
||||
<p className="font-medium text-slate-700 mb-1">Pass-Kriterium:</p>
|
||||
<p className="text-slate-600">{selectedControl.pass_criteria}</p>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<p className="font-medium text-slate-700 mb-1">{selectedControl.title}</p>
|
||||
{selectedControl.pass_criteria && (
|
||||
<div className="p-3 bg-slate-50 rounded-lg text-sm">
|
||||
<p className="font-medium text-slate-600 mb-1">Pass-Kriterium:</p>
|
||||
<p className="text-slate-600">{selectedControl.pass_criteria}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Status</label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{Object.entries(STATUS_STYLES).map(([key, style]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setReviewStatus(key)}
|
||||
className={`p-2 rounded-lg border-2 text-xs font-medium transition-colors ${
|
||||
reviewStatus === key
|
||||
? `${style.bg} ${style.text} border-current`
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{style.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Notizen</label>
|
||||
<textarea
|
||||
value={reviewNotes}
|
||||
onChange={(e) => setReviewNotes(e.target.value)}
|
||||
placeholder="Begruendung, Nachweise, naechste Schritte..."
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-4">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Status</label>
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{Object.entries(STATUS_STYLES).map(([key, style]) => (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => setReviewStatus(key)}
|
||||
className={`p-2 rounded-lg border-2 text-sm font-medium transition-colors ${
|
||||
reviewStatus === key
|
||||
? `${style.bg} ${style.text} border-current`
|
||||
: 'border-slate-200 hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
{key}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mb-6">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">Notizen</label>
|
||||
<textarea
|
||||
value={reviewNotes}
|
||||
onChange={(e) => setReviewNotes(e.target.value)}
|
||||
placeholder="Begruendung, Nachweise, naechste Schritte..."
|
||||
rows={3}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3">
|
||||
<div className="p-6 border-t bg-slate-50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setReviewModalOpen(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
disabled={saving}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={submitReview}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
Speichern
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,691 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DSFA - Datenschutz-Folgenabschaetzung
|
||||
*
|
||||
* Art. 35 DSGVO - Datenschutz-Folgenabschaetzung
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface DSFAProject {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
status: 'draft' | 'in_progress' | 'completed' | 'review_needed'
|
||||
riskLevel: 'low' | 'medium' | 'high' | 'critical'
|
||||
createdAt: string
|
||||
lastUpdated: string
|
||||
dpoApproval?: boolean
|
||||
phases: {
|
||||
description: boolean
|
||||
necessity: boolean
|
||||
risks: boolean
|
||||
measures: boolean
|
||||
consultation: boolean
|
||||
}
|
||||
}
|
||||
|
||||
interface RiskAssessment {
|
||||
id: string
|
||||
category: string
|
||||
risk: string
|
||||
likelihood: 'rare' | 'unlikely' | 'possible' | 'likely' | 'certain'
|
||||
impact: 'negligible' | 'minor' | 'moderate' | 'major' | 'severe'
|
||||
riskScore: number
|
||||
mitigations: string[]
|
||||
residualRisk: 'acceptable' | 'tolerable' | 'unacceptable'
|
||||
}
|
||||
|
||||
export default function DSFAPage() {
|
||||
const [activeTab, setActiveTab] = useState<'overview' | 'projects' | 'methodology' | 'templates'>('overview')
|
||||
const [expandedProject, setExpandedProject] = useState<string | null>('ai_processing')
|
||||
|
||||
const dsfaProjects: DSFAProject[] = [
|
||||
{
|
||||
id: 'ai_processing',
|
||||
name: 'KI-gestuetzte Korrektur und Bewertung',
|
||||
description: 'Automatische Korrektur von Schuelerarbeiten mittels KI (Ollama/OpenAI)',
|
||||
status: 'in_progress',
|
||||
riskLevel: 'high',
|
||||
createdAt: '2024-10-01',
|
||||
lastUpdated: '2024-12-01',
|
||||
dpoApproval: false,
|
||||
phases: {
|
||||
description: true,
|
||||
necessity: true,
|
||||
risks: true,
|
||||
measures: false,
|
||||
consultation: false
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'learning_analytics',
|
||||
name: 'Lernfortschrittsanalyse',
|
||||
description: 'Systematische Analyse des Lernverhaltens zur Personalisierung',
|
||||
status: 'completed',
|
||||
riskLevel: 'medium',
|
||||
createdAt: '2024-06-15',
|
||||
lastUpdated: '2024-11-15',
|
||||
dpoApproval: true,
|
||||
phases: {
|
||||
description: true,
|
||||
necessity: true,
|
||||
risks: true,
|
||||
measures: true,
|
||||
consultation: true
|
||||
}
|
||||
},
|
||||
{
|
||||
id: 'biometric_voice',
|
||||
name: 'Voice-Service Spracherkennung',
|
||||
description: 'Sprachbasierte Interaktion mit potentieller Stimmerkennung',
|
||||
status: 'draft',
|
||||
riskLevel: 'high',
|
||||
createdAt: '2024-11-01',
|
||||
lastUpdated: '2024-11-01',
|
||||
dpoApproval: false,
|
||||
phases: {
|
||||
description: true,
|
||||
necessity: false,
|
||||
risks: false,
|
||||
measures: false,
|
||||
consultation: false
|
||||
}
|
||||
},
|
||||
]
|
||||
|
||||
const riskAssessments: RiskAssessment[] = [
|
||||
{
|
||||
id: 'r1',
|
||||
category: 'Vertraulichkeit',
|
||||
risk: 'Unbefugter Zugriff auf Schuelerdaten durch Drittanbieter-KI',
|
||||
likelihood: 'unlikely',
|
||||
impact: 'major',
|
||||
riskScore: 12,
|
||||
mitigations: [
|
||||
'Lokale Verarbeitung mit Ollama priorisieren',
|
||||
'Anonymisierung vor Cloud-Verarbeitung',
|
||||
'Standardvertragsklauseln mit OpenAI'
|
||||
],
|
||||
residualRisk: 'tolerable'
|
||||
},
|
||||
{
|
||||
id: 'r2',
|
||||
category: 'Integritaet',
|
||||
risk: 'Fehlerhafte KI-Bewertungen fuehren zu falschen Noten',
|
||||
likelihood: 'possible',
|
||||
impact: 'moderate',
|
||||
riskScore: 9,
|
||||
mitigations: [
|
||||
'Menschliche Ueberpruefung aller KI-Bewertungen',
|
||||
'Transparente Darstellung als "Vorschlag"',
|
||||
'Feedback-Mechanismus fuer Korrekturen'
|
||||
],
|
||||
residualRisk: 'acceptable'
|
||||
},
|
||||
{
|
||||
id: 'r3',
|
||||
category: 'Verfuegbarkeit',
|
||||
risk: 'Systemausfall verhindert Zugriff auf Lernmaterialien',
|
||||
likelihood: 'rare',
|
||||
impact: 'minor',
|
||||
riskScore: 2,
|
||||
mitigations: [
|
||||
'Offline-Faehigkeit der App',
|
||||
'Redundante Datenhaltung',
|
||||
'Automatische Backups'
|
||||
],
|
||||
residualRisk: 'acceptable'
|
||||
},
|
||||
{
|
||||
id: 'r4',
|
||||
category: 'Rechte der Betroffenen',
|
||||
risk: 'Automatisierte Entscheidungen ohne menschliche Intervention',
|
||||
likelihood: 'possible',
|
||||
impact: 'major',
|
||||
riskScore: 12,
|
||||
mitigations: [
|
||||
'KI nur als Unterstuetzung, finale Entscheidung beim Lehrer',
|
||||
'Recht auf menschliche Ueberpruefung dokumentiert',
|
||||
'Transparente Information ueber KI-Einsatz'
|
||||
],
|
||||
residualRisk: 'tolerable'
|
||||
},
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Abgeschlossen</span>
|
||||
case 'in_progress':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">In Bearbeitung</span>
|
||||
case 'draft':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">Entwurf</span>
|
||||
case 'review_needed':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Pruefung erforderlich</span>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getRiskBadge = (level: string) => {
|
||||
switch (level) {
|
||||
case 'critical':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Kritisch</span>
|
||||
case 'high':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">Hoch</span>
|
||||
case 'medium':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Mittel</span>
|
||||
case 'low':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Niedrig</span>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const getResidualRiskBadge = (risk: string) => {
|
||||
switch (risk) {
|
||||
case 'acceptable':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Akzeptabel</span>
|
||||
case 'tolerable':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Tolerierbar</span>
|
||||
case 'unacceptable':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Nicht akzeptabel</span>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const calculatePhaseProgress = (phases: DSFAProject['phases']) => {
|
||||
const total = Object.keys(phases).length
|
||||
const completed = Object.values(phases).filter(Boolean).length
|
||||
return Math.round((completed / total) * 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="Datenschutz-Folgenabschaetzung (DSFA)"
|
||||
purpose="Systematische Risikoanalyse fuer Verarbeitungen mit hohem Risiko gemaess Art. 35 DSGVO. Dokumentiert Risiken, Massnahmen und DSB-Freigaben."
|
||||
audience={['DSB', 'Projektleiter', 'Entwickler', 'Geschaeftsfuehrung']}
|
||||
gdprArticles={['Art. 35 (Datenschutz-Folgenabschaetzung)', 'Art. 36 (Vorherige Konsultation)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'VVT', href: '/compliance/vvt', description: 'Verarbeitungsverzeichnis' },
|
||||
{ name: 'TOMs', href: '/compliance/tom', description: 'Technische Massnahmen' },
|
||||
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management-System' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6">
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'projects', label: 'DSFA-Projekte' },
|
||||
{ id: 'methodology', label: 'Methodik' },
|
||||
{ id: 'templates', label: 'Vorlagen' },
|
||||
].map(tab => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="space-y-6">
|
||||
{/* Statistics */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-slate-900">{dsfaProjects.length}</div>
|
||||
<div className="text-sm text-slate-500">DSFA-Projekte</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-green-600">
|
||||
{dsfaProjects.filter(p => p.status === 'completed').length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Abgeschlossen</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-blue-600">
|
||||
{dsfaProjects.filter(p => p.status === 'in_progress').length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">In Bearbeitung</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-orange-600">
|
||||
{dsfaProjects.filter(p => p.riskLevel === 'high' || p.riskLevel === 'critical').length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Hohes Risiko</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* When is DSFA required */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Wann ist eine DSFA erforderlich?</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-slate-700">Art. 35 Abs. 3 - Pflichtfaelle:</h3>
|
||||
<ul className="space-y-2 text-sm text-slate-600">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">●</span>
|
||||
Systematische Bewertung persoenlicher Aspekte (Profiling)
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">●</span>
|
||||
Umfangreiche Verarbeitung besonderer Kategorien (Art. 9)
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-red-500 mt-0.5">●</span>
|
||||
Systematische Ueberwachung oeffentlicher Bereiche
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<h3 className="font-medium text-slate-700">Zusaetzliche Kriterien (DSK-Liste):</h3>
|
||||
<ul className="space-y-2 text-sm text-slate-600">
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-orange-500 mt-0.5">●</span>
|
||||
Verarbeitung von Daten Minderjaehriger
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-orange-500 mt-0.5">●</span>
|
||||
Einsatz neuer Technologien (z.B. KI)
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-orange-500 mt-0.5">●</span>
|
||||
Zusammenfuehrung von Datensaetzen
|
||||
</li>
|
||||
<li className="flex items-start gap-2">
|
||||
<span className="text-orange-500 mt-0.5">●</span>
|
||||
Automatisierte Entscheidungsfindung
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Risk Matrix */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Risiko-Matrix (KI-Verarbeitung)</h2>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-500">Kategorie</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-500">Risiko</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-500">Score</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-500">Massnahmen</th>
|
||||
<th className="text-left py-3 px-4 font-medium text-slate-500">Restrisiko</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{riskAssessments.map(risk => (
|
||||
<tr key={risk.id} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="py-3 px-4 font-medium text-slate-900">{risk.category}</td>
|
||||
<td className="py-3 px-4 text-slate-600">{risk.risk}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
risk.riskScore >= 12 ? 'bg-red-100 text-red-800' :
|
||||
risk.riskScore >= 6 ? 'bg-yellow-100 text-yellow-800' :
|
||||
'bg-green-100 text-green-800'
|
||||
}`}>
|
||||
{risk.riskScore}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<ul className="text-xs text-slate-600 space-y-1">
|
||||
{risk.mitigations.slice(0, 2).map((m, i) => (
|
||||
<li key={i}>• {m}</li>
|
||||
))}
|
||||
{risk.mitigations.length > 2 && (
|
||||
<li className="text-slate-400">+{risk.mitigations.length - 2} weitere</li>
|
||||
)}
|
||||
</ul>
|
||||
</td>
|
||||
<td className="py-3 px-4">{getResidualRiskBadge(risk.residualRisk)}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Projects Tab */}
|
||||
{activeTab === 'projects' && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="font-semibold text-slate-900">DSFA-Projekte</h2>
|
||||
<p className="text-sm text-slate-500">{dsfaProjects.length} dokumentierte Folgenabschaetzungen</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700">
|
||||
+ Neue DSFA
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{dsfaProjects.map(project => (
|
||||
<div key={project.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedProject(expandedProject === project.id ? null : project.id)}
|
||||
className="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-slate-900">{project.name}</h3>
|
||||
{getStatusBadge(project.status)}
|
||||
{getRiskBadge(project.riskLevel)}
|
||||
{project.dpoApproval && (
|
||||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">
|
||||
DSB-Freigabe
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-1">{project.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-right text-sm text-slate-500">
|
||||
<div>{calculatePhaseProgress(project.phases)}% abgeschlossen</div>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-5 h-5 text-slate-400 transition-transform ${expandedProject === project.id ? '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>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expandedProject === project.id && (
|
||||
<div className="px-6 pb-6 border-t border-slate-100">
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Phases */}
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-3">DSFA-Phasen</h4>
|
||||
<div className="space-y-2">
|
||||
{[
|
||||
{ key: 'description', label: 'Beschreibung der Verarbeitung' },
|
||||
{ key: 'necessity', label: 'Notwendigkeit & Verhaeltnismaessigkeit' },
|
||||
{ key: 'risks', label: 'Risikobewertung' },
|
||||
{ key: 'measures', label: 'Abhilfemassnahmen' },
|
||||
{ key: 'consultation', label: 'DSB-Konsultation' },
|
||||
].map(phase => (
|
||||
<div key={phase.key} className="flex items-center gap-3">
|
||||
<div className={`w-5 h-5 rounded-full flex items-center justify-center ${
|
||||
project.phases[phase.key as keyof typeof project.phases]
|
||||
? 'bg-green-100 text-green-600'
|
||||
: 'bg-slate-100 text-slate-400'
|
||||
}`}>
|
||||
{project.phases[phase.key as keyof typeof project.phases] ? (
|
||||
<svg className="w-3 h-3" fill="currentColor" viewBox="0 0 20 20">
|
||||
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
|
||||
</svg>
|
||||
) : (
|
||||
<span className="w-2 h-2 rounded-full bg-slate-300" />
|
||||
)}
|
||||
</div>
|
||||
<span className={project.phases[phase.key as keyof typeof project.phases] ? 'text-slate-900' : 'text-slate-500'}>
|
||||
{phase.label}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Meta Info */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Erstellt</h4>
|
||||
<p className="text-slate-700">{project.createdAt}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Letzte Aktualisierung</h4>
|
||||
<p className="text-slate-700">{project.lastUpdated}</p>
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">DSB-Freigabe</h4>
|
||||
<p className={project.dpoApproval ? 'text-green-600 font-medium' : 'text-yellow-600'}>
|
||||
{project.dpoApproval ? 'Erteilt' : 'Ausstehend'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-slate-100 flex gap-2">
|
||||
<button className="px-3 py-1.5 text-sm text-purple-600 hover:text-purple-700 font-medium">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-700">
|
||||
PDF exportieren
|
||||
</button>
|
||||
{!project.dpoApproval && (
|
||||
<button className="px-3 py-1.5 text-sm text-green-600 hover:text-green-700">
|
||||
Zur DSB-Freigabe einreichen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Methodology Tab */}
|
||||
{activeTab === 'methodology' && (
|
||||
<div className="space-y-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">DSFA-Prozess nach Art. 35 DSGVO</h2>
|
||||
|
||||
<div className="space-y-6">
|
||||
{[
|
||||
{
|
||||
step: 1,
|
||||
title: 'Schwellwertanalyse',
|
||||
description: 'Pruefung ob eine DSFA erforderlich ist anhand der Kriterien aus Art. 35 Abs. 3 und der DSK-Positivliste.',
|
||||
details: [
|
||||
'Verarbeitung besonderer Kategorien (Art. 9)?',
|
||||
'Systematisches Profiling?',
|
||||
'Neue Technologien im Einsatz?',
|
||||
'Daten von Minderjaehrigen?'
|
||||
]
|
||||
},
|
||||
{
|
||||
step: 2,
|
||||
title: 'Beschreibung der Verarbeitung',
|
||||
description: 'Systematische Beschreibung der geplanten Verarbeitungsvorgaenge und Zwecke.',
|
||||
details: [
|
||||
'Art, Umfang, Umstaende der Verarbeitung',
|
||||
'Zweck der Verarbeitung',
|
||||
'Betroffene Personengruppen',
|
||||
'Verantwortlichkeiten'
|
||||
]
|
||||
},
|
||||
{
|
||||
step: 3,
|
||||
title: 'Notwendigkeit & Verhaeltnismaessigkeit',
|
||||
description: 'Bewertung ob die Verarbeitung notwendig und verhaeltnismaessig ist.',
|
||||
details: [
|
||||
'Rechtsgrundlage vorhanden?',
|
||||
'Zweckbindung eingehalten?',
|
||||
'Datenminimierung beachtet?',
|
||||
'Speicherbegrenzung definiert?'
|
||||
]
|
||||
},
|
||||
{
|
||||
step: 4,
|
||||
title: 'Risikobewertung',
|
||||
description: 'Systematische Bewertung der Risiken fuer Rechte und Freiheiten der Betroffenen.',
|
||||
details: [
|
||||
'Risiken identifizieren',
|
||||
'Eintrittswahrscheinlichkeit bewerten',
|
||||
'Schwere der Auswirkungen bewerten',
|
||||
'Risiko-Score berechnen'
|
||||
]
|
||||
},
|
||||
{
|
||||
step: 5,
|
||||
title: 'Abhilfemassnahmen',
|
||||
description: 'Definition von Massnahmen zur Eindaemmung der identifizierten Risiken.',
|
||||
details: [
|
||||
'Technische Massnahmen (TOMs)',
|
||||
'Organisatorische Massnahmen',
|
||||
'Restrisiko-Bewertung',
|
||||
'Implementierungsplan'
|
||||
]
|
||||
},
|
||||
{
|
||||
step: 6,
|
||||
title: 'DSB-Konsultation',
|
||||
description: 'Einholung der Stellungnahme des Datenschutzbeauftragten.',
|
||||
details: [
|
||||
'DSFA dem DSB vorlegen',
|
||||
'Stellungnahme dokumentieren',
|
||||
'Ggf. Anpassungen vornehmen',
|
||||
'Freigabe erteilen'
|
||||
]
|
||||
},
|
||||
{
|
||||
step: 7,
|
||||
title: 'Vorherige Konsultation (Art. 36)',
|
||||
description: 'Bei verbleibendem hohen Risiko: Konsultation der Aufsichtsbehoerde.',
|
||||
details: [
|
||||
'Nur bei hohem Restrisiko erforderlich',
|
||||
'Aufsichtsbehoerde hat 8 Wochen zur Pruefung',
|
||||
'Dokumentation der Konsultation',
|
||||
'Umsetzung der Auflagen'
|
||||
]
|
||||
}
|
||||
].map(item => (
|
||||
<div key={item.step} className="flex gap-4">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-purple-100 text-purple-700 flex items-center justify-center font-bold">
|
||||
{item.step}
|
||||
</div>
|
||||
<div className="flex-grow">
|
||||
<h3 className="font-semibold text-slate-900">{item.title}</h3>
|
||||
<p className="text-sm text-slate-600 mt-1">{item.description}</p>
|
||||
<ul className="mt-2 grid grid-cols-2 gap-x-4 gap-y-1 text-xs text-slate-500">
|
||||
{item.details.map((detail, idx) => (
|
||||
<li key={idx} className="flex items-center gap-1">
|
||||
<span className="text-purple-400">→</span> {detail}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Templates Tab */}
|
||||
{activeTab === 'templates' && (
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">DSFA-Vorlagen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||
{[
|
||||
{
|
||||
name: 'Standard DSFA-Vorlage',
|
||||
description: 'Vollstaendige Vorlage nach Art. 35 DSGVO',
|
||||
format: 'DOCX',
|
||||
size: '45 KB'
|
||||
},
|
||||
{
|
||||
name: 'KI-Verarbeitung Template',
|
||||
description: 'Spezialvorlage fuer KI/ML-Anwendungen',
|
||||
format: 'DOCX',
|
||||
size: '52 KB'
|
||||
},
|
||||
{
|
||||
name: 'Risikobewertungs-Matrix',
|
||||
description: 'Excel-Vorlage fuer systematische Risikobewertung',
|
||||
format: 'XLSX',
|
||||
size: '28 KB'
|
||||
},
|
||||
{
|
||||
name: 'Schwellwert-Checkliste',
|
||||
description: 'Checkliste zur Pruefung ob DSFA erforderlich',
|
||||
format: 'PDF',
|
||||
size: '120 KB'
|
||||
},
|
||||
{
|
||||
name: 'DSB-Konsultationsformular',
|
||||
description: 'Formular zur internen DSB-Freigabe',
|
||||
format: 'DOCX',
|
||||
size: '32 KB'
|
||||
},
|
||||
{
|
||||
name: 'Aufsichtsbehoerden-Vorlage',
|
||||
description: 'Vorlage fuer Art. 36 Konsultation',
|
||||
format: 'DOCX',
|
||||
size: '38 KB'
|
||||
}
|
||||
].map(template => (
|
||||
<div key={template.name} className="p-4 border border-slate-200 rounded-lg hover:border-purple-300 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="font-medium text-slate-900">{template.name}</h3>
|
||||
<p className="text-sm text-slate-500 mt-1">{template.description}</p>
|
||||
</div>
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs font-medium">
|
||||
{template.format}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-3 flex items-center justify-between">
|
||||
<span className="text-xs text-slate-400">{template.size}</span>
|
||||
<button className="text-sm text-purple-600 hover:text-purple-700 font-medium">
|
||||
Herunterladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Info */}
|
||||
<div className="mt-6 bg-yellow-50 border border-yellow-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-yellow-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-semibold text-yellow-900">Wichtiger Hinweis</h4>
|
||||
<p className="text-sm text-yellow-800 mt-1">
|
||||
Eine DSFA ist <strong>vor</strong> Beginn der Verarbeitung durchzufuehren. Bei wesentlichen Aenderungen
|
||||
an bestehenden Verarbeitungen muss die DSFA aktualisiert werden. Die Dokumentation muss
|
||||
der Aufsichtsbehoerde auf Anfrage vorgelegt werden koennen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+67
-47
@@ -1,16 +1,13 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* DSMS Page (SDK Version - Zusatzmodul)
|
||||
* DSMS (Data Protection Management System) Admin Page
|
||||
*
|
||||
* Data Protection Management System overview with:
|
||||
* - DSGVO Compliance Score
|
||||
* - Quick access to compliance modules (SDK paths)
|
||||
* - 6 Module cards with status
|
||||
* - GDPR Rights overview
|
||||
* Central hub for data protection compliance management
|
||||
*/
|
||||
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface ComplianceModule {
|
||||
id: string
|
||||
@@ -32,7 +29,7 @@ export default function DSMSPage() {
|
||||
title: 'Rechtliche Dokumente',
|
||||
description: 'AGB, Datenschutzerklaerung, Cookie-Richtlinie',
|
||||
status: 'active',
|
||||
href: '/sdk/consent-management',
|
||||
href: '/compliance/consent',
|
||||
items: [
|
||||
{ name: 'AGB', status: 'complete', lastUpdated: '2024-12-01' },
|
||||
{ name: 'Datenschutzerklaerung', status: 'complete', lastUpdated: '2024-12-01' },
|
||||
@@ -45,7 +42,7 @@ export default function DSMSPage() {
|
||||
title: 'Betroffenenanfragen (DSR)',
|
||||
description: 'Art. 15-21 DSGVO Anfragen-Management',
|
||||
status: 'active',
|
||||
href: '/sdk/dsr',
|
||||
href: '/compliance/dsr',
|
||||
items: [
|
||||
{ name: 'Auskunftsprozess (Art. 15)', status: 'complete' },
|
||||
{ name: 'Berichtigung (Art. 16)', status: 'complete' },
|
||||
@@ -58,7 +55,7 @@ export default function DSMSPage() {
|
||||
title: 'Einwilligungsverwaltung',
|
||||
description: 'Consent-Tracking und -Nachweis',
|
||||
status: 'active',
|
||||
href: '/sdk/consent',
|
||||
href: '/compliance/consent',
|
||||
items: [
|
||||
{ name: 'Consent-Datenbank', status: 'complete' },
|
||||
{ name: 'Widerrufsprozess', status: 'complete' },
|
||||
@@ -71,7 +68,7 @@ export default function DSMSPage() {
|
||||
title: 'Technische & Organisatorische Massnahmen',
|
||||
description: 'Art. 32 DSGVO Sicherheitsmassnahmen',
|
||||
status: 'active',
|
||||
href: '/sdk/tom',
|
||||
href: '/compliance/tom',
|
||||
items: [
|
||||
{ name: 'Verschluesselung (TLS/Ruhe)', status: 'complete' },
|
||||
{ name: 'Zugriffskontrolle', status: 'complete' },
|
||||
@@ -84,7 +81,7 @@ export default function DSMSPage() {
|
||||
title: 'Verarbeitungsverzeichnis',
|
||||
description: 'Art. 30 DSGVO Dokumentation',
|
||||
status: 'active',
|
||||
href: '/sdk/vvt',
|
||||
href: '/compliance/vvt',
|
||||
items: [
|
||||
{ name: 'Verarbeitungstaetigkeiten', status: 'complete' },
|
||||
{ name: 'Rechtsgrundlagen', status: 'complete' },
|
||||
@@ -97,7 +94,7 @@ export default function DSMSPage() {
|
||||
title: 'Datenschutz-Folgenabschaetzung',
|
||||
description: 'Art. 35 DSGVO Risikoanalyse',
|
||||
status: 'active',
|
||||
href: '/sdk/dsfa',
|
||||
href: '/compliance/dsfa',
|
||||
items: [
|
||||
{ name: 'KI-Verarbeitung', status: 'in_progress' },
|
||||
{ name: 'Profiling-Risiken', status: 'complete' },
|
||||
@@ -106,6 +103,7 @@ export default function DSMSPage() {
|
||||
},
|
||||
]
|
||||
|
||||
// Get status badge
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
@@ -121,6 +119,7 @@ export default function DSMSPage() {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate overall compliance score
|
||||
const calculateScore = () => {
|
||||
let complete = 0
|
||||
let total = 0
|
||||
@@ -136,17 +135,36 @@ export default function DSMSPage() {
|
||||
const complianceScore = calculateScore()
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Title Card (Zusatzmodul - no StepHeader) */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h1 className="text-2xl font-bold text-slate-900">Datenschutz-Management-System (DSMS)</h1>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Zentrale Uebersicht aller Datenschutz-Massnahmen und deren Status. Verfolgen Sie den Compliance-Fortschritt und identifizieren Sie offene Aufgaben.
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Datenschutz-Management-System (DSMS)"
|
||||
purpose="Zentrale Uebersicht aller Datenschutz-Massnahmen und deren Status. Hier verfolgen Sie den Compliance-Fortschritt und identifizieren offene Aufgaben."
|
||||
audience={['DSB', 'Compliance Officer', 'Geschaeftsfuehrung']}
|
||||
gdprArticles={[
|
||||
'Art. 5 (Grundsaetze)',
|
||||
'Art. 24 (Verantwortung)',
|
||||
'Art. 30 (Verarbeitungsverzeichnis)',
|
||||
'Art. 32 (Sicherheit)',
|
||||
'Art. 35 (DSFA)',
|
||||
]}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Consent', href: '/compliance/consent', description: 'Dokumente und Versionen' },
|
||||
{ name: 'DSR', href: '/compliance/dsr', description: 'Betroffenenanfragen' },
|
||||
{ name: 'TOMs', href: '/compliance/tom', description: 'Technische Massnahmen' },
|
||||
{ name: 'VVT', href: '/compliance/vvt', description: 'Verarbeitungsverzeichnis' },
|
||||
{ name: 'DSFA', href: '/compliance/dsfa', description: 'Datenschutz-Folgenabschaetzung' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Compliance Score */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">DSGVO-Compliance Score</h2>
|
||||
@@ -168,9 +186,9 @@ export default function DSMSPage() {
|
||||
</div>
|
||||
|
||||
{/* Quick Actions */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<Link
|
||||
href="/sdk/dsr"
|
||||
href="/compliance/dsr"
|
||||
className="bg-white rounded-xl border border-slate-200 p-4 hover:border-purple-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -187,7 +205,7 @@ export default function DSMSPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/consent"
|
||||
href="/compliance/consent"
|
||||
className="bg-white rounded-xl border border-slate-200 p-4 hover:border-purple-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -204,7 +222,7 @@ export default function DSMSPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/einwilligungen"
|
||||
href="/compliance/einwilligungen"
|
||||
className="bg-white rounded-xl border border-slate-200 p-4 hover:border-purple-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -221,7 +239,7 @@ export default function DSMSPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/loeschfristen"
|
||||
href="/compliance/loeschfristen"
|
||||
className="bg-white rounded-xl border border-slate-200 p-4 hover:border-purple-300 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
@@ -239,30 +257,32 @@ export default function DSMSPage() {
|
||||
</div>
|
||||
|
||||
{/* Audit Report Quick Action */}
|
||||
<Link
|
||||
href="/sdk/audit-report"
|
||||
className="block bg-gradient-to-r from-purple-500 to-indigo-600 rounded-xl p-6 text-white hover:from-purple-600 hover:to-indigo-700 transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Audit Report erstellen</h3>
|
||||
<p className="text-sm text-white/80">PDF-Berichte fuer Auditoren und Aufsichtsbehoerden generieren</p>
|
||||
<div className="mb-6">
|
||||
<Link
|
||||
href="/compliance/audit-report"
|
||||
className="block bg-gradient-to-r from-purple-500 to-indigo-600 rounded-xl p-6 text-white hover:from-purple-600 hover:to-indigo-700 transition-all"
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="w-12 h-12 rounded-xl bg-white/20 flex items-center justify-center">
|
||||
<svg className="w-6 h-6" 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>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">Audit Report erstellen</h3>
|
||||
<p className="text-sm text-white/80">PDF-Berichte fuer Auditoren und Aufsichtsbehoerden generieren</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<svg className="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
</div>
|
||||
</Link>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Compliance Modules */}
|
||||
<h2 className="text-lg font-semibold text-slate-900">Compliance-Module</h2>
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-4">Compliance-Module</h2>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{modules.map((module) => (
|
||||
<div key={module.id} className="bg-white rounded-xl border border-slate-200">
|
||||
@@ -315,7 +335,7 @@ export default function DSMSPage() {
|
||||
</div>
|
||||
|
||||
{/* GDPR Rights Overview */}
|
||||
<div className="bg-purple-50 border border-purple-200 rounded-xl p-6">
|
||||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-purple-900 mb-4">DSGVO Betroffenenrechte (Art. 12-22)</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
@@ -6,8 +6,8 @@
|
||||
* GDPR Article 15-21 Request Management
|
||||
*/
|
||||
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface DSRRequest {
|
||||
id: string
|
||||
@@ -31,7 +31,7 @@ interface DSRStats {
|
||||
overdue: number
|
||||
}
|
||||
|
||||
export default function DSRManagementPage() {
|
||||
export default function DSRPage() {
|
||||
const [adminToken, setAdminToken] = useState('')
|
||||
const [requests, setRequests] = useState<DSRRequest[]>([])
|
||||
const [stats, setStats] = useState<DSRStats | null>(null)
|
||||
@@ -72,7 +72,7 @@ export default function DSRManagementPage() {
|
||||
|
||||
if (!response.ok) {
|
||||
if (response.status === 401) {
|
||||
throw new Error('Nicht autorisiert - Token ungültig')
|
||||
throw new Error('Nicht autorisiert - Token ungueltig')
|
||||
}
|
||||
throw new Error(`HTTP ${response.status}`)
|
||||
}
|
||||
@@ -140,9 +140,9 @@ export default function DSRManagementPage() {
|
||||
const labels: Record<string, string> = {
|
||||
'access': 'Auskunft (Art. 15)',
|
||||
'rectification': 'Berichtigung (Art. 16)',
|
||||
'erasure': 'Löschung (Art. 17)',
|
||||
'restriction': 'Einschränkung (Art. 18)',
|
||||
'portability': 'Datenübertragbarkeit (Art. 20)',
|
||||
'erasure': 'Loeschung (Art. 17)',
|
||||
'restriction': 'Einschraenkung (Art. 18)',
|
||||
'portability': 'Datenuebertragbarkeit (Art. 20)',
|
||||
'objection': 'Widerspruch (Art. 21)',
|
||||
}
|
||||
return labels[type] || type
|
||||
@@ -172,7 +172,33 @@ export default function DSRManagementPage() {
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout title="Datenschutzanfragen" description="DSGVO Art. 15-21 Anfragen verwalten">
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Datenschutzanfragen (DSR)"
|
||||
purpose="Verwalten Sie alle Betroffenenanfragen nach DSGVO Art. 15-21. Hier bearbeiten Sie Auskunfts-, Loesch- und Berichtigungsanfragen mit automatischer Fristueberwachung."
|
||||
audience={['DSB', 'Compliance Officer', 'Support']}
|
||||
gdprArticles={[
|
||||
'Art. 15 (Auskunftsrecht)',
|
||||
'Art. 16 (Berichtigung)',
|
||||
'Art. 17 (Loeschung)',
|
||||
'Art. 18 (Einschraenkung)',
|
||||
'Art. 20 (Datenuebertragbarkeit)',
|
||||
'Art. 21 (Widerspruch)',
|
||||
]}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Consent Verwaltung', href: '/compliance/consent', description: 'Dokumente und Zustimmungen' },
|
||||
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management-System' },
|
||||
{ name: 'Audit', href: '/compliance/audit', description: 'Audit-Dokumentation' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Token Input */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
@@ -184,12 +210,12 @@ export default function DSRManagementPage() {
|
||||
value={adminToken}
|
||||
onChange={(e) => saveToken(e.target.value)}
|
||||
placeholder="JWT Token eingeben..."
|
||||
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-primary-500"
|
||||
className="flex-1 px-3 py-2 border border-slate-300 rounded-lg text-sm focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<button
|
||||
onClick={fetchRequests}
|
||||
disabled={!adminToken || loading}
|
||||
className="px-4 py-2 bg-slate-900 text-white rounded-lg text-sm hover:bg-slate-800 disabled:opacity-50"
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{loading ? 'Laden...' : 'Laden'}
|
||||
</button>
|
||||
@@ -226,7 +252,7 @@ export default function DSRManagementPage() {
|
||||
<div className={`text-2xl font-bold ${stats.overdue > 0 ? 'text-red-600' : 'text-slate-400'}`}>
|
||||
{stats.overdue}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Überfällig</div>
|
||||
<div className="text-sm text-slate-500">Ueberfaellig</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -238,14 +264,14 @@ export default function DSRManagementPage() {
|
||||
{ value: 'pending', label: 'Offen' },
|
||||
{ value: 'in_progress', label: 'In Bearbeitung' },
|
||||
{ value: 'completed', label: 'Abgeschlossen' },
|
||||
{ value: 'overdue', label: 'Überfällig' },
|
||||
{ value: 'overdue', label: 'Ueberfaellig' },
|
||||
].map((tab) => (
|
||||
<button
|
||||
key={tab.value}
|
||||
onClick={() => setFilter(tab.value)}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium whitespace-nowrap transition-colors ${
|
||||
filter === tab.value
|
||||
? 'bg-slate-900 text-white'
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-white text-slate-700 border border-slate-200 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
@@ -263,7 +289,7 @@ export default function DSRManagementPage() {
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Typ</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Anfragesteller</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Status</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Priorität</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Prioritaet</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Frist</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Aktionen</th>
|
||||
</tr>
|
||||
@@ -302,7 +328,7 @@ export default function DSRManagementPage() {
|
||||
<td className="px-4 py-3">
|
||||
<button
|
||||
onClick={() => setSelectedRequest(request)}
|
||||
className="text-primary-600 hover:text-primary-700 text-sm font-medium"
|
||||
className="text-purple-600 hover:text-purple-700 text-sm font-medium"
|
||||
>
|
||||
Details
|
||||
</button>
|
||||
@@ -374,11 +400,11 @@ export default function DSRManagementPage() {
|
||||
)}
|
||||
|
||||
<div className="flex gap-2 pt-4">
|
||||
<button className="flex-1 px-4 py-2 bg-primary-600 text-white rounded-lg text-sm font-medium hover:bg-primary-700">
|
||||
<button className="flex-1 px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="px-4 py-2 border border-slate-300 text-slate-700 rounded-lg text-sm font-medium hover:bg-slate-50">
|
||||
Abschließen
|
||||
Abschliessen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -387,17 +413,17 @@ export default function DSRManagementPage() {
|
||||
)}
|
||||
|
||||
{/* Info Box */}
|
||||
<div className="mt-6 bg-blue-50 border border-blue-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-blue-900 mb-2">DSGVO-Fristen</h4>
|
||||
<ul className="text-sm text-blue-800 space-y-1">
|
||||
<li>Art. 15 (Auskunft): 1 Monat, verlängerbar auf 3 Monate</li>
|
||||
<li>Art. 16 (Berichtigung): Unverzüglich</li>
|
||||
<li>Art. 17 (Löschung): Unverzüglich</li>
|
||||
<li>Art. 18 (Einschränkung): Unverzüglich</li>
|
||||
<li>Art. 20 (Datenübertragbarkeit): 1 Monat</li>
|
||||
<li>Art. 21 (Widerspruch): Unverzüglich</li>
|
||||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<h4 className="font-semibold text-purple-900 mb-2">DSGVO-Fristen</h4>
|
||||
<ul className="text-sm text-purple-800 space-y-1">
|
||||
<li>Art. 15 (Auskunft): 1 Monat, verlaengerbar auf 3 Monate</li>
|
||||
<li>Art. 16 (Berichtigung): Unverzueglich</li>
|
||||
<li>Art. 17 (Loeschung): Unverzueglich</li>
|
||||
<li>Art. 18 (Einschraenkung): Unverzueglich</li>
|
||||
<li>Art. 20 (Datenuebertragbarkeit): 1 Monat</li>
|
||||
<li>Art. 21 (Widerspruch): Unverzueglich</li>
|
||||
</ul>
|
||||
</div>
|
||||
</AdminLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,498 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Einwilligungsverwaltung - User Consent Management
|
||||
*
|
||||
* Zentrale Uebersicht aller Nutzer-Einwilligungen aus:
|
||||
* - Website
|
||||
* - App
|
||||
* - PWA
|
||||
*
|
||||
* Kategorien: Marketing, Statistik, Cookies, Rechtliche Dokumente
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
const API_BASE = '/api/admin/consent'
|
||||
|
||||
type Tab = 'overview' | 'documents' | 'cookies' | 'marketing' | 'audit'
|
||||
|
||||
interface ConsentStats {
|
||||
total_users: number
|
||||
consented_users: number
|
||||
consent_rate: number
|
||||
pending_consents: number
|
||||
}
|
||||
|
||||
interface AuditEntry {
|
||||
id: string
|
||||
user_id: string
|
||||
action: string
|
||||
entity_type: string
|
||||
entity_id: string
|
||||
details: Record<string, unknown>
|
||||
ip_address: string
|
||||
created_at: string
|
||||
}
|
||||
|
||||
interface ConsentSummary {
|
||||
category: string
|
||||
total: number
|
||||
accepted: number
|
||||
declined: number
|
||||
pending: number
|
||||
rate: number
|
||||
}
|
||||
|
||||
export default function EinwilligungenPage() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('overview')
|
||||
const [stats, setStats] = useState<ConsentStats | null>(null)
|
||||
const [auditLog, setAuditLog] = useState<AuditEntry[]>([])
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [authToken, setAuthToken] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
const token = localStorage.getItem('bp_admin_token')
|
||||
if (token) {
|
||||
setAuthToken(token)
|
||||
}
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (activeTab === 'overview') {
|
||||
loadStats()
|
||||
} else if (activeTab === 'audit') {
|
||||
loadAuditLog()
|
||||
}
|
||||
}, [activeTab, authToken])
|
||||
|
||||
async function loadStats() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/stats`, {
|
||||
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setStats(data)
|
||||
} else {
|
||||
setError('Fehler beim Laden der Statistiken')
|
||||
}
|
||||
} catch {
|
||||
setError('Verbindungsfehler')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadAuditLog() {
|
||||
setLoading(true)
|
||||
setError(null)
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/audit-log?limit=50`, {
|
||||
headers: authToken ? { 'Authorization': `Bearer ${authToken}` } : {}
|
||||
})
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setAuditLog(data.entries || [])
|
||||
} else {
|
||||
setError('Fehler beim Laden des Audit-Logs')
|
||||
}
|
||||
} catch {
|
||||
setError('Verbindungsfehler')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
// Mock data for consent summary (in production, this comes from API)
|
||||
const consentSummary: ConsentSummary[] = [
|
||||
{ category: 'AGB', total: 1250, accepted: 1248, declined: 0, pending: 2, rate: 99.8 },
|
||||
{ category: 'Datenschutz', total: 1250, accepted: 1245, declined: 3, pending: 2, rate: 99.6 },
|
||||
{ category: 'Cookies (Notwendig)', total: 1250, accepted: 1250, declined: 0, pending: 0, rate: 100 },
|
||||
{ category: 'Cookies (Analyse)', total: 1250, accepted: 892, declined: 358, pending: 0, rate: 71.4 },
|
||||
{ category: 'Cookies (Marketing)', total: 1250, accepted: 456, declined: 794, pending: 0, rate: 36.5 },
|
||||
{ category: 'Newsletter', total: 1250, accepted: 312, declined: 938, pending: 0, rate: 25.0 },
|
||||
]
|
||||
|
||||
const tabs: { id: Tab; label: string }[] = [
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'documents', label: 'Dokumenten-Consents' },
|
||||
{ id: 'cookies', label: 'Cookie-Consents' },
|
||||
{ id: 'marketing', label: 'Marketing-Consents' },
|
||||
{ id: 'audit', label: 'Audit-Trail' },
|
||||
]
|
||||
|
||||
const getActionLabel = (action: string) => {
|
||||
const labels: Record<string, string> = {
|
||||
'consent_given': 'Zustimmung erteilt',
|
||||
'consent_withdrawn': 'Zustimmung widerrufen',
|
||||
'cookie_consent_updated': 'Cookie-Einstellungen aktualisiert',
|
||||
'data_access': 'Datenzugriff',
|
||||
'data_export_requested': 'Datenexport angefordert',
|
||||
'data_deletion_requested': 'Loeschung angefordert',
|
||||
'account_suspended': 'Account gesperrt',
|
||||
'account_restored': 'Account wiederhergestellt',
|
||||
}
|
||||
return labels[action] || action
|
||||
}
|
||||
|
||||
const getActionColor = (action: string) => {
|
||||
if (action.includes('given') || action.includes('restored')) return 'bg-green-100 text-green-700'
|
||||
if (action.includes('withdrawn') || action.includes('deleted') || action.includes('suspended')) return 'bg-red-100 text-red-700'
|
||||
return 'bg-blue-100 text-blue-700'
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="Einwilligungsverwaltung"
|
||||
purpose="Zentrale Uebersicht aller Nutzer-Einwilligungen. Hier sehen Sie alle Zustimmungen zu rechtlichen Dokumenten, Cookies, Marketing und Statistik - erfasst ueber Website, App und PWA."
|
||||
audience={['DSB', 'Compliance Officer', 'Marketing']}
|
||||
gdprArticles={['Art. 6 (Rechtmaessigkeit)', 'Art. 7 (Einwilligung)', 'Art. 21 (Widerspruch)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)'],
|
||||
databases: ['PostgreSQL (user_consents, cookie_consents)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Consent Dokumente', href: '/compliance/consent', description: 'Rechtliche Dokumente verwalten' },
|
||||
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management' },
|
||||
{ name: 'DSR', href: '/compliance/dsr', description: 'Betroffenenanfragen' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-slate-900">{stats?.total_users || 1250}</div>
|
||||
<div className="text-sm text-slate-500">Registrierte Nutzer</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-green-600">{stats?.consented_users || 1245}</div>
|
||||
<div className="text-sm text-slate-500">Mit Zustimmung</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-yellow-600">{stats?.pending_consents || 5}</div>
|
||||
<div className="text-sm text-slate-500">Ausstehend</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-purple-600">{stats?.consent_rate?.toFixed(1) || 99.6}%</div>
|
||||
<div className="text-sm text-slate-500">Zustimmungsrate</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg w-fit">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-white text-slate-900 shadow-sm'
|
||||
: 'text-slate-600 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 text-red-700 rounded-lg">
|
||||
{error}
|
||||
<button onClick={() => setError(null)} className="ml-4 text-red-500 hover:text-red-700">X</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Content */}
|
||||
<div className="bg-white rounded-xl border border-slate-200">
|
||||
{/* Overview Tab */}
|
||||
{activeTab === 'overview' && (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Consent-Uebersicht nach Kategorie</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
{consentSummary.map((item) => (
|
||||
<div key={item.category} className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<h3 className="font-medium text-slate-900">{item.category}</h3>
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
item.rate >= 90 ? 'bg-green-100 text-green-700' :
|
||||
item.rate >= 50 ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{item.rate}% Zustimmung
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="h-2 bg-slate-100 rounded-full overflow-hidden mb-3">
|
||||
<div
|
||||
className={`h-full ${item.rate >= 90 ? 'bg-green-500' : item.rate >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${item.rate}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-slate-500">Gesamt:</span>
|
||||
<span className="ml-1 font-medium">{item.total}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-green-600">Akzeptiert:</span>
|
||||
<span className="ml-1 font-medium">{item.accepted}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-red-600">Abgelehnt:</span>
|
||||
<span className="ml-1 font-medium">{item.declined}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-yellow-600">Ausstehend:</span>
|
||||
<span className="ml-1 font-medium">{item.pending}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Export Button */}
|
||||
<div className="mt-6 pt-6 border-t border-slate-200">
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
|
||||
Consent-Report exportieren (CSV)
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Documents Tab */}
|
||||
{activeTab === 'documents' && (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Dokumenten-Einwilligungen</h2>
|
||||
<div className="flex gap-2">
|
||||
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
||||
<option value="">Alle Dokumente</option>
|
||||
<option value="terms">AGB</option>
|
||||
<option value="privacy">Datenschutz</option>
|
||||
<option value="cookies">Cookies</option>
|
||||
</select>
|
||||
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
||||
<option value="">Alle Status</option>
|
||||
<option value="active">Aktiv</option>
|
||||
<option value="withdrawn">Widerrufen</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Nutzer-ID</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Dokument</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Version</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Status</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Datum</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Quelle</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{/* Sample data - in production, this comes from API */}
|
||||
{[
|
||||
{ id: 'usr_123', doc: 'AGB', version: 'v2.1.0', status: 'active', date: '2024-12-15', source: 'Website' },
|
||||
{ id: 'usr_124', doc: 'Datenschutz', version: 'v3.0.0', status: 'active', date: '2024-12-15', source: 'App' },
|
||||
{ id: 'usr_125', doc: 'AGB', version: 'v2.1.0', status: 'withdrawn', date: '2024-12-14', source: 'PWA' },
|
||||
].map((consent, idx) => (
|
||||
<tr key={idx} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="py-3 px-4 font-mono text-sm">{consent.id}</td>
|
||||
<td className="py-3 px-4">{consent.doc}</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-500">{consent.version}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${
|
||||
consent.status === 'active' ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{consent.status === 'active' ? 'Aktiv' : 'Widerrufen'}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4 text-sm text-slate-500">{consent.date}</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs">{consent.source}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Cookies Tab */}
|
||||
{activeTab === 'cookies' && (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Cookie-Einwilligungen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{[
|
||||
{ name: 'Notwendige Cookies', key: 'necessary', mandatory: true, rate: 100, description: 'Erforderlich fuer Grundfunktionen' },
|
||||
{ name: 'Funktionale Cookies', key: 'functional', mandatory: false, rate: 82.3, description: 'Verbesserte Nutzererfahrung' },
|
||||
{ name: 'Analyse Cookies', key: 'analytics', mandatory: false, rate: 71.4, description: 'Anonyme Nutzungsstatistiken' },
|
||||
{ name: 'Marketing Cookies', key: 'marketing', mandatory: false, rate: 36.5, description: 'Personalisierte Werbung' },
|
||||
].map((category) => (
|
||||
<div key={category.key} className="border border-slate-200 rounded-xl p-5">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div>
|
||||
<h3 className="font-semibold text-slate-900">{category.name}</h3>
|
||||
<p className="text-sm text-slate-500">{category.description}</p>
|
||||
</div>
|
||||
{category.mandatory && (
|
||||
<span className="px-2 py-1 bg-blue-100 text-blue-700 rounded text-xs font-medium">Pflicht</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-grow h-3 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${category.rate >= 80 ? 'bg-green-500' : category.rate >= 50 ? 'bg-yellow-500' : 'bg-orange-500'}`}
|
||||
style={{ width: `${category.rate}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-lg font-bold text-slate-900">{category.rate}%</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="mt-6 p-4 bg-slate-50 rounded-lg">
|
||||
<h4 className="font-medium text-slate-900 mb-2">Cookie-Banner Einstellungen</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
Das Cookie-Banner wird auf allen Plattformen (Website, App, PWA) einheitlich angezeigt.
|
||||
Nutzer koennen ihre Praeferenzen jederzeit in den Einstellungen aendern.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Marketing Tab */}
|
||||
{activeTab === 'marketing' && (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Marketing-Einwilligungen</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
|
||||
{[
|
||||
{ name: 'E-Mail Newsletter', rate: 25.0, total: 1250, subscribed: 312 },
|
||||
{ name: 'Push-Benachrichtigungen', rate: 45.2, total: 1250, subscribed: 565 },
|
||||
{ name: 'Personalisierte Werbung', rate: 18.5, total: 1250, subscribed: 231 },
|
||||
].map((channel) => (
|
||||
<div key={channel.name} className="bg-white border border-slate-200 rounded-xl p-5">
|
||||
<h3 className="font-semibold text-slate-900 mb-2">{channel.name}</h3>
|
||||
<div className="text-3xl font-bold text-purple-600 mb-1">{channel.rate}%</div>
|
||||
<div className="text-sm text-slate-500">{channel.subscribed} von {channel.total} Nutzern</div>
|
||||
|
||||
<div className="mt-4 h-2 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div className="h-full bg-purple-500" style={{ width: `${channel.rate}%` }} />
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200 rounded-lg p-4">
|
||||
<h4 className="font-medium text-slate-900 mb-3">Opt-Out Anfragen (letzte 30 Tage)</h4>
|
||||
<div className="grid grid-cols-3 gap-4 text-center">
|
||||
<div className="p-3 bg-slate-50 rounded-lg">
|
||||
<div className="text-xl font-bold text-slate-900">23</div>
|
||||
<div className="text-xs text-slate-500">Newsletter</div>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 rounded-lg">
|
||||
<div className="text-xl font-bold text-slate-900">45</div>
|
||||
<div className="text-xs text-slate-500">Push</div>
|
||||
</div>
|
||||
<div className="p-3 bg-slate-50 rounded-lg">
|
||||
<div className="text-xl font-bold text-slate-900">12</div>
|
||||
<div className="text-xs text-slate-500">Werbung</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Audit Tab */}
|
||||
{activeTab === 'audit' && (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Consent Audit-Trail</h2>
|
||||
<div className="flex gap-2">
|
||||
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
||||
<option value="">Alle Aktionen</option>
|
||||
<option value="consent_given">Zustimmung erteilt</option>
|
||||
<option value="consent_withdrawn">Zustimmung widerrufen</option>
|
||||
<option value="cookie_consent_updated">Cookie aktualisiert</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={loadAuditLog}
|
||||
className="px-3 py-2 bg-slate-100 text-slate-700 rounded-lg text-sm hover:bg-slate-200"
|
||||
>
|
||||
Aktualisieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loading ? (
|
||||
<div className="text-center py-12 text-slate-500">Lade Audit-Log...</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{(auditLog.length > 0 ? auditLog : [
|
||||
// Sample data
|
||||
{ id: '1', user_id: 'usr_123', action: 'consent_given', entity_type: 'document', entity_id: 'doc_agb', details: {}, ip_address: '192.168.1.1', created_at: '2024-12-15T10:30:00Z' },
|
||||
{ id: '2', user_id: 'usr_124', action: 'cookie_consent_updated', entity_type: 'cookie', entity_id: 'analytics', details: {}, ip_address: '192.168.1.2', created_at: '2024-12-15T10:25:00Z' },
|
||||
{ id: '3', user_id: 'usr_125', action: 'consent_withdrawn', entity_type: 'document', entity_id: 'doc_newsletter', details: {}, ip_address: '192.168.1.3', created_at: '2024-12-15T10:20:00Z' },
|
||||
]).map((entry) => (
|
||||
<div key={entry.id} className="border border-slate-200 rounded-lg p-4 hover:bg-slate-50">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-center gap-3">
|
||||
<span className={`px-2 py-1 rounded text-xs font-medium ${getActionColor(entry.action)}`}>
|
||||
{getActionLabel(entry.action)}
|
||||
</span>
|
||||
<span className="font-mono text-sm text-slate-600">{entry.user_id}</span>
|
||||
</div>
|
||||
<span className="text-sm text-slate-400">
|
||||
{new Date(entry.created_at).toLocaleString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
<div className="mt-2 text-sm text-slate-500">
|
||||
<span className="text-slate-400">Entity:</span> {entry.entity_type} / {entry.entity_id}
|
||||
<span className="mx-2 text-slate-300">|</span>
|
||||
<span className="text-slate-400">IP:</span> {entry.ip_address}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* GDPR Notice */}
|
||||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-purple-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-purple-900">DSGVO-Hinweis</h4>
|
||||
<p className="text-sm text-purple-800 mt-1">
|
||||
Alle Einwilligungen werden revisionssicher gespeichert und koennen jederzeit nachgewiesen werden.
|
||||
Nutzer koennen ihre Einwilligungen gemaess Art. 7 Abs. 3 DSGVO jederzeit widerrufen.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+148
-87
@@ -13,7 +13,7 @@
|
||||
import { useState, useEffect, useRef, Suspense } from 'react'
|
||||
import { useSearchParams } from 'next/navigation'
|
||||
import Link from 'next/link'
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface Evidence {
|
||||
id: string
|
||||
@@ -50,11 +50,11 @@ const EVIDENCE_TYPES = [
|
||||
{ value: 'manual_upload', label: 'Manueller Upload', icon: 'M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12' },
|
||||
]
|
||||
|
||||
const STATUS_STYLES: Record<string, string> = {
|
||||
valid: 'bg-green-100 text-green-700',
|
||||
expired: 'bg-red-100 text-red-700',
|
||||
pending: 'bg-yellow-100 text-yellow-700',
|
||||
failed: 'bg-red-100 text-red-700',
|
||||
const STATUS_STYLES: Record<string, { bg: string; text: string; label: string }> = {
|
||||
valid: { bg: 'bg-green-100', text: 'text-green-700', label: 'Gueltig' },
|
||||
expired: { bg: 'bg-red-100', text: 'text-red-700', label: 'Abgelaufen' },
|
||||
pending: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Ausstehend' },
|
||||
failed: { bg: 'bg-red-100', text: 'text-red-700', label: 'Fehlgeschlagen' },
|
||||
}
|
||||
|
||||
function EvidencePageContent({ initialControlId }: { initialControlId: string | null }) {
|
||||
@@ -79,8 +79,6 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
const fileInputRef = useRef<HTMLInputElement>(null)
|
||||
const [selectedFile, setSelectedFile] = useState<File | null>(null)
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
useEffect(() => {
|
||||
loadData()
|
||||
}, [filterControlId, filterType])
|
||||
@@ -93,8 +91,8 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
if (filterType) params.append('evidence_type', filterType)
|
||||
|
||||
const [evidenceRes, controlsRes] = await Promise.all([
|
||||
fetch(`${BACKEND_URL}/api/v1/compliance/evidence?${params}`),
|
||||
fetch(`${BACKEND_URL}/api/v1/compliance/controls`),
|
||||
fetch(`/api/admin/compliance/evidence?${params}`),
|
||||
fetch(`/api/admin/compliance/controls`),
|
||||
])
|
||||
|
||||
if (evidenceRes.ok) {
|
||||
@@ -132,7 +130,7 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
params.append('description', newEvidence.description)
|
||||
}
|
||||
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/evidence/upload?${params}`, {
|
||||
const res = await fetch(`/api/admin/compliance/evidence/upload?${params}`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
})
|
||||
@@ -161,7 +159,7 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
|
||||
setUploading(true)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/evidence`, {
|
||||
const res = await fetch(`/api/admin/compliance/evidence`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -213,35 +211,75 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
return control?.control_id || controlUuid
|
||||
}
|
||||
|
||||
// Statistics
|
||||
const stats = {
|
||||
total: evidence.length,
|
||||
valid: evidence.filter(e => e.status === 'valid').length,
|
||||
expired: evidence.filter(e => e.status === 'expired').length,
|
||||
pending: evidence.filter(e => e.status === 'pending').length,
|
||||
automated: evidence.filter(e => e.source === 'ci_pipeline').length,
|
||||
}
|
||||
|
||||
return (
|
||||
<AdminLayout title="Evidence Management" description="Nachweise & Artefakte">
|
||||
<div className="min-h-screen bg-slate-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Evidence Management</h1>
|
||||
<p className="text-slate-600">Nachweise & Artefakte</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/compliance"
|
||||
className="text-sm text-slate-500 hover:text-slate-700 flex items-center gap-1"
|
||||
href="/compliance/hub"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Zurueck
|
||||
Compliance Hub
|
||||
</Link>
|
||||
<div className="flex-1" />
|
||||
<button
|
||||
onClick={() => { resetForm(); setLinkModalOpen(true) }}
|
||||
className="px-4 py-2 border border-primary-600 text-primary-600 rounded-lg hover:bg-primary-50"
|
||||
>
|
||||
Link hinzufuegen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { resetForm(); setUploadModalOpen(true) }}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
>
|
||||
Datei hochladen
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filters */}
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Evidence Management"
|
||||
purpose="Verwalten Sie alle Nachweise und Artefakte, die die Einhaltung von Compliance-Anforderungen belegen. Jeder Control kann mit mehreren Nachweisen verknuepft werden - von automatischen Scan-Reports bis zu manuellen Dokumenten."
|
||||
audience={['CISO', 'DSB', 'Compliance Officer', 'Auditoren']}
|
||||
gdprArticles={['Art. 5(2) (Rechenschaftspflicht)', 'Art. 30 (Verzeichnis von Verarbeitungstaetigkeiten)']}
|
||||
architecture={{
|
||||
services: ['Python Backend (FastAPI)', 'compliance_evidence Modul'],
|
||||
databases: ['PostgreSQL (compliance_evidence Table)', 'MinIO (Datei-Storage)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Control-Katalog verwalten' },
|
||||
{ name: 'Audit Checklist', href: '/compliance/audit-checklist', description: 'Anforderungen pruefen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-5 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<p className="text-sm text-slate-500">Gesamt</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{stats.total}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-green-200">
|
||||
<p className="text-sm text-green-600">Gueltig</p>
|
||||
<p className="text-2xl font-bold text-green-700">{stats.valid}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-red-200">
|
||||
<p className="text-sm text-red-600">Abgelaufen</p>
|
||||
<p className="text-2xl font-bold text-red-700">{stats.expired}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-yellow-200">
|
||||
<p className="text-sm text-yellow-600">Ausstehend</p>
|
||||
<p className="text-2xl font-bold text-yellow-700">{stats.pending}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-blue-200">
|
||||
<p className="text-sm text-blue-600">CI/CD</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{stats.automated}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Actions & Filters */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4 mb-6">
|
||||
<div className="flex flex-wrap items-center gap-4">
|
||||
<select
|
||||
@@ -266,7 +304,20 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
))}
|
||||
</select>
|
||||
|
||||
<span className="text-sm text-slate-500">{evidence.length} Nachweise</span>
|
||||
<div className="flex-1" />
|
||||
|
||||
<button
|
||||
onClick={() => { resetForm(); setLinkModalOpen(true) }}
|
||||
className="px-4 py-2 border border-primary-600 text-primary-600 rounded-lg hover:bg-primary-50"
|
||||
>
|
||||
Link hinzufuegen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => { resetForm(); setUploadModalOpen(true) }}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
>
|
||||
Datei hochladen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -290,58 +341,66 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
</div>
|
||||
) : (
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
{evidence.map((ev) => (
|
||||
<div key={ev.id} className="bg-white rounded-xl shadow-sm border p-4 hover:border-primary-300 transition-colors">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={EVIDENCE_TYPES.find((t) => t.value === ev.evidence_type)?.icon || '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>
|
||||
{evidence.map((ev) => {
|
||||
const statusStyle = STATUS_STYLES[ev.status] || STATUS_STYLES.pending
|
||||
return (
|
||||
<div key={ev.id} className="bg-white rounded-xl shadow-sm border p-4 hover:border-primary-300 transition-colors">
|
||||
<div className="flex items-start justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-10 h-10 bg-slate-100 rounded-lg flex items-center justify-center">
|
||||
<svg className="w-5 h-5 text-slate-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d={EVIDENCE_TYPES.find((t) => t.value === ev.evidence_type)?.icon || '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>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${statusStyle.bg} ${statusStyle.text}`}>
|
||||
{statusStyle.label}
|
||||
</span>
|
||||
</div>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${STATUS_STYLES[ev.status] || 'bg-slate-100 text-slate-700'}`}>
|
||||
{ev.status}
|
||||
</span>
|
||||
<span className="text-xs text-slate-500 font-mono">{getControlTitle(ev.control_id)}</span>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-slate-900 mb-1">{ev.title}</h4>
|
||||
{ev.description && (
|
||||
<p className="text-sm text-slate-500 mb-3 line-clamp-2">{ev.description}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 pt-3 border-t">
|
||||
<span>{EVIDENCE_TYPES.find(t => t.value === ev.evidence_type)?.label || ev.evidence_type}</span>
|
||||
<span>{formatFileSize(ev.file_size_bytes)}</span>
|
||||
</div>
|
||||
|
||||
{ev.artifact_url && (
|
||||
<a
|
||||
href={ev.artifact_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 block text-sm text-primary-600 hover:text-primary-700 truncate"
|
||||
>
|
||||
{ev.artifact_url}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="mt-3 flex items-center justify-between text-xs text-slate-400">
|
||||
<span>Erfasst: {new Date(ev.collected_at).toLocaleDateString('de-DE')}</span>
|
||||
{ev.source === 'ci_pipeline' && (
|
||||
<span className="bg-blue-50 text-blue-600 px-1.5 py-0.5 rounded">CI/CD</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="text-xs text-slate-500 font-mono">{getControlTitle(ev.control_id)}</span>
|
||||
</div>
|
||||
|
||||
<h4 className="font-medium text-slate-900 mb-1">{ev.title}</h4>
|
||||
{ev.description && (
|
||||
<p className="text-sm text-slate-500 mb-3 line-clamp-2">{ev.description}</p>
|
||||
)}
|
||||
|
||||
<div className="flex items-center justify-between text-xs text-slate-500 pt-3 border-t">
|
||||
<span>{ev.evidence_type.replace('_', ' ')}</span>
|
||||
<span>{formatFileSize(ev.file_size_bytes)}</span>
|
||||
</div>
|
||||
|
||||
{ev.artifact_url && (
|
||||
<a
|
||||
href={ev.artifact_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="mt-3 block text-sm text-primary-600 hover:text-primary-700 truncate"
|
||||
>
|
||||
{ev.artifact_url}
|
||||
</a>
|
||||
)}
|
||||
|
||||
<div className="mt-3 text-xs text-slate-400">
|
||||
Erfasst: {new Date(ev.collected_at).toLocaleDateString('de-DE')}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Upload Modal */}
|
||||
{uploadModalOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Datei hochladen</h3>
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-6 border-b">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Datei hochladen</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Control *</label>
|
||||
<select
|
||||
@@ -406,10 +465,11 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<div className="p-6 border-t bg-slate-50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setUploadModalOpen(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
disabled={uploading}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
@@ -427,11 +487,13 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
|
||||
{/* Link Modal */}
|
||||
{linkModalOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Link/Quelle hinzufuegen</h3>
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg">
|
||||
<div className="p-6 border-b">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Link/Quelle hinzufuegen</h3>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Control *</label>
|
||||
<select
|
||||
@@ -479,10 +541,11 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<div className="p-6 border-t bg-slate-50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setLinkModalOpen(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
disabled={uploading}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
@@ -497,7 +560,7 @@ function EvidencePageContent({ initialControlId }: { initialControlId: string |
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -510,11 +573,9 @@ function EvidencePageWithParams() {
|
||||
export default function EvidencePage() {
|
||||
return (
|
||||
<Suspense fallback={
|
||||
<AdminLayout title="Evidence Management" description="Nachweise & Artefakte">
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
</AdminLayout>
|
||||
<div className="min-h-screen bg-slate-50 p-6 flex items-center justify-center">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
}>
|
||||
<EvidencePageWithParams />
|
||||
</Suspense>
|
||||
+46
-22
@@ -1,18 +1,19 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Compliance Hub Page (SDK Version - Zusatzmodul)
|
||||
* Compliance Hub - Central Compliance Management Dashboard
|
||||
*
|
||||
* Central compliance management dashboard with:
|
||||
* Features:
|
||||
* - Compliance Score Overview
|
||||
* - Quick Access to all compliance modules (SDK paths)
|
||||
* - Control-Mappings with statistics
|
||||
* - Audit Findings
|
||||
* - Quick Access to all compliance modules
|
||||
* - 474 Control-Mappings with statistics
|
||||
* - Haupt-/Nebenabweichungen (Major/Minor findings)
|
||||
* - Regulations overview
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// Types
|
||||
interface DashboardData {
|
||||
@@ -39,7 +40,18 @@ interface Regulation {
|
||||
requirement_count: number
|
||||
}
|
||||
|
||||
interface ControlMapping {
|
||||
id: string
|
||||
control_id: string
|
||||
requirement_id: string
|
||||
control_title: string
|
||||
requirement_title: string
|
||||
regulation_code: string
|
||||
mapping_strength: string
|
||||
}
|
||||
|
||||
interface MappingsData {
|
||||
mappings: ControlMapping[]
|
||||
total: number
|
||||
by_regulation: Record<string, number>
|
||||
}
|
||||
@@ -143,13 +155,21 @@ export default function ComplianceHubPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Title Card (Zusatzmodul - no StepHeader) */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h1 className="text-2xl font-bold text-slate-900">Compliance Hub</h1>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Zentrale Verwaltung aller Compliance-Anforderungen nach DSGVO, AI Act, BSI TR-03161 und weiteren Regulierungen.
|
||||
</p>
|
||||
</div>
|
||||
<PagePurpose
|
||||
title="Compliance Hub"
|
||||
purpose="Zentrale Verwaltung aller Compliance-Anforderungen nach DSGVO, AI Act, BSI TR-03161 und weiteren Regulierungen. Hier sehen Sie den aktuellen Compliance-Stand und haben Zugriff auf alle Module."
|
||||
audience={['DSB', 'Compliance Officer', 'Auditor', 'Entwickler']}
|
||||
gdprArticles={['Art. 5 (Grundsaetze)', 'Art. 24 (Verantwortung)', 'Art. 32 (Sicherheit)']}
|
||||
architecture={{
|
||||
services: ['Python Backend', 'PostgreSQL'],
|
||||
databases: ['compliance_*'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Audit Checkliste', href: '/compliance/audit-checklist', description: '476 Anforderungen pruefen' },
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Massnahmen' },
|
||||
{ name: 'Risiken', href: '/compliance/risks', description: 'Risikoregister & Matrix' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Error Banner */}
|
||||
{error && (
|
||||
@@ -183,12 +203,12 @@ export default function ComplianceHubPage() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Quick Actions */}
|
||||
{/* Quick Actions - Always visible at top */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Schnellzugriff</h3>
|
||||
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-4">
|
||||
<Link
|
||||
href="/sdk/audit-checklist"
|
||||
href="/compliance/audit-checklist"
|
||||
className="p-4 rounded-lg border border-slate-200 hover:border-purple-500 hover:bg-purple-50 transition-colors text-center"
|
||||
>
|
||||
<div className="text-purple-600 mb-2 flex justify-center">
|
||||
@@ -201,7 +221,7 @@ export default function ComplianceHubPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/controls"
|
||||
href="/compliance/controls"
|
||||
className="p-4 rounded-lg border border-slate-200 hover:border-green-500 hover:bg-green-50 transition-colors text-center"
|
||||
>
|
||||
<div className="text-green-600 mb-2 flex justify-center">
|
||||
@@ -214,7 +234,7 @@ export default function ComplianceHubPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/evidence"
|
||||
href="/compliance/evidence"
|
||||
className="p-4 rounded-lg border border-slate-200 hover:border-blue-500 hover:bg-blue-50 transition-colors text-center"
|
||||
>
|
||||
<div className="text-blue-600 mb-2 flex justify-center">
|
||||
@@ -227,7 +247,7 @@ export default function ComplianceHubPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/risks"
|
||||
href="/compliance/risks"
|
||||
className="p-4 rounded-lg border border-slate-200 hover:border-red-500 hover:bg-red-50 transition-colors text-center"
|
||||
>
|
||||
<div className="text-red-600 mb-2 flex justify-center">
|
||||
@@ -240,7 +260,7 @@ export default function ComplianceHubPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/modules"
|
||||
href="/compliance/modules"
|
||||
className="p-4 rounded-lg border border-slate-200 hover:border-pink-500 hover:bg-pink-50 transition-colors text-center"
|
||||
>
|
||||
<div className="text-pink-600 mb-2 flex justify-center">
|
||||
@@ -253,7 +273,7 @@ export default function ComplianceHubPage() {
|
||||
</Link>
|
||||
|
||||
<Link
|
||||
href="/sdk/audit-report"
|
||||
href="/compliance/audit-report"
|
||||
className="p-4 rounded-lg border border-slate-200 hover:border-orange-500 hover:bg-orange-50 transition-colors text-center"
|
||||
>
|
||||
<div className="text-orange-600 mb-2 flex justify-center">
|
||||
@@ -275,6 +295,7 @@ export default function ComplianceHubPage() {
|
||||
<>
|
||||
{/* Score and Stats Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-5 gap-4">
|
||||
{/* Score Card */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-sm font-medium text-slate-500 mb-4">Compliance Score</h3>
|
||||
<div className={`text-5xl font-bold ${scoreColor}`}>
|
||||
@@ -291,6 +312,7 @@ export default function ComplianceHubPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats Cards */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
@@ -356,10 +378,11 @@ export default function ComplianceHubPage() {
|
||||
|
||||
{/* Control-Mappings & Findings Row */}
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
|
||||
{/* 474 Control-Mappings Card */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Control-Mappings</h3>
|
||||
<Link href="/sdk/controls" className="text-sm text-purple-600 hover:text-purple-700">
|
||||
<Link href="/compliance/controls" className="text-sm text-purple-600 hover:text-purple-700">
|
||||
Alle anzeigen →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -393,10 +416,11 @@ export default function ComplianceHubPage() {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Haupt-/Nebenabweichungen Card */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Audit Findings</h3>
|
||||
<Link href="/sdk/audit-checklist" className="text-sm text-purple-600 hover:text-purple-700">
|
||||
<Link href="/compliance/audit-checklist" className="text-sm text-purple-600 hover:text-purple-700">
|
||||
Audit Checkliste →
|
||||
</Link>
|
||||
</div>
|
||||
@@ -435,7 +459,7 @@ export default function ComplianceHubPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Domain Chart */}
|
||||
{/* Domain Chart - Full Width */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Controls nach Domain</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
@@ -0,0 +1,511 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Loeschfristen - Data Retention Management
|
||||
*
|
||||
* Art. 17 DSGVO - Recht auf Loeschung
|
||||
* Art. 5 Abs. 1 lit. e DSGVO - Speicherbegrenzung
|
||||
*
|
||||
* Verwaltet:
|
||||
* - Aufbewahrungsfristen
|
||||
* - Consent-Deadlines
|
||||
* - Automatische Loeschungen
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
const API_BASE = '/api/admin/consent'
|
||||
|
||||
interface RetentionPolicy {
|
||||
id: string
|
||||
dataCategory: string
|
||||
retentionPeriod: string
|
||||
legalBasis: string
|
||||
autoDelete: boolean
|
||||
lastRun?: string
|
||||
nextRun?: string
|
||||
itemsToDelete?: number
|
||||
}
|
||||
|
||||
interface ConsentDeadline {
|
||||
id: string
|
||||
userId: string
|
||||
documentName: string
|
||||
versionNumber: string
|
||||
deadlineAt: string
|
||||
reminderCount: number
|
||||
daysRemaining: number
|
||||
status: 'pending' | 'overdue' | 'completed'
|
||||
}
|
||||
|
||||
interface DeletionJob {
|
||||
id: string
|
||||
dataCategory: string
|
||||
scheduledAt: string
|
||||
status: 'scheduled' | 'running' | 'completed' | 'failed'
|
||||
itemsProcessed: number
|
||||
itemsTotal: number
|
||||
completedAt?: string
|
||||
}
|
||||
|
||||
export default function LoeschfristenPage() {
|
||||
const [activeTab, setActiveTab] = useState<'policies' | 'deadlines' | 'jobs' | 'manual'>('policies')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [processing, setProcessing] = useState(false)
|
||||
|
||||
// Mock data - in production, this comes from API
|
||||
const retentionPolicies: RetentionPolicy[] = [
|
||||
{
|
||||
id: 'pol_1',
|
||||
dataCategory: 'Nutzerkonten (inaktiv)',
|
||||
retentionPeriod: '3 Jahre nach letzter Aktivitaet',
|
||||
legalBasis: 'Art. 5 Abs. 1 lit. e DSGVO',
|
||||
autoDelete: true,
|
||||
lastRun: '2024-12-01',
|
||||
nextRun: '2025-01-01',
|
||||
itemsToDelete: 23
|
||||
},
|
||||
{
|
||||
id: 'pol_2',
|
||||
dataCategory: 'Consent-Nachweise',
|
||||
retentionPeriod: '6 Jahre nach Widerruf',
|
||||
legalBasis: 'Nachweispflicht',
|
||||
autoDelete: true,
|
||||
lastRun: '2024-12-01',
|
||||
nextRun: '2025-01-01',
|
||||
itemsToDelete: 0
|
||||
},
|
||||
{
|
||||
id: 'pol_3',
|
||||
dataCategory: 'System-Logs',
|
||||
retentionPeriod: '90 Tage',
|
||||
legalBasis: 'Berechtigtes Interesse (IT-Sicherheit)',
|
||||
autoDelete: true,
|
||||
lastRun: '2024-12-14',
|
||||
nextRun: '2024-12-15',
|
||||
itemsToDelete: 15420
|
||||
},
|
||||
{
|
||||
id: 'pol_4',
|
||||
dataCategory: 'Security-Logs',
|
||||
retentionPeriod: '2 Jahre',
|
||||
legalBasis: 'Berechtigtes Interesse (Sicherheit)',
|
||||
autoDelete: true,
|
||||
lastRun: '2024-12-01',
|
||||
nextRun: '2025-01-01',
|
||||
itemsToDelete: 0
|
||||
},
|
||||
{
|
||||
id: 'pol_5',
|
||||
dataCategory: 'Lernfortschrittsdaten',
|
||||
retentionPeriod: 'Ende Schuljahr + 1 Jahr',
|
||||
legalBasis: 'Vertragserfuellung',
|
||||
autoDelete: false,
|
||||
itemsToDelete: 45
|
||||
},
|
||||
{
|
||||
id: 'pol_6',
|
||||
dataCategory: 'KI-Verarbeitungsdaten',
|
||||
retentionPeriod: 'Sofortige Loeschung',
|
||||
legalBasis: 'Datenminimierung',
|
||||
autoDelete: true,
|
||||
lastRun: '2024-12-15',
|
||||
nextRun: 'Kontinuierlich',
|
||||
itemsToDelete: 0
|
||||
},
|
||||
]
|
||||
|
||||
const consentDeadlines: ConsentDeadline[] = [
|
||||
{ id: 'dl_1', userId: 'usr_456', documentName: 'AGB', versionNumber: 'v2.1.0', deadlineAt: '2025-01-15', reminderCount: 2, daysRemaining: 20, status: 'pending' },
|
||||
{ id: 'dl_2', userId: 'usr_789', documentName: 'Datenschutz', versionNumber: 'v3.0.0', deadlineAt: '2024-12-28', reminderCount: 3, daysRemaining: 3, status: 'pending' },
|
||||
{ id: 'dl_3', userId: 'usr_012', documentName: 'AGB', versionNumber: 'v2.1.0', deadlineAt: '2024-12-10', reminderCount: 4, daysRemaining: -5, status: 'overdue' },
|
||||
]
|
||||
|
||||
const deletionJobs: DeletionJob[] = [
|
||||
{ id: 'job_1', dataCategory: 'System-Logs', scheduledAt: '2024-12-14T02:00:00', status: 'completed', itemsProcessed: 12500, itemsTotal: 12500, completedAt: '2024-12-14T02:15:00' },
|
||||
{ id: 'job_2', dataCategory: 'Inaktive Sessions', scheduledAt: '2024-12-14T03:00:00', status: 'completed', itemsProcessed: 450, itemsTotal: 450, completedAt: '2024-12-14T03:02:00' },
|
||||
{ id: 'job_3', dataCategory: 'System-Logs', scheduledAt: '2024-12-15T02:00:00', status: 'scheduled', itemsProcessed: 0, itemsTotal: 15420 },
|
||||
]
|
||||
|
||||
async function triggerDeadlineProcessing() {
|
||||
setProcessing(true)
|
||||
try {
|
||||
const token = localStorage.getItem('bp_admin_token')
|
||||
const res = await fetch(`${API_BASE}/deadlines`, {
|
||||
method: 'POST',
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
})
|
||||
if (res.ok) {
|
||||
alert('Deadline-Verarbeitung gestartet')
|
||||
} else {
|
||||
alert('Fehler bei der Verarbeitung')
|
||||
}
|
||||
} catch {
|
||||
alert('Verbindungsfehler')
|
||||
} finally {
|
||||
setProcessing(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tabs = [
|
||||
{ id: 'policies', label: 'Aufbewahrungsfristen' },
|
||||
{ id: 'deadlines', label: 'Consent-Deadlines' },
|
||||
{ id: 'jobs', label: 'Loeschjobs' },
|
||||
{ id: 'manual', label: 'Manuelle Loeschung' },
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Abgeschlossen</span>
|
||||
case 'running':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">Laeuft</span>
|
||||
case 'scheduled':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Geplant</span>
|
||||
case 'failed':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Fehlgeschlagen</span>
|
||||
case 'pending':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Ausstehend</span>
|
||||
case 'overdue':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-red-100 text-red-800">Ueberfaellig</span>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="Loeschfristen & Datenaufbewahrung"
|
||||
purpose="Verwaltung von Aufbewahrungsfristen, automatischen Loeschungen und Consent-Deadlines gemaess DSGVO Art. 5 (Speicherbegrenzung) und Art. 17 (Recht auf Loeschung)."
|
||||
audience={['DSB', 'IT-Admin', 'Compliance Officer']}
|
||||
gdprArticles={['Art. 5 Abs. 1 lit. e (Speicherbegrenzung)', 'Art. 17 (Recht auf Loeschung)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'cron-jobs'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'VVT', href: '/compliance/vvt', description: 'Verarbeitungsverzeichnis' },
|
||||
{ name: 'DSR', href: '/compliance/dsr', description: 'Loeschanfragen' },
|
||||
{ name: 'Einwilligungen', href: '/compliance/einwilligungen', description: 'Consent-Uebersicht' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Quick Stats */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-slate-900">{retentionPolicies.length}</div>
|
||||
<div className="text-sm text-slate-500">Aufbewahrungsrichtlinien</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-yellow-600">
|
||||
{consentDeadlines.filter(d => d.status === 'pending').length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Offene Deadlines</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-red-600">
|
||||
{consentDeadlines.filter(d => d.status === 'overdue').length}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Ueberfaellige</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-purple-600">
|
||||
{retentionPolicies.reduce((sum, p) => sum + (p.itemsToDelete || 0), 0).toLocaleString()}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Zur Loeschung vorgemerkt</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg w-fit">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id as typeof activeTab)}
|
||||
className={`px-4 py-2 text-sm font-medium rounded-md transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'bg-white text-slate-900 shadow-sm'
|
||||
: 'text-slate-600 hover:text-slate-900'
|
||||
}`}
|
||||
>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white rounded-xl border border-slate-200">
|
||||
{/* Policies Tab */}
|
||||
{activeTab === 'policies' && (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Aufbewahrungsfristen</h2>
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
|
||||
+ Neue Richtlinie
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{retentionPolicies.map((policy) => (
|
||||
<div key={policy.id} className="border border-slate-200 rounded-lg p-4 hover:border-purple-300 transition-colors">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-grow">
|
||||
<div className="flex items-center gap-3 mb-2">
|
||||
<h3 className="font-semibold text-slate-900">{policy.dataCategory}</h3>
|
||||
{policy.autoDelete ? (
|
||||
<span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">Auto-Loeschung</span>
|
||||
) : (
|
||||
<span className="px-2 py-0.5 bg-slate-100 text-slate-600 rounded text-xs">Manuell</span>
|
||||
)}
|
||||
{(policy.itemsToDelete || 0) > 0 && (
|
||||
<span className="px-2 py-0.5 bg-orange-100 text-orange-700 rounded text-xs">
|
||||
{policy.itemsToDelete} zur Loeschung
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-slate-500">Frist:</span>
|
||||
<span className="ml-1 font-medium text-slate-700">{policy.retentionPeriod}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Rechtsgrundlage:</span>
|
||||
<span className="ml-1 text-slate-600">{policy.legalBasis}</span>
|
||||
</div>
|
||||
{policy.lastRun && (
|
||||
<div>
|
||||
<span className="text-slate-500">Letzter Lauf:</span>
|
||||
<span className="ml-1 text-slate-600">{policy.lastRun}</span>
|
||||
</div>
|
||||
)}
|
||||
{policy.nextRun && (
|
||||
<div>
|
||||
<span className="text-slate-500">Naechster Lauf:</span>
|
||||
<span className="ml-1 text-slate-600">{policy.nextRun}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg">
|
||||
Bearbeiten
|
||||
</button>
|
||||
{(policy.itemsToDelete || 0) > 0 && (
|
||||
<button className="px-3 py-1.5 text-sm text-white bg-red-600 hover:bg-red-700 rounded-lg">
|
||||
Jetzt loeschen
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Deadlines Tab */}
|
||||
{activeTab === 'deadlines' && (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Consent-Deadlines</h2>
|
||||
<button
|
||||
onClick={triggerDeadlineProcessing}
|
||||
disabled={processing}
|
||||
className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
{processing ? 'Verarbeite...' : 'Deadlines verarbeiten'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="mb-4 p-4 bg-yellow-50 border border-yellow-200 rounded-lg">
|
||||
<p className="text-sm text-yellow-800">
|
||||
Nutzer haben 30 Tage Zeit, neue Pflichtdokumente zu akzeptieren.
|
||||
Nach Ablauf wird der Account gesperrt, bis die Zustimmung erteilt wird.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Nutzer</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Dokument</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Deadline</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Erinnerungen</th>
|
||||
<th className="text-left py-3 px-4 text-sm font-medium text-slate-500">Status</th>
|
||||
<th className="text-right py-3 px-4 text-sm font-medium text-slate-500">Aktionen</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{consentDeadlines.map((deadline) => (
|
||||
<tr key={deadline.id} className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="py-3 px-4 font-mono text-sm">{deadline.userId}</td>
|
||||
<td className="py-3 px-4">
|
||||
<div>{deadline.documentName}</div>
|
||||
<div className="text-xs text-slate-500">{deadline.versionNumber}</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<div>{deadline.deadlineAt}</div>
|
||||
<div className={`text-xs ${deadline.daysRemaining < 0 ? 'text-red-600' : deadline.daysRemaining <= 7 ? 'text-orange-600' : 'text-slate-500'}`}>
|
||||
{deadline.daysRemaining < 0
|
||||
? `${Math.abs(deadline.daysRemaining)} Tage ueberfaellig`
|
||||
: `${deadline.daysRemaining} Tage verbleibend`}
|
||||
</div>
|
||||
</td>
|
||||
<td className="py-3 px-4">
|
||||
<span className="px-2 py-1 bg-slate-100 text-slate-600 rounded text-xs">
|
||||
{deadline.reminderCount} gesendet
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-3 px-4">{getStatusBadge(deadline.status)}</td>
|
||||
<td className="py-3 px-4 text-right">
|
||||
<button className="text-purple-600 hover:text-purple-700 text-sm font-medium mr-3">
|
||||
Erinnerung senden
|
||||
</button>
|
||||
{deadline.status === 'overdue' && (
|
||||
<button className="text-red-600 hover:text-red-700 text-sm font-medium">
|
||||
Account sperren
|
||||
</button>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Jobs Tab */}
|
||||
{activeTab === 'jobs' && (
|
||||
<div className="p-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Loeschjobs</h2>
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 text-sm font-medium">
|
||||
+ Neuer Job
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
{deletionJobs.map((job) => (
|
||||
<div key={job.id} className="border border-slate-200 rounded-lg p-4">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-medium text-slate-900">{job.dataCategory}</h3>
|
||||
{getStatusBadge(job.status)}
|
||||
</div>
|
||||
<span className="text-sm text-slate-500">
|
||||
Geplant: {new Date(job.scheduledAt).toLocaleString('de-DE')}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="flex-grow h-2 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full ${job.status === 'completed' ? 'bg-green-500' : job.status === 'running' ? 'bg-blue-500' : 'bg-slate-300'}`}
|
||||
style={{ width: `${job.itemsTotal > 0 ? (job.itemsProcessed / job.itemsTotal) * 100 : 0}%` }}
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-slate-600 whitespace-nowrap">
|
||||
{job.itemsProcessed.toLocaleString()} / {job.itemsTotal.toLocaleString()}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{job.completedAt && (
|
||||
<div className="mt-2 text-xs text-slate-500">
|
||||
Abgeschlossen: {new Date(job.completedAt).toLocaleString('de-DE')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Manual Tab */}
|
||||
{activeTab === 'manual' && (
|
||||
<div className="p-6">
|
||||
<h2 className="text-lg font-semibold text-slate-900 mb-6">Manuelle Loeschung</h2>
|
||||
|
||||
<div className="bg-red-50 border border-red-200 rounded-lg p-4 mb-6">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-red-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
<div>
|
||||
<h4 className="font-semibold text-red-900">Achtung: Manuelle Loeschung</h4>
|
||||
<p className="text-sm text-red-800 mt-1">
|
||||
Manuelle Loeschungen sind unwiderruflich. Stellen Sie sicher, dass keine gesetzlichen
|
||||
Aufbewahrungsfristen verletzt werden und alle notwendigen Backups erstellt wurden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<div className="border border-slate-200 rounded-lg p-4">
|
||||
<h3 className="font-medium text-slate-900 mb-3">Nutzer-Daten loeschen</h3>
|
||||
<div className="flex gap-3">
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Nutzer-ID eingeben..."
|
||||
className="flex-grow px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
/>
|
||||
<button className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium">
|
||||
Daten loeschen
|
||||
</button>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-2">
|
||||
Loescht alle personenbezogenen Daten eines Nutzers (Art. 17 DSGVO)
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200 rounded-lg p-4">
|
||||
<h3 className="font-medium text-slate-900 mb-3">Alte Logs bereinigen</h3>
|
||||
<div className="flex gap-3">
|
||||
<select className="px-3 py-2 border border-slate-300 rounded-lg text-sm">
|
||||
<option value="system">System-Logs</option>
|
||||
<option value="audit">Audit-Logs</option>
|
||||
<option value="access">Zugriffs-Logs</option>
|
||||
</select>
|
||||
<input
|
||||
type="number"
|
||||
placeholder="Aelter als (Tage)"
|
||||
className="w-40 px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
defaultValue={90}
|
||||
/>
|
||||
<button className="px-4 py-2 bg-red-600 text-white rounded-lg hover:bg-red-700 text-sm font-medium">
|
||||
Logs bereinigen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-purple-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-purple-900">Speicherbegrenzung (Art. 5)</h4>
|
||||
<p className="text-sm text-purple-800 mt-1">
|
||||
Personenbezogene Daten duerfen nur so lange gespeichert werden, wie es fuer die Zwecke
|
||||
erforderlich ist. Die automatische Loeschung stellt die Einhaltung dieser Vorgabe sicher.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,601 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Service Module Registry Page
|
||||
*
|
||||
* Features:
|
||||
* - List all Breakpilot services with regulation mappings
|
||||
* - Filter by type, criticality, PII, AI
|
||||
* - Detail panel with regulations
|
||||
* - Seed functionality
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface ServiceModule {
|
||||
id: string
|
||||
name: string
|
||||
display_name: string
|
||||
description: string | null
|
||||
service_type: string
|
||||
port: number | null
|
||||
technology_stack: string[]
|
||||
repository_path: string | null
|
||||
docker_image: string | null
|
||||
data_categories: string[]
|
||||
processes_pii: boolean
|
||||
processes_health_data: boolean
|
||||
ai_components: boolean
|
||||
criticality: string
|
||||
owner_team: string | null
|
||||
is_active: boolean
|
||||
compliance_score: number | null
|
||||
regulation_count: number
|
||||
risk_count: number
|
||||
created_at: string
|
||||
regulations?: Array<{
|
||||
code: string
|
||||
name: string
|
||||
relevance_level: string
|
||||
notes: string | null
|
||||
}>
|
||||
}
|
||||
|
||||
interface ModulesOverview {
|
||||
total_modules: number
|
||||
modules_by_type: Record<string, number>
|
||||
modules_by_criticality: Record<string, number>
|
||||
modules_processing_pii: number
|
||||
modules_with_ai: number
|
||||
average_compliance_score: number | null
|
||||
regulations_coverage: Record<string, number>
|
||||
}
|
||||
|
||||
const SERVICE_TYPE_CONFIG: Record<string, { icon: string; color: string; bgColor: string }> = {
|
||||
backend: { icon: '⚙️', color: 'text-blue-700', bgColor: 'bg-blue-100' },
|
||||
database: { icon: '🗄️', color: 'text-purple-700', bgColor: 'bg-purple-100' },
|
||||
ai: { icon: '🤖', color: 'text-pink-700', bgColor: 'bg-pink-100' },
|
||||
communication: { icon: '💬', color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
storage: { icon: '📦', color: 'text-orange-700', bgColor: 'bg-orange-100' },
|
||||
infrastructure: { icon: '🌐', color: 'text-slate-700', bgColor: 'bg-slate-100' },
|
||||
monitoring: { icon: '📊', color: 'text-cyan-700', bgColor: 'bg-cyan-100' },
|
||||
security: { icon: '🔒', color: 'text-red-700', bgColor: 'bg-red-100' },
|
||||
}
|
||||
|
||||
const CRITICALITY_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
critical: { color: 'text-red-700', bgColor: 'bg-red-100' },
|
||||
high: { color: 'text-orange-700', bgColor: 'bg-orange-100' },
|
||||
medium: { color: 'text-yellow-700', bgColor: 'bg-yellow-100' },
|
||||
low: { color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
}
|
||||
|
||||
const RELEVANCE_CONFIG: Record<string, { color: string; bgColor: string }> = {
|
||||
critical: { color: 'text-red-700', bgColor: 'bg-red-100' },
|
||||
high: { color: 'text-orange-700', bgColor: 'bg-orange-100' },
|
||||
medium: { color: 'text-yellow-700', bgColor: 'bg-yellow-100' },
|
||||
low: { color: 'text-green-700', bgColor: 'bg-green-100' },
|
||||
}
|
||||
|
||||
export default function ModulesPage() {
|
||||
const [modules, setModules] = useState<ServiceModule[]>([])
|
||||
const [overview, setOverview] = useState<ModulesOverview | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
|
||||
const [typeFilter, setTypeFilter] = useState<string>('all')
|
||||
const [criticalityFilter, setCriticalityFilter] = useState<string>('all')
|
||||
const [piiFilter, setPiiFilter] = useState<boolean | null>(null)
|
||||
const [aiFilter, setAiFilter] = useState<boolean | null>(null)
|
||||
const [searchTerm, setSearchTerm] = useState('')
|
||||
|
||||
const [selectedModule, setSelectedModule] = useState<ServiceModule | null>(null)
|
||||
const [loadingDetail, setLoadingDetail] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
fetchModules()
|
||||
fetchOverview()
|
||||
}, [])
|
||||
|
||||
const fetchModules = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const params = new URLSearchParams()
|
||||
if (typeFilter !== 'all') params.append('service_type', typeFilter)
|
||||
if (criticalityFilter !== 'all') params.append('criticality', criticalityFilter)
|
||||
if (piiFilter !== null) params.append('processes_pii', String(piiFilter))
|
||||
if (aiFilter !== null) params.append('ai_components', String(aiFilter))
|
||||
|
||||
const url = `/api/admin/compliance/modules${params.toString() ? '?' + params.toString() : ''}`
|
||||
const res = await fetch(url)
|
||||
if (!res.ok) throw new Error('Failed to fetch modules')
|
||||
const data = await res.json()
|
||||
setModules(data.modules || [])
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unknown error')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchOverview = async () => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/compliance/modules/overview`)
|
||||
if (!res.ok) throw new Error('Failed to fetch overview')
|
||||
const data = await res.json()
|
||||
setOverview(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch overview:', err)
|
||||
}
|
||||
}
|
||||
|
||||
const fetchModuleDetail = async (moduleId: string) => {
|
||||
try {
|
||||
setLoadingDetail(true)
|
||||
const res = await fetch(`/api/admin/compliance/modules/${moduleId}`)
|
||||
if (!res.ok) throw new Error('Failed to fetch module details')
|
||||
const data = await res.json()
|
||||
setSelectedModule(data)
|
||||
} catch (err) {
|
||||
console.error('Failed to fetch module details:', err)
|
||||
} finally {
|
||||
setLoadingDetail(false)
|
||||
}
|
||||
}
|
||||
|
||||
const seedModules = async (force: boolean = false) => {
|
||||
try {
|
||||
const res = await fetch(`/api/admin/compliance/modules/seed`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ force }),
|
||||
})
|
||||
if (!res.ok) throw new Error('Failed to seed modules')
|
||||
const data = await res.json()
|
||||
alert(`Seeded ${data.modules_created} modules with ${data.mappings_created} regulation mappings`)
|
||||
fetchModules()
|
||||
fetchOverview()
|
||||
} catch (err) {
|
||||
alert('Failed to seed modules: ' + (err instanceof Error ? err.message : 'Unknown error'))
|
||||
}
|
||||
}
|
||||
|
||||
const filteredModules = modules.filter(m => {
|
||||
if (!searchTerm) return true
|
||||
const term = searchTerm.toLowerCase()
|
||||
return (
|
||||
m.name.toLowerCase().includes(term) ||
|
||||
m.display_name.toLowerCase().includes(term) ||
|
||||
(m.description && m.description.toLowerCase().includes(term)) ||
|
||||
m.technology_stack.some(t => t.toLowerCase().includes(term))
|
||||
)
|
||||
})
|
||||
|
||||
const modulesByType = filteredModules.reduce((acc, m) => {
|
||||
const type = m.service_type || 'unknown'
|
||||
if (!acc[type]) acc[type] = []
|
||||
acc[type].push(m)
|
||||
return acc
|
||||
}, {} as Record<string, ServiceModule[]>)
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-slate-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Service Module Registry</h1>
|
||||
<p className="text-slate-600">
|
||||
Alle {overview?.total_modules || 0} Breakpilot-Services mit Regulation-Mappings
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/compliance/hub"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Compliance Hub
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Service Module Registry"
|
||||
purpose="Das Service Registry dokumentiert alle Breakpilot-Microservices und deren regulatorische Anforderungen. Jeder Service wird mit relevanten Regulierungen (DSGVO, AI Act, BSI TR-03161) verknuepft und zeigt an, welche Compliance-Anforderungen gelten."
|
||||
audience={['Entwickler', 'CISO', 'Compliance Officer', 'Architekten']}
|
||||
gdprArticles={['Art. 30 (Verzeichnis)', 'Art. 32 (Sicherheit)']}
|
||||
architecture={{
|
||||
services: ['Python Backend (FastAPI)', 'compliance_modules Modul'],
|
||||
databases: ['PostgreSQL (service_modules, module_regulation_mappings Tables)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Massnahmen verwalten' },
|
||||
{ name: 'Risks', href: '/compliance/risks', description: 'Risikomatrix' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Overview Stats */}
|
||||
{overview && (
|
||||
<div className="grid grid-cols-2 md:grid-cols-3 lg:grid-cols-6 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<p className="text-3xl font-bold text-blue-600">{overview.total_modules}</p>
|
||||
<p className="text-sm text-slate-500">Services</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-red-200">
|
||||
<p className="text-3xl font-bold text-red-600">{overview.modules_by_criticality?.critical || 0}</p>
|
||||
<p className="text-sm text-slate-500">Critical</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-purple-200">
|
||||
<p className="text-3xl font-bold text-purple-600">{overview.modules_processing_pii}</p>
|
||||
<p className="text-sm text-slate-500">PII-Processing</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-pink-200">
|
||||
<p className="text-3xl font-bold text-pink-600">{overview.modules_with_ai}</p>
|
||||
<p className="text-sm text-slate-500">AI-Komponenten</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-green-200">
|
||||
<p className="text-3xl font-bold text-green-600">
|
||||
{Object.keys(overview.regulations_coverage || {}).length}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">Regulations</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-cyan-200">
|
||||
<p className="text-3xl font-bold text-cyan-600">
|
||||
{overview.average_compliance_score !== null
|
||||
? `${overview.average_compliance_score}%`
|
||||
: 'N/A'}
|
||||
</p>
|
||||
<p className="text-sm text-slate-500">Avg. Score</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Filters */}
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4 mb-6">
|
||||
<div className="flex flex-wrap gap-4 items-end">
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">Service Type</label>
|
||||
<select
|
||||
value={typeFilter}
|
||||
onChange={(e) => setTypeFilter(e.target.value)}
|
||||
className="border rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="all">Alle Typen</option>
|
||||
<option value="backend">Backend</option>
|
||||
<option value="database">Database</option>
|
||||
<option value="ai">AI/ML</option>
|
||||
<option value="communication">Communication</option>
|
||||
<option value="storage">Storage</option>
|
||||
<option value="infrastructure">Infrastructure</option>
|
||||
<option value="monitoring">Monitoring</option>
|
||||
<option value="security">Security</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">Criticality</label>
|
||||
<select
|
||||
value={criticalityFilter}
|
||||
onChange={(e) => setCriticalityFilter(e.target.value)}
|
||||
className="border rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="all">Alle</option>
|
||||
<option value="critical">Critical</option>
|
||||
<option value="high">High</option>
|
||||
<option value="medium">Medium</option>
|
||||
<option value="low">Low</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">PII</label>
|
||||
<select
|
||||
value={piiFilter === null ? 'all' : String(piiFilter)}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
setPiiFilter(val === 'all' ? null : val === 'true')
|
||||
}}
|
||||
className="border rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="all">Alle</option>
|
||||
<option value="true">Verarbeitet PII</option>
|
||||
<option value="false">Keine PII</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-xs text-slate-500 mb-1">AI</label>
|
||||
<select
|
||||
value={aiFilter === null ? 'all' : String(aiFilter)}
|
||||
onChange={(e) => {
|
||||
const val = e.target.value
|
||||
setAiFilter(val === 'all' ? null : val === 'true')
|
||||
}}
|
||||
className="border rounded-lg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="all">Alle</option>
|
||||
<option value="true">Mit AI</option>
|
||||
<option value="false">Ohne AI</option>
|
||||
</select>
|
||||
</div>
|
||||
<div className="flex-1 min-w-[200px]">
|
||||
<label className="block text-xs text-slate-500 mb-1">Suche</label>
|
||||
<input
|
||||
type="text"
|
||||
placeholder="Service, Beschreibung, Technologie..."
|
||||
value={searchTerm}
|
||||
onChange={(e) => setSearchTerm(e.target.value)}
|
||||
className="border rounded-lg px-3 py-2 text-sm w-full"
|
||||
/>
|
||||
</div>
|
||||
<button
|
||||
onClick={fetchModules}
|
||||
className="px-4 py-2 bg-slate-100 rounded-lg hover:bg-slate-200 text-sm"
|
||||
>
|
||||
Filter
|
||||
</button>
|
||||
<button
|
||||
onClick={() => seedModules(false)}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 text-sm"
|
||||
>
|
||||
Seed
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Error */}
|
||||
{error && (
|
||||
<div className="bg-red-50 border border-red-200 text-red-700 p-4 rounded-lg mb-6">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Main Content */}
|
||||
{loading ? (
|
||||
<div className="flex items-center justify-center h-64">
|
||||
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary-600" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex gap-6">
|
||||
{/* Module List */}
|
||||
<div className="flex-1 space-y-4">
|
||||
{Object.entries(modulesByType).map(([type, typeModules]) => (
|
||||
<div key={type} className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<div className={`px-4 py-2 border-b ${SERVICE_TYPE_CONFIG[type]?.bgColor || 'bg-slate-100'}`}>
|
||||
<span className="text-lg mr-2">{SERVICE_TYPE_CONFIG[type]?.icon || '📁'}</span>
|
||||
<span className={`font-semibold ${SERVICE_TYPE_CONFIG[type]?.color || 'text-slate-700'}`}>
|
||||
{type.charAt(0).toUpperCase() + type.slice(1)}
|
||||
</span>
|
||||
<span className="text-slate-500 ml-2">({typeModules.length})</span>
|
||||
</div>
|
||||
<div className="divide-y">
|
||||
{typeModules.map((module) => (
|
||||
<div
|
||||
key={module.id}
|
||||
onClick={() => fetchModuleDetail(module.name)}
|
||||
className={`p-4 cursor-pointer hover:bg-slate-50 transition ${
|
||||
selectedModule?.id === module.id ? 'bg-blue-50' : ''
|
||||
}`}
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-slate-900">{module.display_name}</span>
|
||||
{module.port && (
|
||||
<span className="text-xs text-slate-400">:{module.port}</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm text-slate-500 mt-1">{module.name}</div>
|
||||
{module.description && (
|
||||
<div className="text-sm text-slate-600 mt-1 line-clamp-2">
|
||||
{module.description}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex flex-wrap gap-1 mt-2">
|
||||
{module.technology_stack.slice(0, 4).map((tech, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-600 text-xs rounded">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
{module.technology_stack.length > 4 && (
|
||||
<span className="px-2 py-0.5 text-slate-400 text-xs">
|
||||
+{module.technology_stack.length - 4}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<span className={`px-2 py-0.5 text-xs rounded ${
|
||||
CRITICALITY_CONFIG[module.criticality]?.bgColor || 'bg-slate-100'
|
||||
} ${CRITICALITY_CONFIG[module.criticality]?.color || 'text-slate-700'}`}>
|
||||
{module.criticality}
|
||||
</span>
|
||||
<div className="flex gap-1 mt-1">
|
||||
{module.processes_pii && (
|
||||
<span className="px-1.5 py-0.5 bg-purple-100 text-purple-700 text-xs rounded" title="Verarbeitet PII">
|
||||
PII
|
||||
</span>
|
||||
)}
|
||||
{module.ai_components && (
|
||||
<span className="px-1.5 py-0.5 bg-pink-100 text-pink-700 text-xs rounded" title="AI-Komponenten">
|
||||
AI
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-slate-400 mt-1">
|
||||
{module.regulation_count} Regulations
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
|
||||
{filteredModules.length === 0 && !loading && (
|
||||
<div className="text-center py-12 text-slate-500 bg-white rounded-xl shadow-sm border">
|
||||
Keine Module gefunden.
|
||||
<button
|
||||
onClick={() => seedModules(false)}
|
||||
className="text-primary-600 hover:underline ml-1"
|
||||
>
|
||||
Jetzt seeden?
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Detail Panel */}
|
||||
{selectedModule && (
|
||||
<div className="w-96 bg-white rounded-xl shadow-sm border sticky top-6 h-fit">
|
||||
<div className={`px-4 py-3 border-b ${SERVICE_TYPE_CONFIG[selectedModule.service_type]?.bgColor || 'bg-slate-100'}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<span className="text-lg">{SERVICE_TYPE_CONFIG[selectedModule.service_type]?.icon || '📁'}</span>
|
||||
<button
|
||||
onClick={() => setSelectedModule(null)}
|
||||
className="text-slate-400 hover:text-slate-600"
|
||||
>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
<h3 className="font-bold text-lg mt-2">{selectedModule.display_name}</h3>
|
||||
<div className="text-sm text-slate-600">{selectedModule.name}</div>
|
||||
</div>
|
||||
|
||||
{loadingDetail ? (
|
||||
<div className="p-4 text-center text-slate-500">Lade Details...</div>
|
||||
) : (
|
||||
<div className="p-4 space-y-4 max-h-[70vh] overflow-y-auto">
|
||||
{selectedModule.description && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase mb-1">Beschreibung</div>
|
||||
<div className="text-sm text-slate-700">{selectedModule.description}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="grid grid-cols-2 gap-2 text-sm">
|
||||
{selectedModule.port && (
|
||||
<div>
|
||||
<span className="text-slate-500">Port:</span>
|
||||
<span className="ml-1 font-mono">{selectedModule.port}</span>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<span className="text-slate-500">Criticality:</span>
|
||||
<span className={`ml-1 px-1.5 py-0.5 rounded text-xs ${
|
||||
CRITICALITY_CONFIG[selectedModule.criticality]?.bgColor || ''
|
||||
} ${CRITICALITY_CONFIG[selectedModule.criticality]?.color || ''}`}>
|
||||
{selectedModule.criticality}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase mb-1">Tech Stack</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedModule.technology_stack.map((tech, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-slate-100 text-slate-700 text-xs rounded">
|
||||
{tech}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{selectedModule.data_categories.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase mb-1">Daten-Kategorien</div>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{selectedModule.data_categories.map((cat, i) => (
|
||||
<span key={i} className="px-2 py-0.5 bg-blue-50 text-blue-700 text-xs rounded">
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{selectedModule.processes_pii && (
|
||||
<span className="px-2 py-1 bg-purple-100 text-purple-700 text-xs rounded">
|
||||
Verarbeitet PII
|
||||
</span>
|
||||
)}
|
||||
{selectedModule.ai_components && (
|
||||
<span className="px-2 py-1 bg-pink-100 text-pink-700 text-xs rounded">
|
||||
AI-Komponenten
|
||||
</span>
|
||||
)}
|
||||
{selectedModule.processes_health_data && (
|
||||
<span className="px-2 py-1 bg-red-100 text-red-700 text-xs rounded">
|
||||
Gesundheitsdaten
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedModule.regulations && selectedModule.regulations.length > 0 && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase mb-2">
|
||||
Applicable Regulations ({selectedModule.regulations.length})
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{selectedModule.regulations.map((reg, i) => (
|
||||
<div key={i} className="p-2 bg-slate-50 rounded text-sm">
|
||||
<div className="flex justify-between items-start">
|
||||
<span className="font-medium">{reg.code}</span>
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
RELEVANCE_CONFIG[reg.relevance_level]?.bgColor || 'bg-slate-100'
|
||||
} ${RELEVANCE_CONFIG[reg.relevance_level]?.color || 'text-slate-700'}`}>
|
||||
{reg.relevance_level}
|
||||
</span>
|
||||
</div>
|
||||
<div className="text-slate-500 text-xs">{reg.name}</div>
|
||||
{reg.notes && (
|
||||
<div className="text-slate-600 text-xs mt-1 italic">{reg.notes}</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedModule.owner_team && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase mb-1">Owner</div>
|
||||
<div className="text-sm text-slate-700">{selectedModule.owner_team}</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{selectedModule.repository_path && (
|
||||
<div>
|
||||
<div className="text-xs text-slate-500 uppercase mb-1">Repository</div>
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded block">
|
||||
{selectedModule.repository_path}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Regulations Coverage Overview */}
|
||||
{overview && overview.regulations_coverage && Object.keys(overview.regulations_coverage).length > 0 && (
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4 mt-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Regulation Coverage</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-6 gap-3">
|
||||
{Object.entries(overview.regulations_coverage)
|
||||
.sort(([, a], [, b]) => b - a)
|
||||
.map(([code, count]) => (
|
||||
<div key={code} className="bg-slate-50 rounded-lg p-3 text-center">
|
||||
<div className="text-2xl font-bold text-blue-600">{count}</div>
|
||||
<div className="text-xs text-slate-600 truncate" title={code}>{code}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
'use client'
|
||||
|
||||
import { getCategoryById } from '@/lib/navigation'
|
||||
import { ModuleCard } from '@/components/common/ModuleCard'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
export default function CompliancePage() {
|
||||
const category = getCategoryById('compliance')
|
||||
|
||||
if (!category) {
|
||||
return <div>Kategorie nicht gefunden</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title={category.name}
|
||||
purpose="Diese Kategorie umfasst alle Module fuer Datenschutz, DSGVO-Compliance und rechtliche Dokumentation. Hier verwalten Sie Einwilligungen, bearbeiten Betroffenenanfragen und dokumentieren Audit-Nachweise."
|
||||
audience={['DSB', 'Compliance Officer', 'Auditoren']}
|
||||
gdprArticles={['Art. 5 (Rechenschaftspflicht)', 'Art. 7 (Einwilligung)', 'Art. 15-21 (Betroffenenrechte)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
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-purple-50 border border-purple-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-purple-800 flex items-center gap-2">
|
||||
<span>🛡️</span>
|
||||
DSGVO-Konformitaet
|
||||
</h3>
|
||||
<p className="text-sm text-purple-700 mt-2">
|
||||
Alle Module in dieser Kategorie sind darauf ausgelegt, die DSGVO-Anforderungen zu erfuellen.
|
||||
Die Dokumentation aller Verarbeitungstaetigkeiten erfolgt automatisch und kann jederzeit
|
||||
fuer Audits exportiert werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,446 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Requirements Page - Alle Compliance-Anforderungen mit Implementation-Status
|
||||
*
|
||||
* Features:
|
||||
* - Liste aller 19 Verordnungen mit URLs zu Originaldokumenten
|
||||
* - 558+ Requirements mit Implementation-Status
|
||||
* - Filterung nach Regulation, Status, Prioritaet
|
||||
* - Detail-Ansicht mit Breakpilot-Interpretation
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// Types
|
||||
interface Regulation {
|
||||
id: string
|
||||
code: string
|
||||
name: string
|
||||
full_name: string
|
||||
regulation_type: string
|
||||
source_url: string
|
||||
local_pdf_path?: string
|
||||
effective_date?: string
|
||||
description: string
|
||||
is_active: boolean
|
||||
requirement_count: number
|
||||
}
|
||||
|
||||
interface Requirement {
|
||||
id: string
|
||||
regulation_id: string
|
||||
regulation_code: string
|
||||
article: string
|
||||
paragraph?: string
|
||||
title: string
|
||||
description?: string
|
||||
requirement_text?: string
|
||||
breakpilot_interpretation?: string
|
||||
implementation_status: 'not_started' | 'in_progress' | 'implemented' | 'verified' | 'not_applicable'
|
||||
implementation_details?: string
|
||||
code_references?: Array<{ file: string; line?: number; description?: string }>
|
||||
evidence_description?: string
|
||||
priority: number
|
||||
is_applicable: boolean
|
||||
controls_count: number
|
||||
}
|
||||
|
||||
const STATUS_CONFIG: Record<string, { bg: string; text: string; label: string }> = {
|
||||
not_started: { bg: 'bg-slate-100', text: 'text-slate-700', label: 'Nicht begonnen' },
|
||||
in_progress: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'In Arbeit' },
|
||||
implemented: { bg: 'bg-blue-100', text: 'text-blue-700', label: 'Implementiert' },
|
||||
verified: { bg: 'bg-green-100', text: 'text-green-700', label: 'Verifiziert' },
|
||||
not_applicable: { bg: 'bg-slate-50', text: 'text-slate-500', label: 'N/A' },
|
||||
}
|
||||
|
||||
const PRIORITY_CONFIG: Record<number, { bg: string; text: string; label: string }> = {
|
||||
1: { bg: 'bg-red-100', text: 'text-red-700', label: 'Kritisch' },
|
||||
2: { bg: 'bg-orange-100', text: 'text-orange-700', label: 'Hoch' },
|
||||
3: { bg: 'bg-yellow-100', text: 'text-yellow-700', label: 'Mittel' },
|
||||
}
|
||||
|
||||
const REGULATION_TYPE_LABELS: Record<string, string> = {
|
||||
eu_regulation: 'EU-Verordnung',
|
||||
eu_directive: 'EU-Richtlinie',
|
||||
de_law: 'DE Gesetz',
|
||||
bsi_standard: 'BSI Standard',
|
||||
}
|
||||
|
||||
export default function RequirementsPage() {
|
||||
const [regulations, setRegulations] = useState<Regulation[]>([])
|
||||
const [requirements, setRequirements] = useState<Requirement[]>([])
|
||||
const [selectedRegulation, setSelectedRegulation] = useState<string | null>(null)
|
||||
const [selectedRequirement, setSelectedRequirement] = useState<Requirement | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [requirementsLoading, setRequirementsLoading] = useState(false)
|
||||
|
||||
// Filters
|
||||
const [statusFilter, setStatusFilter] = useState<string>('')
|
||||
const [priorityFilter, setPriorityFilter] = useState<string>('')
|
||||
const [searchQuery, setSearchQuery] = useState('')
|
||||
|
||||
useEffect(() => {
|
||||
loadRegulations()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
if (selectedRegulation) {
|
||||
loadRequirements(selectedRegulation)
|
||||
}
|
||||
}, [selectedRegulation])
|
||||
|
||||
const loadRegulations = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch('/api/admin/compliance/regulations')
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setRegulations(data.regulations || data || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load regulations:', err)
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const loadRequirements = async (regulationCode: string) => {
|
||||
setRequirementsLoading(true)
|
||||
try {
|
||||
const params = new URLSearchParams({ regulation_code: regulationCode })
|
||||
if (statusFilter) params.set('status', statusFilter)
|
||||
if (priorityFilter) params.set('priority', priorityFilter)
|
||||
if (searchQuery) params.set('search', searchQuery)
|
||||
|
||||
const res = await fetch(`/api/admin/compliance/requirements?${params}`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setRequirements(data.requirements || data || [])
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Failed to load requirements:', err)
|
||||
} finally {
|
||||
setRequirementsLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const filteredRequirements = requirements.filter(req => {
|
||||
if (statusFilter && req.implementation_status !== statusFilter) return false
|
||||
if (priorityFilter && req.priority !== parseInt(priorityFilter)) return false
|
||||
if (searchQuery) {
|
||||
const query = searchQuery.toLowerCase()
|
||||
return (
|
||||
req.title.toLowerCase().includes(query) ||
|
||||
req.article.toLowerCase().includes(query) ||
|
||||
req.description?.toLowerCase().includes(query)
|
||||
)
|
||||
}
|
||||
return true
|
||||
})
|
||||
|
||||
const totalRequirements = regulations.reduce((sum, r) => sum + (r.requirement_count || 0), 0)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<PagePurpose
|
||||
title="Requirements & Anforderungen"
|
||||
purpose="Uebersicht aller 558+ Compliance-Anforderungen aus 19 Verordnungen (DSGVO, AI Act, CRA, BSI-TR-03161, etc.). Sehen Sie den Implementation-Status und wie Breakpilot jede Anforderung erfuellt."
|
||||
audience={['DSB', 'Compliance Officer', 'Entwickler', 'Auditoren']}
|
||||
gdprArticles={['Art. 5 (Rechenschaftspflicht)', 'Art. 24 (Verantwortung)']}
|
||||
architecture={{
|
||||
services: ['Python Backend', 'PostgreSQL'],
|
||||
databases: ['compliance_regulations', 'compliance_requirements', 'compliance_control_mappings'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Compliance Hub', href: '/compliance/hub', description: 'Dashboard & Uebersicht' },
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Massnahmen' },
|
||||
{ name: 'Audit Checklist', href: '/compliance/audit-checklist', description: 'Audit durchfuehren' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Stats */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<p className="text-3xl font-bold text-purple-600">{regulations.length}</p>
|
||||
<p className="text-sm text-slate-600">Verordnungen</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<p className="text-3xl font-bold text-blue-600">{totalRequirements}</p>
|
||||
<p className="text-sm text-slate-600">Anforderungen</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<p className="text-3xl font-bold text-green-600">
|
||||
{regulations.filter(r => r.regulation_type === 'eu_regulation').length}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600">EU-Verordnungen</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<p className="text-3xl font-bold text-orange-600">
|
||||
{regulations.filter(r => r.regulation_type === 'bsi_standard').length}
|
||||
</p>
|
||||
<p className="text-sm text-slate-600">BSI Standards</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-3 gap-6">
|
||||
{/* Regulations List */}
|
||||
<div className="lg:col-span-1 space-y-4">
|
||||
<h2 className="text-lg font-semibold text-slate-900">Verordnungen & Standards</h2>
|
||||
|
||||
{loading ? (
|
||||
<div className="flex justify-center py-8">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2 max-h-[70vh] overflow-y-auto pr-2">
|
||||
{regulations.map((reg) => (
|
||||
<div
|
||||
key={reg.id}
|
||||
onClick={() => setSelectedRegulation(reg.code)}
|
||||
className={`p-4 rounded-lg border cursor-pointer transition-colors ${
|
||||
selectedRegulation === reg.code
|
||||
? 'border-purple-500 bg-purple-50'
|
||||
: 'border-slate-200 hover:border-slate-300 bg-white'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-mono font-bold text-purple-600">{reg.code}</span>
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${
|
||||
reg.regulation_type === 'eu_regulation' ? 'bg-blue-100 text-blue-700' :
|
||||
reg.regulation_type === 'eu_directive' ? 'bg-indigo-100 text-indigo-700' :
|
||||
reg.regulation_type === 'bsi_standard' ? 'bg-orange-100 text-orange-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{REGULATION_TYPE_LABELS[reg.regulation_type] || reg.regulation_type}
|
||||
</span>
|
||||
</div>
|
||||
<h3 className="font-medium text-slate-900 text-sm">{reg.name}</h3>
|
||||
<p className="text-xs text-slate-500 mt-1 line-clamp-2">{reg.description}</p>
|
||||
|
||||
<div className="flex items-center justify-between mt-3">
|
||||
<span className="text-xs text-slate-600">
|
||||
{reg.requirement_count || 0} Anforderungen
|
||||
</span>
|
||||
{reg.source_url && (
|
||||
<a
|
||||
href={reg.source_url}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
className="text-xs text-purple-600 hover:text-purple-700 flex items-center gap-1"
|
||||
>
|
||||
<svg className="w-3 h-3" 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>
|
||||
Original
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Requirements List */}
|
||||
<div className="lg:col-span-2 space-y-4">
|
||||
{!selectedRegulation ? (
|
||||
<div className="bg-white rounded-xl shadow-sm border 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={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>
|
||||
<h3 className="text-lg font-medium text-slate-900">Waehlen Sie eine Verordnung</h3>
|
||||
<p className="text-slate-500 mt-2">Waehlen Sie eine Verordnung aus der Liste um deren Anforderungen zu sehen.</p>
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{/* Filters */}
|
||||
<div className="flex flex-wrap gap-4 items-center">
|
||||
<input
|
||||
type="text"
|
||||
value={searchQuery}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
placeholder="Suche in Anforderungen..."
|
||||
className="flex-1 min-w-[200px] px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
/>
|
||||
<select
|
||||
value={statusFilter}
|
||||
onChange={(e) => setStatusFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Alle Status</option>
|
||||
<option value="not_started">Nicht begonnen</option>
|
||||
<option value="in_progress">In Arbeit</option>
|
||||
<option value="implemented">Implementiert</option>
|
||||
<option value="verified">Verifiziert</option>
|
||||
<option value="not_applicable">N/A</option>
|
||||
</select>
|
||||
<select
|
||||
value={priorityFilter}
|
||||
onChange={(e) => setPriorityFilter(e.target.value)}
|
||||
className="px-4 py-2 border border-slate-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500"
|
||||
>
|
||||
<option value="">Alle Prioritaeten</option>
|
||||
<option value="1">Kritisch</option>
|
||||
<option value="2">Hoch</option>
|
||||
<option value="3">Mittel</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
{/* Requirements Table */}
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
{requirementsLoading ? (
|
||||
<div className="flex justify-center py-12">
|
||||
<div className="animate-spin rounded-full h-8 w-8 border-b-2 border-purple-600" />
|
||||
</div>
|
||||
) : filteredRequirements.length === 0 ? (
|
||||
<div className="text-center py-12 text-slate-500">
|
||||
{requirements.length === 0 ? (
|
||||
<>
|
||||
<p className="font-medium">Keine Anforderungen gefunden</p>
|
||||
<p className="text-sm mt-2">Starten Sie den Scraper um Anforderungen zu extrahieren.</p>
|
||||
<button
|
||||
onClick={() => {/* TODO: Trigger scraper */}}
|
||||
className="mt-4 px-4 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700"
|
||||
>
|
||||
Anforderungen extrahieren
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<p>Keine Anforderungen entsprechen den Filterkriterien</p>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="divide-y divide-slate-200">
|
||||
{filteredRequirements.map((req) => {
|
||||
const statusConfig = STATUS_CONFIG[req.implementation_status] || STATUS_CONFIG.not_started
|
||||
const priorityConfig = PRIORITY_CONFIG[req.priority] || PRIORITY_CONFIG[2]
|
||||
|
||||
return (
|
||||
<div
|
||||
key={req.id}
|
||||
className={`p-4 hover:bg-slate-50 cursor-pointer transition-colors ${
|
||||
selectedRequirement?.id === req.id ? 'bg-purple-50' : ''
|
||||
}`}
|
||||
onClick={() => setSelectedRequirement(selectedRequirement?.id === req.id ? null : req)}
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-mono text-sm font-medium text-purple-600">
|
||||
{req.article}
|
||||
</span>
|
||||
{req.paragraph && (
|
||||
<span className="text-xs text-slate-500">({req.paragraph})</span>
|
||||
)}
|
||||
<span className={`px-2 py-0.5 text-xs rounded-full ${priorityConfig.bg} ${priorityConfig.text}`}>
|
||||
{priorityConfig.label}
|
||||
</span>
|
||||
</div>
|
||||
<h4 className="font-medium text-slate-900">{req.title}</h4>
|
||||
{req.description && (
|
||||
<p className="text-sm text-slate-600 mt-1 line-clamp-2">{req.description}</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col items-end gap-2 ml-4">
|
||||
<span className={`px-3 py-1 text-xs rounded-full ${statusConfig.bg} ${statusConfig.text}`}>
|
||||
{statusConfig.label}
|
||||
</span>
|
||||
{req.controls_count > 0 && (
|
||||
<span className="text-xs text-slate-500">
|
||||
{req.controls_count} Controls
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Expanded Details */}
|
||||
{selectedRequirement?.id === req.id && (
|
||||
<div className="mt-4 pt-4 border-t border-slate-200 space-y-4">
|
||||
{req.requirement_text && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Originaltext</h5>
|
||||
<p className="text-sm text-slate-700 bg-slate-50 p-3 rounded-lg">
|
||||
{req.requirement_text}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{req.breakpilot_interpretation && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Breakpilot Interpretation</h5>
|
||||
<p className="text-sm text-slate-700 bg-purple-50 p-3 rounded-lg border border-purple-100">
|
||||
{req.breakpilot_interpretation}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{req.implementation_details && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Implementation</h5>
|
||||
<p className="text-sm text-slate-700 bg-green-50 p-3 rounded-lg border border-green-100">
|
||||
{req.implementation_details}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{req.code_references && req.code_references.length > 0 && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Code-Referenzen</h5>
|
||||
<div className="space-y-1">
|
||||
{req.code_references.map((ref, idx) => (
|
||||
<div key={idx} className="text-sm font-mono bg-slate-100 p-2 rounded">
|
||||
<span className="text-purple-600">{ref.file}</span>
|
||||
{ref.line && <span className="text-slate-500">:{ref.line}</span>}
|
||||
{ref.description && (
|
||||
<span className="text-slate-600 ml-2">- {ref.description}</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{req.evidence_description && (
|
||||
<div>
|
||||
<h5 className="text-xs font-medium text-slate-500 uppercase mb-1">Nachweis</h5>
|
||||
<p className="text-sm text-slate-700">{req.evidence_description}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex gap-2">
|
||||
<button className="px-3 py-1.5 text-sm bg-purple-600 text-white rounded-lg hover:bg-purple-700">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm border border-purple-600 text-purple-600 rounded-lg hover:bg-purple-50">
|
||||
Mit Claude interpretieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Summary */}
|
||||
{requirements.length > 0 && (
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-sm text-slate-600">
|
||||
<p>
|
||||
<strong>{filteredRequirements.length}</strong> von <strong>{requirements.length}</strong> Anforderungen angezeigt
|
||||
{statusFilter && <span> (Status: {STATUS_CONFIG[statusFilter]?.label})</span>}
|
||||
{priorityFilter && <span> (Prioritaet: {PRIORITY_CONFIG[parseInt(priorityFilter)]?.label})</span>}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+123
-33
@@ -11,7 +11,7 @@
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import AdminLayout from '@/components/admin/AdminLayout'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface Risk {
|
||||
id: string
|
||||
@@ -45,7 +45,12 @@ const RISK_BG_COLORS: Record<string, string> = {
|
||||
critical: 'bg-red-100 border-red-300',
|
||||
}
|
||||
|
||||
const STATUS_OPTIONS = ['open', 'mitigated', 'accepted', 'transferred']
|
||||
const STATUS_OPTIONS = [
|
||||
{ value: 'open', label: 'Offen' },
|
||||
{ value: 'mitigated', label: 'Mitigiert' },
|
||||
{ value: 'accepted', label: 'Akzeptiert' },
|
||||
{ value: 'transferred', label: 'Transferiert' },
|
||||
]
|
||||
|
||||
const CATEGORY_OPTIONS = [
|
||||
{ value: 'data_breach', label: 'Datenschutzverletzung' },
|
||||
@@ -71,6 +76,7 @@ export default function RisksPage() {
|
||||
const [selectedRisk, setSelectedRisk] = useState<Risk | null>(null)
|
||||
const [editModalOpen, setEditModalOpen] = useState(false)
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false)
|
||||
const [saving, setSaving] = useState(false)
|
||||
|
||||
const [formData, setFormData] = useState({
|
||||
risk_id: '',
|
||||
@@ -87,8 +93,6 @@ export default function RisksPage() {
|
||||
residual_impact: null as number | null,
|
||||
})
|
||||
|
||||
const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || 'http://localhost:8000'
|
||||
|
||||
useEffect(() => {
|
||||
loadRisks()
|
||||
}, [])
|
||||
@@ -96,7 +100,7 @@ export default function RisksPage() {
|
||||
const loadRisks = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/risks`)
|
||||
const res = await fetch(`/api/admin/compliance/risks`)
|
||||
if (res.ok) {
|
||||
const data = await res.json()
|
||||
setRisks(data.risks || [])
|
||||
@@ -146,8 +150,9 @@ export default function RisksPage() {
|
||||
}
|
||||
|
||||
const handleCreate = async () => {
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/risks`, {
|
||||
const res = await fetch(`/api/admin/compliance/risks`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -173,14 +178,17 @@ export default function RisksPage() {
|
||||
} catch (error) {
|
||||
console.error('Create failed:', error)
|
||||
alert('Fehler beim Erstellen')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
const handleUpdate = async () => {
|
||||
if (!selectedRisk) return
|
||||
|
||||
setSaving(true)
|
||||
try {
|
||||
const res = await fetch(`${BACKEND_URL}/api/v1/compliance/risks/${selectedRisk.risk_id}`, {
|
||||
const res = await fetch(`/api/admin/compliance/risks/${selectedRisk.risk_id}`, {
|
||||
method: 'PUT',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
@@ -208,6 +216,8 @@ export default function RisksPage() {
|
||||
} catch (error) {
|
||||
console.error('Update failed:', error)
|
||||
alert('Fehler beim Aktualisieren')
|
||||
} finally {
|
||||
setSaving(false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -228,6 +238,17 @@ export default function RisksPage() {
|
||||
return matrix
|
||||
}
|
||||
|
||||
// Statistics
|
||||
const stats = {
|
||||
total: risks.length,
|
||||
critical: risks.filter(r => r.inherent_risk === 'critical').length,
|
||||
high: risks.filter(r => r.inherent_risk === 'high').length,
|
||||
medium: risks.filter(r => r.inherent_risk === 'medium').length,
|
||||
low: risks.filter(r => r.inherent_risk === 'low').length,
|
||||
open: risks.filter(r => r.status === 'open').length,
|
||||
mitigated: risks.filter(r => r.status === 'mitigated').length,
|
||||
}
|
||||
|
||||
const renderMatrix = () => {
|
||||
const matrix = buildMatrix()
|
||||
|
||||
@@ -309,7 +330,7 @@ export default function RisksPage() {
|
||||
const renderList = () => (
|
||||
<div className="bg-white rounded-xl shadow-sm border overflow-hidden">
|
||||
<table className="w-full">
|
||||
<thead className="bg-slate-50">
|
||||
<thead className="bg-slate-50 border-b">
|
||||
<tr>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">ID</th>
|
||||
<th className="px-4 py-3 text-left text-xs font-medium text-slate-500 uppercase">Titel</th>
|
||||
@@ -351,9 +372,10 @@ export default function RisksPage() {
|
||||
<span className={`px-2 py-1 text-xs rounded-full ${
|
||||
risk.status === 'mitigated' ? 'bg-green-100 text-green-700' :
|
||||
risk.status === 'accepted' ? 'bg-blue-100 text-blue-700' :
|
||||
risk.status === 'transferred' ? 'bg-purple-100 text-purple-700' :
|
||||
'bg-slate-100 text-slate-700'
|
||||
}`}>
|
||||
{risk.status}
|
||||
{STATUS_OPTIONS.find(s => s.value === risk.status)?.label || risk.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-3 text-right">
|
||||
@@ -427,7 +449,7 @@ export default function RisksPage() {
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
>
|
||||
{STATUS_OPTIONS.map((s) => (
|
||||
<option key={s} value={s}>{s}</option>
|
||||
<option key={s.value} value={s.value}>{s.label}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
@@ -484,6 +506,7 @@ export default function RisksPage() {
|
||||
type="text"
|
||||
value={formData.owner}
|
||||
onChange={(e) => setFormData({ ...formData, owner: e.target.value })}
|
||||
placeholder="z.B. CISO, DSB"
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
</div>
|
||||
@@ -493,6 +516,7 @@ export default function RisksPage() {
|
||||
<textarea
|
||||
value={formData.treatment_plan}
|
||||
onChange={(e) => setFormData({ ...formData, treatment_plan: e.target.value })}
|
||||
placeholder="Massnahmen zur Risikominderung..."
|
||||
rows={2}
|
||||
className="w-full px-3 py-2 border rounded-lg focus:ring-2 focus:ring-primary-500"
|
||||
/>
|
||||
@@ -501,21 +525,74 @@ export default function RisksPage() {
|
||||
)
|
||||
|
||||
return (
|
||||
<AdminLayout title="Risk Matrix" description="Risikobewertung & Management">
|
||||
<div className="min-h-screen bg-slate-50 p-6">
|
||||
{/* Header */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-6">
|
||||
<div className="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 className="text-2xl font-bold text-slate-900">Risk Matrix</h1>
|
||||
<p className="text-slate-600">Risikobewertung & Management</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/admin/compliance"
|
||||
className="text-sm text-slate-500 hover:text-slate-700 flex items-center gap-1"
|
||||
href="/compliance/hub"
|
||||
className="flex items-center gap-2 text-slate-600 hover:text-slate-800"
|
||||
>
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M10 19l-7-7m0 0l7-7m-7 7h18" />
|
||||
</svg>
|
||||
Zurueck
|
||||
Compliance Hub
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Risk Matrix"
|
||||
purpose="Die Risikomatrix visualisiert alle identifizierten Compliance- und Sicherheitsrisiken nach Eintrittswahrscheinlichkeit und Auswirkung. Hier werden Risiken bewertet, Behandlungsplaene erstellt und der Mitigationsstatus verfolgt."
|
||||
audience={['CISO', 'DSB', 'Compliance Officer', 'Management']}
|
||||
gdprArticles={['Art. 32 (Risikobewertung)', 'Art. 35 (DSFA)']}
|
||||
architecture={{
|
||||
services: ['Python Backend (FastAPI)', 'compliance_risks Modul'],
|
||||
databases: ['PostgreSQL (compliance_risks Table)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Massnahmen zur Risikominderung' },
|
||||
{ name: 'Audit Checklist', href: '/compliance/audit-checklist', description: 'Anforderungen pruefen' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Statistics Cards */}
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 lg:grid-cols-7 gap-4 mb-6">
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<p className="text-sm text-slate-500">Gesamt</p>
|
||||
<p className="text-2xl font-bold text-slate-900">{stats.total}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-red-200">
|
||||
<p className="text-sm text-red-600">Critical</p>
|
||||
<p className="text-2xl font-bold text-red-700">{stats.critical}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-orange-200">
|
||||
<p className="text-sm text-orange-600">High</p>
|
||||
<p className="text-2xl font-bold text-orange-700">{stats.high}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-yellow-200">
|
||||
<p className="text-sm text-yellow-600">Medium</p>
|
||||
<p className="text-2xl font-bold text-yellow-700">{stats.medium}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-green-200">
|
||||
<p className="text-sm text-green-600">Low</p>
|
||||
<p className="text-2xl font-bold text-green-700">{stats.low}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-slate-200">
|
||||
<p className="text-sm text-slate-500">Offen</p>
|
||||
<p className="text-2xl font-bold text-slate-700">{stats.open}</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl p-4 border border-blue-200">
|
||||
<p className="text-sm text-blue-600">Mitigiert</p>
|
||||
<p className="text-2xl font-bold text-blue-700">{stats.mitigated}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* View Toggle & Actions */}
|
||||
<div className="flex flex-wrap items-center gap-4 mb-6">
|
||||
{/* View Toggle */}
|
||||
<div className="flex bg-slate-100 rounded-lg p-1">
|
||||
<button
|
||||
@@ -536,6 +613,8 @@ export default function RisksPage() {
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="flex-1" />
|
||||
|
||||
<button
|
||||
onClick={openCreateModal}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
@@ -570,22 +649,28 @@ export default function RisksPage() {
|
||||
|
||||
{/* Create Modal */}
|
||||
{createModalOpen && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">Neues Risiko</h3>
|
||||
{renderForm(true)}
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Neues Risiko</h3>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{renderForm(true)}
|
||||
</div>
|
||||
<div className="p-6 border-t bg-slate-50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setCreateModalOpen(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
disabled={saving}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleCreate}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
Erstellen
|
||||
{saving ? 'Erstellen...' : 'Erstellen'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -594,29 +679,34 @@ export default function RisksPage() {
|
||||
|
||||
{/* Edit Modal */}
|
||||
{editModalOpen && selectedRisk && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg p-6 max-h-[90vh] overflow-y-auto">
|
||||
<h3 className="text-lg font-semibold text-slate-900 mb-4">
|
||||
Risiko bearbeiten: {selectedRisk.risk_id}
|
||||
</h3>
|
||||
{renderForm(false)}
|
||||
<div className="flex justify-end gap-3 mt-6">
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50 p-4">
|
||||
<div className="bg-white rounded-xl shadow-xl w-full max-w-lg max-h-[90vh] overflow-y-auto">
|
||||
<div className="p-6 border-b">
|
||||
<h3 className="text-lg font-semibold text-slate-900">Risiko bearbeiten</h3>
|
||||
<p className="text-sm text-slate-500 font-mono">{selectedRisk.risk_id}</p>
|
||||
</div>
|
||||
<div className="p-6">
|
||||
{renderForm(false)}
|
||||
</div>
|
||||
<div className="p-6 border-t bg-slate-50 flex justify-end gap-3">
|
||||
<button
|
||||
onClick={() => setEditModalOpen(false)}
|
||||
className="px-4 py-2 text-slate-600 hover:text-slate-800"
|
||||
disabled={saving}
|
||||
>
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={handleUpdate}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700"
|
||||
disabled={saving}
|
||||
className="px-4 py-2 bg-primary-600 text-white rounded-lg hover:bg-primary-700 disabled:opacity-50"
|
||||
>
|
||||
Speichern
|
||||
{saving ? 'Speichern...' : 'Speichern'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</AdminLayout>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,804 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Source Policy Management Page
|
||||
*
|
||||
* Whitelist-based data source management for edu-search-service.
|
||||
* For auditors: Full audit trail for all changes.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
import { SourcesTab } from './components/SourcesTab'
|
||||
import { OperationsMatrixTab } from './components/OperationsMatrixTab'
|
||||
import { PIIRulesTab } from './components/PIIRulesTab'
|
||||
import { AuditTab } from './components/AuditTab'
|
||||
|
||||
// API base URL for edu-search-service
|
||||
// Uses nginx HTTPS proxy on port 8089 when accessed remotely
|
||||
const getApiBase = () => {
|
||||
if (typeof window === 'undefined') return 'http://localhost:8088'
|
||||
const hostname = window.location.hostname
|
||||
if (hostname === 'localhost' || hostname === '127.0.0.1') {
|
||||
return 'http://localhost:8088'
|
||||
}
|
||||
// Use nginx HTTPS proxy on port 8089 (proxies to edu-search-service:8088)
|
||||
return `https://${hostname}:8089`
|
||||
}
|
||||
|
||||
interface PolicyStats {
|
||||
active_policies: number
|
||||
allowed_sources: number
|
||||
pii_rules: number
|
||||
blocked_today: number
|
||||
blocked_total: number
|
||||
}
|
||||
|
||||
type TabId = 'dashboard' | 'sources' | 'operations' | 'pii' | 'audit'
|
||||
|
||||
export default function SourcePolicyPage() {
|
||||
const [activeTab, setActiveTab] = useState<TabId>('dashboard')
|
||||
const [stats, setStats] = useState<PolicyStats | null>(null)
|
||||
const [loading, setLoading] = useState(true)
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [apiBase, setApiBase] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
// Set API base on client side - only runs in browser
|
||||
const base = getApiBase()
|
||||
setApiBase(base)
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
// Only fetch when apiBase has been set by the first useEffect
|
||||
if (apiBase !== null) {
|
||||
fetchStats()
|
||||
}
|
||||
}, [apiBase])
|
||||
|
||||
const fetchStats = async () => {
|
||||
try {
|
||||
setLoading(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/policy-stats`)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new Error('Fehler beim Laden der Statistiken')
|
||||
}
|
||||
|
||||
const data = await res.json()
|
||||
setStats(data)
|
||||
} catch (err) {
|
||||
setError(err instanceof Error ? err.message : 'Unbekannter Fehler')
|
||||
// Set default stats on error
|
||||
setStats({
|
||||
active_policies: 0,
|
||||
allowed_sources: 0,
|
||||
pii_rules: 0,
|
||||
blocked_today: 0,
|
||||
blocked_total: 0,
|
||||
})
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
const tabs: { id: TabId; name: string; icon: JSX.Element }[] = [
|
||||
{
|
||||
id: 'dashboard',
|
||||
name: 'Dashboard',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2V6zM14 6a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2V6zM4 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2H6a2 2 0 01-2-2v-2zM14 16a2 2 0 012-2h2a2 2 0 012 2v2a2 2 0 01-2 2h-2a2 2 0 01-2-2v-2z" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'sources',
|
||||
name: 'Quellen',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'operations',
|
||||
name: 'Operations',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-6 9l2 2 4-4" />
|
||||
</svg>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'pii',
|
||||
name: 'PII-Regeln',
|
||||
icon: (
|
||||
<svg className="w-4 h-4" 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>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: 'audit',
|
||||
name: 'Audit',
|
||||
icon: (
|
||||
<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>
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
return (
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Quellen-Policy"
|
||||
purpose="Whitelist-basiertes Datenquellen-Management fuer das Bildungssuch-System. Nur offizielle Open-Data-Portale und amtliche Quellen (§5 UrhG). Training mit externen Daten ist VERBOTEN. Fuer Auditoren pruefbar mit vollstaendigem Audit-Trail."
|
||||
audience={['DSB', 'Compliance Officer', 'Auditor']}
|
||||
gdprArticles={[
|
||||
'Art. 5 (Rechtmaessigkeit)',
|
||||
'Art. 6 (Rechtsgrundlage)',
|
||||
'Art. 24 (Verantwortung)',
|
||||
]}
|
||||
architecture={{
|
||||
services: ['edu-search-service (Go)', 'PostgreSQL'],
|
||||
databases: ['source_policies', 'allowed_sources', 'pii_rules', 'policy_audit_log'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Audit Report', href: '/compliance/audit-report', description: 'Compliance-Berichte' },
|
||||
{ name: 'Controls', href: '/compliance/controls', description: 'Technische Kontrollen' },
|
||||
{ name: 'Education Search', href: '/education/edu-search', description: 'Bildungsquellen' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Error Display */}
|
||||
{error && (
|
||||
<div className="mb-4 p-4 bg-red-50 border border-red-200 rounded-lg text-red-700 flex items-center justify-between">
|
||||
<span>{error}</span>
|
||||
<button onClick={() => setError(null)} className="text-red-500 hover:text-red-700">
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Stats Cards */}
|
||||
{stats && (
|
||||
<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">
|
||||
<div className="text-2xl font-bold text-purple-600">{stats.active_policies}</div>
|
||||
<div className="text-sm text-slate-500">Aktive Policies</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-green-600">{stats.allowed_sources}</div>
|
||||
<div className="text-sm text-slate-500">Zugelassene Quellen</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-red-600">{stats.blocked_today}</div>
|
||||
<div className="text-sm text-slate-500">Blockiert (heute)</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="text-2xl font-bold text-blue-600">{stats.pii_rules}</div>
|
||||
<div className="text-sm text-slate-500">PII-Regeln</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div className="flex gap-2 mb-6 flex-wrap">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
key={tab.id}
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`px-4 py-2 rounded-lg font-medium transition-colors flex items-center gap-2 ${
|
||||
activeTab === tab.id
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{tab.icon}
|
||||
{tab.name}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Tab Content */}
|
||||
{apiBase === null ? (
|
||||
<div className="text-center py-12 text-slate-500">Initialisiere...</div>
|
||||
) : (
|
||||
<>
|
||||
{activeTab === 'dashboard' && (
|
||||
<DashboardTab stats={stats} loading={loading} apiBase={apiBase} />
|
||||
)}
|
||||
|
||||
{activeTab === 'sources' && <SourcesTab apiBase={apiBase} onUpdate={fetchStats} />}
|
||||
|
||||
{activeTab === 'operations' && <OperationsMatrixTab apiBase={apiBase} />}
|
||||
|
||||
{activeTab === 'pii' && <PIIRulesTab apiBase={apiBase} onUpdate={fetchStats} />}
|
||||
|
||||
{activeTab === 'audit' && <AuditTab apiBase={apiBase} />}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
// Dashboard Tab Component
|
||||
function DashboardTab({
|
||||
stats,
|
||||
loading,
|
||||
apiBase,
|
||||
}: {
|
||||
stats: PolicyStats | null
|
||||
loading: boolean
|
||||
apiBase: string
|
||||
}) {
|
||||
const [complianceCheck, setComplianceCheck] = useState({
|
||||
url: '',
|
||||
operation: 'lookup',
|
||||
})
|
||||
const [checkResult, setCheckResult] = useState<any>(null)
|
||||
const [checking, setChecking] = useState(false)
|
||||
|
||||
const runComplianceCheck = async () => {
|
||||
if (!complianceCheck.url) return
|
||||
|
||||
try {
|
||||
setChecking(true)
|
||||
const res = await fetch(`${apiBase}/v1/admin/check-compliance`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(complianceCheck),
|
||||
})
|
||||
|
||||
const data = await res.json()
|
||||
setCheckResult(data)
|
||||
} catch (err) {
|
||||
setCheckResult({ error: 'Fehler bei der Pruefung' })
|
||||
} finally {
|
||||
setChecking(false)
|
||||
}
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return <div className="text-center py-12 text-slate-500">Lade Dashboard...</div>
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Important Notice */}
|
||||
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
|
||||
<div className="flex items-start gap-4">
|
||||
<div className="w-10 h-10 rounded-full bg-red-100 flex items-center justify-center flex-shrink-0">
|
||||
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="font-semibold text-red-800">Training mit externen Daten: VERBOTEN</h3>
|
||||
<p className="text-sm text-red-700 mt-1">
|
||||
Gemaess unserer Datenschutz-Policy ist das Training von KI-Modellen mit gecrawlten Daten
|
||||
strengstens untersagt. Diese Einschraenkung kann nicht ueber die UI geaendert werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Quick Compliance Check */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Schnell-Pruefung</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Pruefen Sie, ob eine URL in der Whitelist enthalten ist und welche Operationen erlaubt sind.
|
||||
</p>
|
||||
|
||||
<div className="flex flex-col md:flex-row gap-4">
|
||||
<input
|
||||
type="url"
|
||||
value={complianceCheck.url}
|
||||
onChange={(e) => setComplianceCheck({ ...complianceCheck, url: e.target.value })}
|
||||
placeholder="https://nibis.de/beispiel-seite"
|
||||
className="flex-1 px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
/>
|
||||
<select
|
||||
value={complianceCheck.operation}
|
||||
onChange={(e) => setComplianceCheck({ ...complianceCheck, operation: e.target.value })}
|
||||
className="px-4 py-2 border border-slate-200 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent"
|
||||
>
|
||||
<option value="lookup">Lookup (Anzeigen)</option>
|
||||
<option value="rag">RAG (Retrieval)</option>
|
||||
<option value="export">Export</option>
|
||||
<option value="training">Training</option>
|
||||
</select>
|
||||
<button
|
||||
onClick={runComplianceCheck}
|
||||
disabled={checking || !complianceCheck.url}
|
||||
className="px-6 py-2 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50"
|
||||
>
|
||||
{checking ? 'Pruefe...' : 'Pruefen'}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{checkResult && (
|
||||
<div className={`mt-4 p-4 rounded-lg ${checkResult.is_allowed ? 'bg-green-50 border border-green-200' : 'bg-red-50 border border-red-200'}`}>
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
{checkResult.is_allowed ? (
|
||||
<>
|
||||
<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="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
<span className="font-medium text-green-800">Erlaubt</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<svg className="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
<span className="font-medium text-red-800">
|
||||
Blockiert: {checkResult.block_reason || 'Nicht in Whitelist'}
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{checkResult.source && (
|
||||
<div className="text-sm text-slate-600">
|
||||
<p><strong>Quelle:</strong> {checkResult.source.name}</p>
|
||||
<p><strong>Lizenz:</strong> {checkResult.license}</p>
|
||||
{checkResult.requires_citation && (
|
||||
<p className="text-amber-600">Zitation erforderlich</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Operations Matrix by Source Type */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Zulaessige Operationen nach Quellentyp</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Uebersicht welche Operationen fuer welche Datenquellen erlaubt sind.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Quelle / Typ</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Daten</th>
|
||||
<th className="text-center py-2 px-2 font-medium text-slate-700">Lookup</th>
|
||||
<th className="text-center py-2 px-2 font-medium text-slate-700">RAG</th>
|
||||
<th className="text-center py-2 px-2 font-medium text-slate-700">Training</th>
|
||||
<th className="text-center py-2 px-2 font-medium text-slate-700">Export</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Rechtsgrundlage</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Auflagen / Controls</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{ source: 'Landes-Open-Data-Portale (alle Laender)', data: 'SMD', lookup: 'yes', rag: 'yes', training: 'no', export: 'warn', basis: 'DL-DE-BY-2.0', note: 'Namensnennung, Quellenlink, Zweckbindung' },
|
||||
{ source: 'Landes-Open-Data-Portale', data: 'PBD', lookup: 'no', rag: 'no', training: 'no', export: 'no', basis: '—', note: 'Technisch filtern (Schema-Block)' },
|
||||
{ source: 'Regelwerke / Schulordnungen (Ministerien)', data: 'DOK', lookup: 'yes', rag: 'yes', training: 'warn', export: 'warn', basis: 'UrhG §5 / CC / DL', note: 'Nur amtliche Texte, Versions-Hash' },
|
||||
{ source: 'GovData', data: 'SMD', lookup: 'yes', rag: 'yes', training: 'no', export: 'warn', basis: 'DL-DE-BY-2.0', note: 'Bundesweiter Fallback' },
|
||||
{ source: 'Einzelschul-Websites', data: 'SMD', lookup: 'warn', rag: 'no', training: 'no', export: 'no', basis: '§60d greift nicht', note: 'Nur manuell, kein Crawling' },
|
||||
{ source: 'Private Schulverzeichnisse', data: 'SMD', lookup: 'no', rag: 'no', training: 'no', export: 'no', basis: 'Datenbankrecht', note: 'Nicht zulaessig' },
|
||||
{ source: 'Vom Lehrer eingegebene Daten', data: 'SMD', lookup: 'yes', rag: 'yes', training: 'warn', export: 'warn', basis: 'Art. 6(1)b DSGVO', note: 'Zweckbindung, Namespace' },
|
||||
{ source: 'Vom Lehrer hochgeladene Dokumente', data: 'DOK', lookup: 'yes', rag: 'yes', training: 'no', export: 'no', basis: 'Art. 6(1)b DSGVO', note: 'Kein Training, nur Session-RAG' },
|
||||
].map((row, idx) => (
|
||||
<tr key={idx} className={`border-b border-slate-100 hover:bg-slate-50 ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/30'}`}>
|
||||
<td className="py-2 px-3 font-medium text-slate-800 text-xs">{row.source}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-1.5 py-0.5 rounded text-xs ${
|
||||
row.data === 'SMD' ? 'bg-blue-100 text-blue-700' : row.data === 'PBD' ? 'bg-red-100 text-red-700' : 'bg-purple-100 text-purple-700'
|
||||
}`}>{row.data}</span>
|
||||
</td>
|
||||
<td className="py-2 px-2 text-center">{row.lookup === 'yes' ? '✅' : row.lookup === 'warn' ? '⚠️' : '❌'}</td>
|
||||
<td className="py-2 px-2 text-center">{row.rag === 'yes' ? '✅' : row.rag === 'warn' ? '⚠️' : '❌'}</td>
|
||||
<td className="py-2 px-2 text-center">{row.training === 'yes' ? '✅' : row.training === 'warn' ? '⚠️' : '❌'}</td>
|
||||
<td className="py-2 px-2 text-center">{row.export === 'yes' ? '✅' : row.export === 'warn' ? '⚠️' : '❌'}</td>
|
||||
<td className="py-2 px-3 text-xs">
|
||||
<span className={`px-1.5 py-0.5 rounded ${
|
||||
row.basis === '—' ? 'bg-slate-100 text-slate-500' :
|
||||
row.basis.includes('DSGVO') ? 'bg-blue-100 text-blue-700' :
|
||||
row.basis.includes('DL-DE') ? 'bg-green-100 text-green-700' :
|
||||
row.basis.includes('UrhG') || row.basis.includes('CC') ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>{row.basis}</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-600 text-xs">{row.note}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Legend and Explanation */}
|
||||
<div className="mt-4 p-4 bg-slate-50 rounded-lg">
|
||||
<h4 className="font-medium text-slate-800 mb-3">Geltungsbereich der Matrix</h4>
|
||||
|
||||
{/* Datenarten */}
|
||||
<div className="mb-4">
|
||||
<div className="text-sm font-medium text-slate-700 mb-2">Datenarten</div>
|
||||
<div className="flex flex-wrap gap-3 text-xs">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 bg-blue-100 text-blue-700 rounded">SMD</span>
|
||||
<span className="text-slate-600">= Schul-Metadaten (Name, Nummer, Schulform, Ort, Traeger)</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 bg-red-100 text-red-700 rounded">PBD</span>
|
||||
<span className="text-slate-600">= Personenbezogene Daten (Leitung, E-Mail, Telefon)</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<span className="px-1.5 py-0.5 bg-purple-100 text-purple-700 rounded">DOK</span>
|
||||
<span className="text-slate-600">= Regelwerke / Ordnungen / Lehrplaene</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Verarbeitungsarten mit aufklappbarer Erklärung */}
|
||||
<details className="group">
|
||||
<summary className="cursor-pointer text-sm font-medium text-slate-700 mb-2 flex items-center gap-2 hover:text-purple-600">
|
||||
<svg className="w-4 h-4 transition-transform group-open:rotate-90" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5l7 7-7 7" />
|
||||
</svg>
|
||||
Verarbeitungsarten (Details anzeigen)
|
||||
</summary>
|
||||
<div className="ml-6 mt-2 space-y-3 text-sm">
|
||||
<div className="p-3 bg-white rounded border border-slate-200">
|
||||
<div className="font-medium text-slate-800 flex items-center gap-2">
|
||||
<span className="text-green-600">Lookup</span>
|
||||
<span className="text-slate-400">=</span>
|
||||
<span className="text-slate-600">Auswahl / Validierung / Anzeige</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Daten werden abgerufen und dem Nutzer angezeigt, z.B. bei der Schulauswahl im Onboarding
|
||||
oder zur Validierung eingegebener Schulnummern. Keine dauerhafte Speicherung oder Weiterverarbeitung.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded border border-slate-200">
|
||||
<div className="font-medium text-slate-800 flex items-center gap-2">
|
||||
<span className="text-blue-600">RAG</span>
|
||||
<span className="text-slate-400">=</span>
|
||||
<span className="text-slate-600">Retrieval-Index (Kontext, Zitierquelle)</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Daten werden in einen Vektor-Index aufgenommen und koennen als Kontext fuer KI-Antworten
|
||||
herangezogen werden. Die Quelle wird zitiert. Keine Veraenderung der Modelgewichte.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded border border-slate-200">
|
||||
<div className="font-medium text-slate-800 flex items-center gap-2">
|
||||
<span className="text-red-600">Training</span>
|
||||
<span className="text-slate-400">=</span>
|
||||
<span className="text-slate-600">Modellanpassung / Fine-Tuning</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Daten fliessen in das Training oder Fine-Tuning eines KI-Modells ein und veraendern
|
||||
dessen Gewichte permanent. <strong className="text-red-600">Grundsaetzlich VERBOTEN</strong> fuer
|
||||
externe Daten gemaess unserer Datenschutz-Policy.
|
||||
</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded border border-slate-200">
|
||||
<div className="font-medium text-slate-800 flex items-center gap-2">
|
||||
<span className="text-amber-600">Export</span>
|
||||
<span className="text-slate-400">=</span>
|
||||
<span className="text-slate-600">Weitergabe / Download / API</span>
|
||||
</div>
|
||||
<p className="text-xs text-slate-500 mt-1">
|
||||
Daten werden an Dritte weitergegeben, zum Download bereitgestellt oder ueber eine API
|
||||
ausgegeben. Erfordert Pruefung der Lizenzbedingungen und ggf. Namensnennung.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* KI Use-Case Risk Matrix */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">KI-Use-Case Risikomatrix</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Zulaessigkeit von KI-Anwendungsfaellen nach Datenquelle.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200 bg-slate-50">
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">KI-Use-Case</th>
|
||||
<th className="text-center py-2 px-3 font-medium text-slate-700">Open-Data SMD</th>
|
||||
<th className="text-center py-2 px-3 font-medium text-slate-700">Regelwerke</th>
|
||||
<th className="text-center py-2 px-3 font-medium text-slate-700">Lehrer-Uploads</th>
|
||||
<th className="text-center py-2 px-3 font-medium text-slate-700">Risiko</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{ useCase: 'Schul-Auswahl / Onboarding', openData: 'yes', rules: 'na', uploads: 'na', risk: 'low' },
|
||||
{ useCase: 'Erwartungshorizont-Suche', openData: 'na', rules: 'yes', uploads: 'warn', risk: 'medium' },
|
||||
{ useCase: 'Klausur-Korrektur (RAG)', openData: 'na', rules: 'warn', uploads: 'yes', risk: 'medium' },
|
||||
{ useCase: 'Modell-Training', openData: 'no', rules: 'warn', uploads: 'no', risk: 'high' },
|
||||
{ useCase: 'Auto-Schulerkennung', openData: 'no', rules: 'no', uploads: 'no', risk: 'high' },
|
||||
].map((row, idx) => (
|
||||
<tr key={idx} className={`border-b border-slate-100 hover:bg-slate-50 ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/30'}`}>
|
||||
<td className="py-2 px-3 font-medium text-slate-800">{row.useCase}</td>
|
||||
<td className="py-2 px-3 text-center">{row.openData === 'yes' ? '✅' : row.openData === 'warn' ? '⚠️' : row.openData === 'no' ? '❌' : '—'}</td>
|
||||
<td className="py-2 px-3 text-center">{row.rules === 'yes' ? '✅' : row.rules === 'warn' ? '⚠️' : row.rules === 'no' ? '❌' : '—'}</td>
|
||||
<td className="py-2 px-3 text-center">{row.uploads === 'yes' ? '✅' : row.uploads === 'warn' ? '⚠️' : row.uploads === 'no' ? '❌' : '—'}</td>
|
||||
<td className="py-2 px-3 text-center">
|
||||
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
|
||||
row.risk === 'low' ? 'bg-green-100 text-green-700' :
|
||||
row.risk === 'medium' ? 'bg-amber-100 text-amber-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{row.risk === 'low' ? 'Niedrig' : row.risk === 'medium' ? 'Mittel' : 'Hoch'}
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Licenses Info */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Unterstuetzte Lizenzen</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="font-medium text-slate-800">DL-DE-BY-2.0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Datenlizenz Deutschland - Namensnennung</div>
|
||||
<div className="text-xs text-green-600 mt-2">Attribution erforderlich</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="font-medium text-slate-800">CC-BY</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Creative Commons Attribution</div>
|
||||
<div className="text-xs text-green-600 mt-2">Attribution erforderlich</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="font-medium text-slate-800">CC-BY-SA</div>
|
||||
<div className="text-xs text-slate-500 mt-1">CC Attribution-ShareAlike</div>
|
||||
<div className="text-xs text-amber-600 mt-2">Attribution + ShareAlike</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="font-medium text-slate-800">CC0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Public Domain</div>
|
||||
<div className="text-xs text-slate-400 mt-2">Keine Attribution noetig</div>
|
||||
</div>
|
||||
<div className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="font-medium text-slate-800">§5 UrhG</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Amtliche Werke</div>
|
||||
<div className="text-xs text-green-600 mt-2">Quellenangabe erforderlich</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Technische Controls fuer Attribution */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Technische Controls fuer Attribution</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Massnahmen zur Sicherstellung der lizenzkonformen Quellenangabe im System.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
id: 'CTRL-SRC-001',
|
||||
name: 'Attribution bei Schulsuche',
|
||||
description: 'Bei jedem Suchergebnis aus Open-Data-Portalen wird die Datenquelle, Lizenz und ein Link zum Bereitsteller angezeigt.',
|
||||
status: 'implemented',
|
||||
location: 'studio-v2/components/SchoolSearch.tsx',
|
||||
},
|
||||
{
|
||||
id: 'CTRL-SRC-002',
|
||||
name: 'Attribution bei RAG-Ergebnissen',
|
||||
description: 'Pro EH-Vorschlag werden Dokumentname, Herausgeber und Lizenz angezeigt. Bei Einfuegen in Gutachten wird Zitation automatisch ergaenzt.',
|
||||
status: 'implemented',
|
||||
location: 'studio-v2/components/korrektur/EHSuggestionPanel.tsx',
|
||||
},
|
||||
{
|
||||
id: 'CTRL-SRC-003',
|
||||
name: 'Export-Attribution',
|
||||
description: 'Bei PDF-Export wird ein Quellenverzeichnis am Ende eingefuegt. Bei Daten-Export werden Attribution-Metadaten mitgeliefert.',
|
||||
status: 'planned',
|
||||
location: 'klausur-service/export',
|
||||
},
|
||||
{
|
||||
id: 'CTRL-SRC-004',
|
||||
name: 'Attribution-Audit-Trail',
|
||||
description: 'Logging welche Quellen fuer welche Outputs verwendet wurden. Nachweis fuer Auditoren ueber policy_audit_log.',
|
||||
status: 'planned',
|
||||
location: 'edu-search-service/internal/policy/audit.go',
|
||||
},
|
||||
].map((ctrl) => (
|
||||
<div key={ctrl.id} className="p-4 border border-slate-200 rounded-lg hover:bg-slate-50">
|
||||
<div className="flex items-start justify-between gap-4">
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-mono text-xs px-2 py-0.5 bg-purple-100 text-purple-700 rounded">{ctrl.id}</span>
|
||||
<span className="font-medium text-slate-800">{ctrl.name}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600">{ctrl.description}</p>
|
||||
<p className="text-xs text-slate-400 mt-1 font-mono">{ctrl.location}</p>
|
||||
</div>
|
||||
<span className={`flex-shrink-0 px-2 py-1 rounded text-xs font-medium ${
|
||||
ctrl.status === 'implemented' ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'
|
||||
}`}>
|
||||
{ctrl.status === 'implemented' ? 'Implementiert' : 'Geplant'}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Erlaubte Referenz-Domains */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Erlaubte Referenz-Domains (Audit-Dokumentation)</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Domains, auf die das System zu Referenz- und Compliance-Zwecken zugreifen darf.
|
||||
Diese Zugriffe dienen ausschliesslich der rechtssicheren Klassifikation und Dokumentation.
|
||||
</p>
|
||||
<div className="space-y-3">
|
||||
{[
|
||||
{
|
||||
domain: 'govdata.de',
|
||||
reason: 'Der Zugriff auf govdata.de ist dauerhaft erlaubt, da es sich um ein amtliches Open-Data-Portal mit klarer Lizenz handelt. Die Nutzung erfolgt ausschliesslich zu Recherche- und Referenzzwecken, nicht fuer KI-Training.',
|
||||
type: 'Datenquelle',
|
||||
},
|
||||
{
|
||||
domain: 'creativecommons.org',
|
||||
reason: 'Der Zugriff auf creativecommons.org ist dauerhaft erlaubt, da es sich um offizielle Lizenztexte handelt, die fuer die rechtssichere Klassifikation und Nutzung von Open-Data-Quellen erforderlich sind.',
|
||||
type: 'Lizenz-Referenz',
|
||||
},
|
||||
{
|
||||
domain: 'wiki.creativecommons.org',
|
||||
reason: 'Der Zugriff auf wiki.creativecommons.org ist dauerhaft erlaubt, da es sich um offizielle Lizenzdokumentation handelt, die zur rechtssicheren Klassifikation von Datenquellen erforderlich ist.',
|
||||
type: 'Lizenz-Dokumentation',
|
||||
},
|
||||
{
|
||||
domain: 'gesetze-im-internet.de',
|
||||
reason: 'Der Zugriff auf gesetze-im-internet.de ist dauerhaft erlaubt, da es sich um amtliche, urheberrechtsfreie Rechtsquellen (§5 UrhG) handelt, die zur rechtlichen Einordnung und Compliance-Dokumentation erforderlich sind.',
|
||||
type: 'Rechtsquelle',
|
||||
},
|
||||
{
|
||||
domain: 'nibis.de',
|
||||
reason: 'Der Zugriff auf nibis.de (Niedersaechsischer Bildungsserver) ist dauerhaft erlaubt fuer den Abruf von Kerncurricula und Erwartungshorizonten. Die Nutzung erfolgt unter DL-DE-BY-2.0 mit Attribution.',
|
||||
type: 'Bildungsquelle',
|
||||
},
|
||||
{
|
||||
domain: 'kmk.org',
|
||||
reason: 'Der Zugriff auf kmk.org (Kultusministerkonferenz) ist dauerhaft erlaubt, da KMK-Beschluesse als amtliche Werke nach §5 UrhG frei nutzbar sind. Quellenangabe erforderlich.',
|
||||
type: 'Amtliche Quelle',
|
||||
},
|
||||
].map((item, idx) => (
|
||||
<div key={item.domain} className="p-4 bg-slate-50 rounded-lg border border-slate-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="flex-shrink-0 w-8 h-8 rounded-full bg-green-100 flex items-center justify-center">
|
||||
<svg className="w-4 h-4 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
|
||||
</svg>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<span className="font-mono text-sm font-medium text-slate-800">{item.domain}</span>
|
||||
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">{item.type}</span>
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 italic">"{item.reason}"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-blue-50 border border-blue-200 rounded-lg text-sm text-blue-800">
|
||||
<strong>Fuer Auditoren:</strong> Diese Statements dokumentieren die rechtliche Grundlage fuer den Systemzugriff auf externe Domains.
|
||||
Alle Zugriffe werden im Audit-Log protokolliert.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bundesweite Quellen */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Bundesweite Quellen</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Uebergreifende Open-Data-Portale und amtliche Quellen auf Bundesebene.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Quelle</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Typ</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Lizenz</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Einsatz</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr className="border-b border-slate-100 hover:bg-slate-50">
|
||||
<td className="py-2 px-3 font-medium text-slate-800">GovData</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className="px-2 py-0.5 bg-blue-100 text-blue-700 rounded text-xs">Bund-ODP</span>
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className="px-2 py-0.5 bg-green-100 text-green-700 rounded text-xs">DL-DE-BY-2.0</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-600">Aggregation / Fallback</td>
|
||||
</tr>
|
||||
<tr className="hover:bg-slate-50">
|
||||
<td className="py-2 px-3 font-medium text-slate-800">Statistische Landesaemter</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs">Amtlich</span>
|
||||
</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className="px-2 py-0.5 bg-amber-100 text-amber-700 rounded text-xs">variabel</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-600">Plausibilisierung</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Bundeslaender Open Data Portale */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Bundeslaender Open Data Portale</h3>
|
||||
<p className="text-sm text-slate-600 mb-4">
|
||||
Zulaessige Landes-Open-Data-Portale fuer Schulstammdaten und Bildungsinformationen.
|
||||
</p>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b border-slate-200">
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Bundesland</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Zulaessige Quelle</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700">Lizenz</th>
|
||||
<th className="text-left py-2 px-3 font-medium text-slate-700 hidden md:table-cell">Hinweise</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{[
|
||||
{ bl: 'BW', name: 'Baden-Wuerttemberg', source: 'Open Data Baden-Wuerttemberg', license: 'DL-DE-BY-2.0', note: 'Schulverzeichnisse ueber Ministerium / Kommunen' },
|
||||
{ bl: 'BY', name: 'Bayern', source: 'Open Data Bayern', license: 'DL-DE-BY-2.0', note: 'Amtliche Schulnummern, Standorte' },
|
||||
{ bl: 'BE', name: 'Berlin', source: 'Datenportal Berlin', license: 'CC-BY', note: 'Sehr gut gepflegte Schulstammdaten' },
|
||||
{ bl: 'BB', name: 'Brandenburg', source: 'Daten Brandenburg', license: 'DL-DE-BY-2.0', note: 'Kommunale Ergaenzungen pruefen' },
|
||||
{ bl: 'HB', name: 'Bremen', source: 'Open Data Bremen', license: 'CC-BY', note: 'Kleine Datenmenge, sauber' },
|
||||
{ bl: 'HH', name: 'Hamburg', source: 'Transparenzportal Hamburg', license: 'DL-DE-BY-2.0', note: 'Sehr gute Metadaten' },
|
||||
{ bl: 'HE', name: 'Hessen', source: 'Open Data Hessen', license: 'DL-DE-BY-2.0', note: 'Schultraegerdaten' },
|
||||
{ bl: 'MV', name: 'Mecklenburg-Vorpommern', source: 'Open Data MV', license: 'DL-DE-BY-2.0', note: 'Teilweise CSV/Excel' },
|
||||
{ bl: 'NI', name: 'Niedersachsen', source: 'Open Data Niedersachsen', license: 'DL-DE-BY-2.0', note: 'Ergaenzend: NIBIS nur Regelwerke, nicht Personen' },
|
||||
{ bl: 'NW', name: 'Nordrhein-Westfalen', source: 'Open.NRW', license: 'DL-DE-BY-2.0', note: 'Umfangreich, kommunale Qualitaet pruefen' },
|
||||
{ bl: 'RP', name: 'Rheinland-Pfalz', source: 'Open Data Rheinland-Pfalz', license: 'DL-DE-BY-2.0', note: 'Schulformen & Standorte' },
|
||||
{ bl: 'SL', name: 'Saarland', source: 'Open Data Saarland', license: 'DL-DE-BY-2.0', note: 'Klein, aber zulaessig' },
|
||||
{ bl: 'SN', name: 'Sachsen', source: 'Datenportal Sachsen', license: 'DL-DE-BY-2.0', note: 'Gute Pflege' },
|
||||
{ bl: 'ST', name: 'Sachsen-Anhalt', source: 'Open Data Sachsen-Anhalt', license: 'DL-DE-BY-2.0', note: 'CSV/JSON verfuegbar' },
|
||||
{ bl: 'SH', name: 'Schleswig-Holstein', source: 'Open Data Schleswig-Holstein', license: 'DL-DE-BY-2.0', note: 'Einheitliche IDs' },
|
||||
{ bl: 'TH', name: 'Thueringen', source: 'Open Data Thueringen', license: 'DL-DE-BY-2.0', note: 'Kommunale Ergaenzungen' },
|
||||
].map((item, idx) => (
|
||||
<tr key={item.bl} className={`border-b border-slate-100 hover:bg-slate-50 ${idx % 2 === 0 ? 'bg-white' : 'bg-slate-50/50'}`}>
|
||||
<td className="py-2 px-3">
|
||||
<span className="inline-flex items-center gap-2">
|
||||
<span className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs font-mono">{item.bl}</span>
|
||||
<span className="text-slate-700 hidden sm:inline">{item.name}</span>
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 font-medium text-slate-800">{item.source}</td>
|
||||
<td className="py-2 px-3">
|
||||
<span className={`px-2 py-0.5 rounded text-xs ${
|
||||
item.license === 'CC-BY'
|
||||
? 'bg-blue-100 text-blue-700'
|
||||
: 'bg-green-100 text-green-700'
|
||||
}`}>
|
||||
{item.license}
|
||||
</span>
|
||||
</td>
|
||||
<td className="py-2 px-3 text-slate-500 text-xs hidden md:table-cell">{item.note}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="mt-4 p-3 bg-amber-50 border border-amber-200 rounded-lg text-sm text-amber-800">
|
||||
<strong>Hinweis:</strong> Alle Landes-ODP sind vom Typ "Landes-ODP" und erfordern Attribution gemaess der jeweiligen Lizenz.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,395 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* TOM - Technische und Organisatorische Massnahmen
|
||||
*
|
||||
* Art. 32 DSGVO - Sicherheit der Verarbeitung
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface TOMCategory {
|
||||
id: string
|
||||
title: string
|
||||
article: string
|
||||
description: string
|
||||
measures: {
|
||||
name: string
|
||||
description: string
|
||||
status: 'implemented' | 'partial' | 'planned' | 'not_applicable'
|
||||
evidence?: string
|
||||
lastReview?: string
|
||||
}[]
|
||||
}
|
||||
|
||||
export default function TOMPage() {
|
||||
const [expandedCategory, setExpandedCategory] = useState<string | null>('encryption')
|
||||
|
||||
const tomCategories: TOMCategory[] = [
|
||||
{
|
||||
id: 'encryption',
|
||||
title: 'Verschluesselung',
|
||||
article: 'Art. 32 Abs. 1 lit. a',
|
||||
description: 'Pseudonymisierung und Verschluesselung personenbezogener Daten',
|
||||
measures: [
|
||||
{
|
||||
name: 'TLS 1.3 fuer alle Verbindungen',
|
||||
description: 'Alle HTTP-Verbindungen werden ueber HTTPS mit TLS 1.3 verschluesselt',
|
||||
status: 'implemented',
|
||||
evidence: 'SSL Labs A+ Rating, Nginx Config',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Verschluesselung ruhender Daten',
|
||||
description: 'PostgreSQL-Datenbank mit AES-256 Verschluesselung (pgcrypto)',
|
||||
status: 'implemented',
|
||||
evidence: 'PostgreSQL Config, Encryption Keys in Vault',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'E-Mail-Verschluesselung',
|
||||
description: 'Optionale PGP-Verschluesselung fuer sensible E-Mails',
|
||||
status: 'partial',
|
||||
evidence: 'PGP-Keys verfuegbar, nicht fuer alle Empfaenger',
|
||||
},
|
||||
{
|
||||
name: 'Backup-Verschluesselung',
|
||||
description: 'Alle Backups werden mit AES-256 verschluesselt gespeichert',
|
||||
status: 'implemented',
|
||||
evidence: 'restic Backup Config',
|
||||
lastReview: '2024-11-15'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'access_control',
|
||||
title: 'Zugriffskontrolle',
|
||||
article: 'Art. 32 Abs. 1 lit. b',
|
||||
description: 'Faehigkeit, Vertraulichkeit und Integritaet auf Dauer sicherzustellen',
|
||||
measures: [
|
||||
{
|
||||
name: 'Role-Based Access Control (RBAC)',
|
||||
description: 'Zugriff basierend auf Rollen: user, admin, data_protection_officer',
|
||||
status: 'implemented',
|
||||
evidence: 'consent-service/internal/middleware/auth.go',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Multi-Faktor-Authentifizierung',
|
||||
description: '2FA fuer Admin-Zugaenge (TOTP)',
|
||||
status: 'implemented',
|
||||
evidence: 'Auth-Service Implementation',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Passwort-Policy',
|
||||
description: 'Min. 12 Zeichen, Komplexitaetsanforderungen, bcrypt-Hashing',
|
||||
status: 'implemented',
|
||||
evidence: 'consent-service/internal/services/auth_service.go',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Session-Management',
|
||||
description: 'JWT mit 24h Ablauf, Refresh-Token-Rotation',
|
||||
status: 'implemented',
|
||||
evidence: 'JWT Config, Token-Rotation Logic',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'IP-Whitelisting Admin',
|
||||
description: 'Admin-Zugang nur von definierten IP-Bereichen',
|
||||
status: 'planned',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'availability',
|
||||
title: 'Verfuegbarkeit & Belastbarkeit',
|
||||
article: 'Art. 32 Abs. 1 lit. b',
|
||||
description: 'Faehigkeit, Verfuegbarkeit und Belastbarkeit der Systeme sicherzustellen',
|
||||
measures: [
|
||||
{
|
||||
name: 'Automatische Backups',
|
||||
description: 'Taegliche inkrementelle Backups, woechentliche Vollbackups',
|
||||
status: 'implemented',
|
||||
evidence: 'restic + cron Jobs',
|
||||
lastReview: '2024-11-15'
|
||||
},
|
||||
{
|
||||
name: 'Disaster Recovery Plan',
|
||||
description: 'Dokumentierter Wiederherstellungsplan mit RTO < 4h',
|
||||
status: 'partial',
|
||||
evidence: 'DR-Dokumentation in Arbeit',
|
||||
},
|
||||
{
|
||||
name: 'Health Monitoring',
|
||||
description: 'Prometheus + Grafana fuer System-Monitoring',
|
||||
status: 'implemented',
|
||||
evidence: 'Monitoring Stack deployed',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Rate Limiting',
|
||||
description: 'API Rate Limiting zum Schutz vor DDoS',
|
||||
status: 'implemented',
|
||||
evidence: 'Nginx Rate Limit Config',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'restore',
|
||||
title: 'Wiederherstellung',
|
||||
article: 'Art. 32 Abs. 1 lit. c',
|
||||
description: 'Rasche Wiederherstellung nach physischem oder technischem Zwischenfall',
|
||||
measures: [
|
||||
{
|
||||
name: 'Backup-Restore-Tests',
|
||||
description: 'Quartalsweise Tests der Backup-Wiederherstellung',
|
||||
status: 'partial',
|
||||
evidence: 'Letzter Test: 2024-10-15',
|
||||
},
|
||||
{
|
||||
name: 'Dokumentierte Recovery-Prozeduren',
|
||||
description: 'Schritt-fuer-Schritt Anleitungen fuer verschiedene Szenarien',
|
||||
status: 'implemented',
|
||||
evidence: 'docs/disaster-recovery/',
|
||||
lastReview: '2024-11-01'
|
||||
},
|
||||
{
|
||||
name: 'Redundante Datenhaltung',
|
||||
description: 'Datenbank-Replikation auf zweitem Server',
|
||||
status: 'planned',
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'review',
|
||||
title: 'Regelmaessige Ueberpruefung',
|
||||
article: 'Art. 32 Abs. 1 lit. d',
|
||||
description: 'Verfahren zur regelmaessigen Ueberpruefung, Bewertung und Evaluierung',
|
||||
measures: [
|
||||
{
|
||||
name: 'Security Audits',
|
||||
description: 'Jaehrliche externe Security-Audits',
|
||||
status: 'implemented',
|
||||
evidence: 'Letzter Audit: 2024-09',
|
||||
lastReview: '2024-09-15'
|
||||
},
|
||||
{
|
||||
name: 'Penetration Tests',
|
||||
description: 'Jaehrliche Penetrationstests durch externen Dienstleister',
|
||||
status: 'partial',
|
||||
evidence: 'Naechster Test geplant: Q1 2025',
|
||||
},
|
||||
{
|
||||
name: 'Vulnerability Scanning',
|
||||
description: 'Woechentliche automatisierte Schwachstellen-Scans',
|
||||
status: 'implemented',
|
||||
evidence: 'GitHub Dependabot + Trivy',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'TOM-Review',
|
||||
description: 'Jaehrliche Ueberpruefung aller TOMs',
|
||||
status: 'implemented',
|
||||
evidence: 'Diese Seite',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
]
|
||||
},
|
||||
{
|
||||
id: 'logging',
|
||||
title: 'Protokollierung & Audit-Trail',
|
||||
article: 'Art. 32 Abs. 2',
|
||||
description: 'Nachweis der Einhaltung durch Protokollierung',
|
||||
measures: [
|
||||
{
|
||||
name: 'Zugriffs-Logging',
|
||||
description: 'Protokollierung aller Zugriffe auf personenbezogene Daten',
|
||||
status: 'implemented',
|
||||
evidence: 'consent-service Audit-Logs',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Aenderungs-Historie',
|
||||
description: 'Vollstaendige Historie aller Datenänderungen',
|
||||
status: 'implemented',
|
||||
evidence: 'audit_logs Tabelle in PostgreSQL',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Admin-Aktionen-Log',
|
||||
description: 'Protokollierung aller administrativen Aktionen',
|
||||
status: 'implemented',
|
||||
evidence: 'Admin Action Logger',
|
||||
lastReview: '2024-12-01'
|
||||
},
|
||||
{
|
||||
name: 'Log-Aufbewahrung',
|
||||
description: 'Logs werden 2 Jahre aufbewahrt, dann automatisch geloescht',
|
||||
status: 'implemented',
|
||||
evidence: 'Log Retention Policy',
|
||||
lastReview: '2024-11-01'
|
||||
},
|
||||
]
|
||||
},
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'implemented':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Umgesetzt</span>
|
||||
case 'partial':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Teilweise</span>
|
||||
case 'planned':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-blue-100 text-blue-800">Geplant</span>
|
||||
case 'not_applicable':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">N/A</span>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const calculateCategoryScore = (category: TOMCategory) => {
|
||||
const total = category.measures.length
|
||||
const implemented = category.measures.filter(m => m.status === 'implemented').length
|
||||
const partial = category.measures.filter(m => m.status === 'partial').length
|
||||
return Math.round(((implemented + partial * 0.5) / total) * 100)
|
||||
}
|
||||
|
||||
const calculateOverallScore = () => {
|
||||
let totalMeasures = 0
|
||||
let implementedScore = 0
|
||||
tomCategories.forEach(cat => {
|
||||
cat.measures.forEach(m => {
|
||||
totalMeasures++
|
||||
if (m.status === 'implemented') implementedScore += 1
|
||||
else if (m.status === 'partial') implementedScore += 0.5
|
||||
})
|
||||
})
|
||||
return Math.round((implementedScore / totalMeasures) * 100)
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="Technische & Organisatorische Massnahmen (TOMs)"
|
||||
purpose="Dokumentation aller Sicherheitsmassnahmen gemaess Art. 32 DSGVO. Diese Seite dient als Nachweis fuer Auditoren und den DSB."
|
||||
audience={['DSB', 'IT-Sicherheit', 'Auditoren', 'Geschaeftsfuehrung']}
|
||||
gdprArticles={['Art. 32 (Sicherheit der Verarbeitung)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)', 'Nginx', 'PostgreSQL'],
|
||||
databases: ['PostgreSQL (verschluesselt)', 'MinIO (Backups)'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management-System' },
|
||||
{ name: 'Security', href: '/infrastructure/security', description: 'DevSecOps Dashboard' },
|
||||
{ name: 'Audit', href: '/compliance/audit', description: 'Audit-Dokumentation' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Overall Score */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">TOM-Umsetzungsgrad</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">Gesamtfortschritt aller technischen und organisatorischen Massnahmen</p>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<div className={`text-4xl font-bold ${calculateOverallScore() >= 80 ? 'text-green-600' : calculateOverallScore() >= 50 ? 'text-yellow-600' : 'text-red-600'}`}>
|
||||
{calculateOverallScore()}%
|
||||
</div>
|
||||
<div className="text-sm text-slate-500">Umgesetzt</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 h-3 bg-slate-100 rounded-full overflow-hidden">
|
||||
<div
|
||||
className={`h-full transition-all ${calculateOverallScore() >= 80 ? 'bg-green-500' : calculateOverallScore() >= 50 ? 'bg-yellow-500' : 'bg-red-500'}`}
|
||||
style={{ width: `${calculateOverallScore()}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* TOM Categories */}
|
||||
<div className="space-y-4">
|
||||
{tomCategories.map((category) => (
|
||||
<div key={category.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedCategory(expandedCategory === category.id ? null : category.id)}
|
||||
className="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-12 h-12 rounded-lg flex items-center justify-center text-lg font-bold ${
|
||||
calculateCategoryScore(category) >= 80 ? 'bg-green-100 text-green-700' :
|
||||
calculateCategoryScore(category) >= 50 ? 'bg-yellow-100 text-yellow-700' :
|
||||
'bg-red-100 text-red-700'
|
||||
}`}>
|
||||
{calculateCategoryScore(category)}%
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<h3 className="font-semibold text-slate-900">{category.title}</h3>
|
||||
<p className="text-sm text-slate-500">{category.article} - {category.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-5 h-5 text-slate-400 transition-transform ${expandedCategory === category.id ? '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>
|
||||
|
||||
{expandedCategory === category.id && (
|
||||
<div className="px-6 pb-6 border-t border-slate-100">
|
||||
<div className="mt-4 space-y-3">
|
||||
{category.measures.map((measure, idx) => (
|
||||
<div key={idx} className="p-4 bg-slate-50 rounded-lg">
|
||||
<div className="flex items-start justify-between mb-2">
|
||||
<h4 className="font-medium text-slate-900">{measure.name}</h4>
|
||||
{getStatusBadge(measure.status)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-600 mb-2">{measure.description}</p>
|
||||
{(measure.evidence || measure.lastReview) && (
|
||||
<div className="flex flex-wrap gap-4 text-xs text-slate-500">
|
||||
{measure.evidence && (
|
||||
<span>Nachweis: <span className="font-mono">{measure.evidence}</span></span>
|
||||
)}
|
||||
{measure.lastReview && (
|
||||
<span>Letzte Pruefung: {measure.lastReview}</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-purple-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-purple-900">Dokumentationspflicht</h4>
|
||||
<p className="text-sm text-purple-800 mt-1">
|
||||
Gemaess Art. 32 Abs. 1 DSGVO muessen geeignete technische und organisatorische Massnahmen
|
||||
implementiert werden, um ein dem Risiko angemessenes Schutzniveau zu gewaehrleisten.
|
||||
Diese Dokumentation dient als Nachweis fuer Aufsichtsbehoerden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,334 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* VVT - Verarbeitungsverzeichnis
|
||||
*
|
||||
* Art. 30 DSGVO - Verzeichnis von Verarbeitungstaetigkeiten
|
||||
*/
|
||||
|
||||
import { useState } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
interface ProcessingActivity {
|
||||
id: string
|
||||
name: string
|
||||
purpose: string
|
||||
legalBasis: string
|
||||
legalBasisDetail: string
|
||||
categories: string[]
|
||||
recipients: string[]
|
||||
thirdCountryTransfer: boolean
|
||||
thirdCountryDetails?: string
|
||||
retentionPeriod: string
|
||||
technicalMeasures: string[]
|
||||
lastReview: string
|
||||
status: 'active' | 'inactive' | 'review_needed'
|
||||
}
|
||||
|
||||
export default function VVTPage() {
|
||||
const [expandedActivity, setExpandedActivity] = useState<string | null>('user_accounts')
|
||||
const [filterStatus, setFilterStatus] = useState<string>('all')
|
||||
|
||||
const processingActivities: ProcessingActivity[] = [
|
||||
{
|
||||
id: 'user_accounts',
|
||||
name: 'Nutzerkontenverwaltung',
|
||||
purpose: 'Bereitstellung und Verwaltung von Benutzerkonten fuer die Plattform-Nutzung',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||
legalBasisDetail: 'Vertragserfuellung - Notwendig zur Bereitstellung des Dienstes',
|
||||
categories: ['Name', 'E-Mail-Adresse', 'Passwort (gehasht)', 'Profilbild (optional)'],
|
||||
recipients: ['Keine externen Empfaenger'],
|
||||
thirdCountryTransfer: false,
|
||||
retentionPeriod: '3 Jahre nach Kontolöschung',
|
||||
technicalMeasures: ['Verschluesselung', 'Zugriffskontrolle', 'Audit-Logging'],
|
||||
lastReview: '2024-12-01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'consent_management',
|
||||
name: 'Einwilligungsverwaltung',
|
||||
purpose: 'Verwaltung und Dokumentation von Einwilligungen gemaess DSGVO',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. c DSGVO',
|
||||
legalBasisDetail: 'Rechtliche Verpflichtung - Nachweis der Einwilligung',
|
||||
categories: ['Benutzer-ID', 'Einwilligungstyp', 'Zeitstempel', 'IP-Adresse', 'Version'],
|
||||
recipients: ['DSB (Datenschutzbeauftragter)', 'Aufsichtsbehoerden bei Anfrage'],
|
||||
thirdCountryTransfer: false,
|
||||
retentionPeriod: '6 Jahre nach Widerruf (Nachweispflicht)',
|
||||
technicalMeasures: ['Unveraenderbarkeit', 'Zeitstempel', 'Audit-Trail'],
|
||||
lastReview: '2024-12-01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'learning_analytics',
|
||||
name: 'Lernfortschrittsanalyse',
|
||||
purpose: 'Analyse des Lernfortschritts zur Verbesserung der Lernerfahrung',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
legalBasisDetail: 'Einwilligung - Nutzer stimmt der Analyse explizit zu',
|
||||
categories: ['Benutzer-ID', 'Lernaktivitaeten', 'Testergebnisse', 'Zeitaufwand'],
|
||||
recipients: ['Lehrer (aggregiert)', 'Eltern (mit Einwilligung)'],
|
||||
thirdCountryTransfer: false,
|
||||
retentionPeriod: 'Bis zum Ende des Schuljahres + 1 Jahr',
|
||||
technicalMeasures: ['Pseudonymisierung', 'Verschluesselung', 'Zugriffsbeschraenkung'],
|
||||
lastReview: '2024-11-15',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'ai_processing',
|
||||
name: 'KI-gestuetzte Verarbeitung',
|
||||
purpose: 'Automatische Korrektur und Feedback-Generierung mittels KI',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
legalBasisDetail: 'Einwilligung - Explizite Zustimmung zur KI-Verarbeitung',
|
||||
categories: ['Benutzer-ID', 'Eingabetexte', 'Generierte Bewertungen'],
|
||||
recipients: ['Ollama (lokal)', 'Optional: Cloud-LLM (mit Einwilligung)'],
|
||||
thirdCountryTransfer: true,
|
||||
thirdCountryDetails: 'OpenAI (USA) - nur bei expliziter Einwilligung, Standardvertragsklauseln',
|
||||
retentionPeriod: 'Sofortige Loeschung nach Verarbeitung (keine Speicherung)',
|
||||
technicalMeasures: ['Anonymisierung wo moeglich', 'Keine Speicherung bei Drittanbietern'],
|
||||
lastReview: '2024-12-01',
|
||||
status: 'review_needed'
|
||||
},
|
||||
{
|
||||
id: 'support_requests',
|
||||
name: 'Support-Anfragen',
|
||||
purpose: 'Bearbeitung von Support- und Hilfe-Anfragen',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO',
|
||||
legalBasisDetail: 'Vertragserfuellung - Teil des Service-Angebots',
|
||||
categories: ['Name', 'E-Mail', 'Anfrage-Inhalt', 'Anhaenge'],
|
||||
recipients: ['Support-Team', 'Entwickler (bei technischen Problemen)'],
|
||||
thirdCountryTransfer: false,
|
||||
retentionPeriod: '2 Jahre nach Abschluss des Tickets',
|
||||
technicalMeasures: ['Zugriffskontrolle', 'Verschluesselung'],
|
||||
lastReview: '2024-10-01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'newsletter',
|
||||
name: 'Newsletter-Versand',
|
||||
purpose: 'Information ueber Updates, Features und relevante Bildungsthemen',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO',
|
||||
legalBasisDetail: 'Einwilligung - Double-Opt-In Verfahren',
|
||||
categories: ['E-Mail-Adresse', 'Anrede', 'Praeferenzen'],
|
||||
recipients: ['E-Mail-Provider (Mailpit/SMTP)'],
|
||||
thirdCountryTransfer: false,
|
||||
retentionPeriod: 'Bis zum Widerruf',
|
||||
technicalMeasures: ['Abmelde-Link in jeder E-Mail', 'Verschluesselung'],
|
||||
lastReview: '2024-11-01',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
id: 'logging',
|
||||
name: 'System-Logging',
|
||||
purpose: 'Sicherheit, Fehleranalyse und Betrieb der Plattform',
|
||||
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO',
|
||||
legalBasisDetail: 'Berechtigtes Interesse - Sicherheit und Betrieb',
|
||||
categories: ['IP-Adresse', 'Zeitstempel', 'Anfrage-Details', 'User-Agent'],
|
||||
recipients: ['IT-Administratoren', 'Bei Sicherheitsvorfaellen: Behoerden'],
|
||||
thirdCountryTransfer: false,
|
||||
retentionPeriod: '90 Tage (Standard-Logs), 2 Jahre (Security-Logs)',
|
||||
technicalMeasures: ['IP-Anonymisierung nach 7 Tagen', 'Zugriffsbeschraenkung'],
|
||||
lastReview: '2024-12-01',
|
||||
status: 'active'
|
||||
},
|
||||
]
|
||||
|
||||
const getStatusBadge = (status: string) => {
|
||||
switch (status) {
|
||||
case 'active':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-green-100 text-green-800">Aktiv</span>
|
||||
case 'inactive':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-slate-100 text-slate-600">Inaktiv</span>
|
||||
case 'review_needed':
|
||||
return <span className="px-2 py-1 rounded-full text-xs font-medium bg-yellow-100 text-yellow-800">Pruefung erforderlich</span>
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
const filteredActivities = filterStatus === 'all'
|
||||
? processingActivities
|
||||
: processingActivities.filter(a => a.status === filterStatus)
|
||||
|
||||
return (
|
||||
<div>
|
||||
<PagePurpose
|
||||
title="Verarbeitungsverzeichnis (VVT)"
|
||||
purpose="Verzeichnis aller Verarbeitungstaetigkeiten gemaess Art. 30 DSGVO. Dokumentiert Zweck, Rechtsgrundlage, Kategorien und Loeschfristen."
|
||||
audience={['DSB', 'Auditoren', 'Aufsichtsbehoerden']}
|
||||
gdprArticles={['Art. 30 (Verzeichnis von Verarbeitungstaetigkeiten)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'DSMS', href: '/compliance/dsms', description: 'Datenschutz-Management-System' },
|
||||
{ name: 'TOMs', href: '/compliance/tom', description: 'Technische Massnahmen' },
|
||||
{ name: 'DSFA', href: '/compliance/dsfa', description: 'Datenschutz-Folgenabschaetzung' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
|
||||
{/* Header */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 mb-6">
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<div>
|
||||
<h2 className="text-lg font-semibold text-slate-900">Verarbeitungstaetigkeiten</h2>
|
||||
<p className="text-sm text-slate-500 mt-1">{processingActivities.length} dokumentierte Taetigkeiten</p>
|
||||
</div>
|
||||
<button className="px-4 py-2 bg-purple-600 text-white rounded-lg text-sm font-medium hover:bg-purple-700">
|
||||
+ Neue Taetigkeit
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Filter */}
|
||||
<div className="flex gap-2">
|
||||
{[
|
||||
{ value: 'all', label: 'Alle' },
|
||||
{ value: 'active', label: 'Aktiv' },
|
||||
{ value: 'review_needed', label: 'Pruefung erforderlich' },
|
||||
{ value: 'inactive', label: 'Inaktiv' },
|
||||
].map(filter => (
|
||||
<button
|
||||
key={filter.value}
|
||||
onClick={() => setFilterStatus(filter.value)}
|
||||
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition-colors ${
|
||||
filterStatus === filter.value
|
||||
? 'bg-purple-600 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
{filter.label}
|
||||
</button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Activities List */}
|
||||
<div className="space-y-4">
|
||||
{filteredActivities.map((activity) => (
|
||||
<div key={activity.id} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpandedActivity(expandedActivity === activity.id ? null : activity.id)}
|
||||
className="w-full px-6 py-4 flex items-center justify-between hover:bg-slate-50 transition-colors"
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className="text-left">
|
||||
<div className="flex items-center gap-3">
|
||||
<h3 className="font-semibold text-slate-900">{activity.name}</h3>
|
||||
{getStatusBadge(activity.status)}
|
||||
{activity.thirdCountryTransfer && (
|
||||
<span className="px-2 py-1 rounded-full text-xs font-medium bg-orange-100 text-orange-800">
|
||||
Drittland-Transfer
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<p className="text-sm text-slate-500 mt-1">{activity.purpose}</p>
|
||||
</div>
|
||||
</div>
|
||||
<svg
|
||||
className={`w-5 h-5 text-slate-400 transition-transform ${expandedActivity === activity.id ? '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>
|
||||
|
||||
{expandedActivity === activity.id && (
|
||||
<div className="px-6 pb-6 border-t border-slate-100">
|
||||
<div className="mt-4 grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
{/* Left Column */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Rechtsgrundlage</h4>
|
||||
<p className="font-semibold text-slate-900">{activity.legalBasis}</p>
|
||||
<p className="text-sm text-slate-600">{activity.legalBasisDetail}</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Datenkategorien</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{activity.categories.map((cat, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-slate-100 text-slate-700 rounded text-sm">
|
||||
{cat}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Empfaenger</h4>
|
||||
<ul className="text-sm text-slate-700 list-disc list-inside">
|
||||
{activity.recipients.map((rec, idx) => (
|
||||
<li key={idx}>{rec}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column */}
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Loeschfrist</h4>
|
||||
<p className="text-slate-700">{activity.retentionPeriod}</p>
|
||||
</div>
|
||||
|
||||
{activity.thirdCountryTransfer && activity.thirdCountryDetails && (
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Drittland-Transfer</h4>
|
||||
<p className="text-slate-700">{activity.thirdCountryDetails}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Technische Massnahmen</h4>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{activity.technicalMeasures.map((measure, idx) => (
|
||||
<span key={idx} className="px-2 py-1 bg-green-100 text-green-700 rounded text-sm">
|
||||
{measure}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-slate-500 mb-1">Letzte Pruefung</h4>
|
||||
<p className="text-slate-700">{activity.lastReview}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 pt-4 border-t border-slate-100 flex gap-2">
|
||||
<button className="px-3 py-1.5 text-sm text-purple-600 hover:text-purple-700 font-medium">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-700">
|
||||
PDF exportieren
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Info */}
|
||||
<div className="mt-6 bg-purple-50 border border-purple-200 rounded-xl p-4">
|
||||
<div className="flex gap-3">
|
||||
<svg className="w-5 h-5 text-purple-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-purple-900">Pflicht zur Fuehrung</h4>
|
||||
<p className="text-sm text-purple-800 mt-1">
|
||||
Gemaess Art. 30 DSGVO ist jeder Verantwortliche verpflichtet, ein Verzeichnis aller
|
||||
Verarbeitungstaetigkeiten zu fuehren. Dieses Verzeichnis muss der Aufsichtsbehoerde
|
||||
auf Anfrage zur Verfuegung gestellt werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
+18
-6
@@ -1,19 +1,18 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Document Workflow Page (SDK Version)
|
||||
* Workflow - Document Versioning & Approval
|
||||
*
|
||||
* Split-view editor for legal documents with synchronized scrolling:
|
||||
* - Left: Current published version (read-only)
|
||||
* - Right: Draft/new version (editable)
|
||||
* - Approval workflow: Draft -> Review -> Approved -> Published
|
||||
* - Approval workflow: Draft → Review → Approved → Published
|
||||
* - DOCX upload with Word conversion
|
||||
* - Rich text editor with formatting toolbar
|
||||
*/
|
||||
|
||||
import { useState, useEffect, useRef, useCallback } from 'react'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// Types
|
||||
interface Document {
|
||||
@@ -68,7 +67,6 @@ const DOCUMENT_TYPES: Record<string, string> = {
|
||||
}
|
||||
|
||||
export default function WorkflowPage() {
|
||||
const { state } = useSDK()
|
||||
const [documents, setDocuments] = useState<Document[]>([])
|
||||
const [versions, setVersions] = useState<Version[]>([])
|
||||
const [selectedDocument, setSelectedDocument] = useState<Document | null>(null)
|
||||
@@ -469,7 +467,20 @@ export default function WorkflowPage() {
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader stepId="workflow" showProgress={true} />
|
||||
<PagePurpose
|
||||
title="Workflow"
|
||||
purpose="Bearbeiten und versionieren Sie rechtliche Dokumente (AGB, Datenschutz, Nutzungsbedingungen) mit einem integrierten Freigabeprozess. Links sehen Sie die aktuelle Version, rechts die Aenderungsversion mit synchronisiertem Scrollen."
|
||||
audience={['DSB', 'Rechtsabteilung', 'Compliance Officer']}
|
||||
gdprArticles={['Art. 7 (Einwilligung)', 'Art. 13/14 (Informationspflichten)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'Python Backend'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'Consent Verwaltung', href: '/compliance/consent', description: 'Dokumente verwalten' },
|
||||
{ name: 'Einwilligungen', href: '/compliance/einwilligungen', description: 'Nutzer-Consents' },
|
||||
]}
|
||||
/>
|
||||
|
||||
{/* Error Banner */}
|
||||
{error && (
|
||||
@@ -554,6 +565,7 @@ export default function WorkflowPage() {
|
||||
<div className="bg-white rounded-xl shadow-sm border p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-6">
|
||||
{/* Workflow Steps */}
|
||||
{['draft', 'review', 'approved', 'published'].map((status, idx) => (
|
||||
<div key={status} className="flex items-center">
|
||||
{idx > 0 && <div className="w-8 h-0.5 bg-slate-200 mr-2" />}
|
||||
@@ -1,12 +0,0 @@
|
||||
'use client'
|
||||
|
||||
import { SDKProvider } from '@/lib/sdk/context'
|
||||
import { CatalogManagerContent } from '@/components/catalog-manager/CatalogManagerContent'
|
||||
|
||||
export default function AdminCatalogManagerPage() {
|
||||
return (
|
||||
<SDKProvider>
|
||||
<CatalogManagerContent />
|
||||
</SDKProvider>
|
||||
)
|
||||
}
|
||||
@@ -6,7 +6,6 @@ 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 {
|
||||
@@ -112,24 +111,13 @@ export default function DashboardPage() {
|
||||
))}
|
||||
</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">
|
||||
<Link href="/compliance/dsr" className="text-sm text-primary-600 hover:text-primary-700">
|
||||
Alle anzeigen
|
||||
</Link>
|
||||
</div>
|
||||
@@ -139,6 +127,9 @@ export default function DashboardPage() {
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* System Status */}
|
||||
<ServiceStatus />
|
||||
</div>
|
||||
|
||||
{/* Info Box */}
|
||||
|
||||
@@ -1,797 +0,0 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Screen Flow Visualization
|
||||
*
|
||||
* Visualisiert alle Screens aus:
|
||||
* - Studio (Port 8000): Lehrer-Oberflaeche
|
||||
* - Admin v2 (Port 3002): Admin Panel
|
||||
*/
|
||||
|
||||
import { useCallback, useState, useMemo, useEffect } from 'react'
|
||||
import ReactFlow, {
|
||||
Node,
|
||||
Edge,
|
||||
Controls,
|
||||
Background,
|
||||
MiniMap,
|
||||
useNodesState,
|
||||
useEdgesState,
|
||||
BackgroundVariant,
|
||||
MarkerType,
|
||||
Panel,
|
||||
} from 'reactflow'
|
||||
import 'reactflow/dist/style.css'
|
||||
|
||||
// ============================================
|
||||
// TYPES
|
||||
// ============================================
|
||||
|
||||
interface ScreenDefinition {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
icon: string
|
||||
url?: string
|
||||
}
|
||||
|
||||
interface ConnectionDef {
|
||||
source: string
|
||||
target: string
|
||||
label?: string
|
||||
}
|
||||
|
||||
type FlowType = 'studio' | 'admin'
|
||||
|
||||
// ============================================
|
||||
// STUDIO SCREENS (Port 8000)
|
||||
// ============================================
|
||||
|
||||
const STUDIO_SCREENS: ScreenDefinition[] = [
|
||||
{ id: 'lehrer-dashboard', name: 'Mein Dashboard', description: 'Hauptuebersicht mit Widgets', category: 'navigation', icon: '🏠', url: '/app#lehrer-dashboard' },
|
||||
{ id: 'lehrer-onboarding', name: 'Erste Schritte', description: 'Onboarding & Schnellstart', category: 'navigation', icon: '🚀', url: '/app#lehrer-onboarding' },
|
||||
{ id: 'hilfe', name: 'Dokumentation', description: 'Hilfe & Anleitungen', category: 'navigation', icon: '📚', url: '/app#hilfe' },
|
||||
{ id: 'worksheets', name: 'Arbeitsblaetter Studio', description: 'Lernmaterialien erstellen', category: 'content', icon: '📝', url: '/app#worksheets' },
|
||||
{ id: 'content-creator', name: 'Content Creator', description: 'Inhalte erstellen', category: 'content', icon: '✨', url: '/app#content-creator' },
|
||||
{ id: 'content-feed', name: 'Content Feed', description: 'Inhalte durchsuchen', category: 'content', icon: '📰', url: '/app#content-feed' },
|
||||
{ id: 'unit-creator', name: 'Unit Creator', description: 'Lerneinheiten erstellen', category: 'content', icon: '📦', url: '/app#unit-creator' },
|
||||
{ id: 'letters', name: 'Briefe & Vorlagen', description: 'Brief-Generator', category: 'content', icon: '✉️', url: '/app#letters' },
|
||||
{ id: 'correction', name: 'Korrektur', description: 'Arbeiten korrigieren', category: 'content', icon: '✏️', url: '/app#correction' },
|
||||
{ id: 'klausur-korrektur', name: 'Abiturklausuren', description: 'KI-gestuetzte Klausurkorrektur', category: 'content', icon: '📋', url: '/app#klausur-korrektur' },
|
||||
{ id: 'jitsi', name: 'Videokonferenz', description: 'Jitsi Meet Integration', category: 'communication', icon: '🎥', url: '/app#jitsi' },
|
||||
{ id: 'messenger', name: 'Messenger', description: 'Matrix E2EE Chat', category: 'communication', icon: '💬', url: '/app#messenger' },
|
||||
{ id: 'mail', name: 'Unified Inbox', description: 'E-Mail Verwaltung', category: 'communication', icon: '📧', url: '/app#mail' },
|
||||
{ id: 'school-classes', name: 'Klassen', description: 'Klassenverwaltung', category: 'school', icon: '👥', url: '/app#school-classes' },
|
||||
{ id: 'school-exams', name: 'Pruefungen', description: 'Pruefungsverwaltung', category: 'school', icon: '📝', url: '/app#school-exams' },
|
||||
{ id: 'school-grades', name: 'Noten', description: 'Notenverwaltung', category: 'school', icon: '📊', url: '/app#school-grades' },
|
||||
{ id: 'school-gradebook', name: 'Notenbuch', description: 'Digitales Notenbuch', category: 'school', icon: '📖', url: '/app#school-gradebook' },
|
||||
{ id: 'school-certificates', name: 'Zeugnisse', description: 'Zeugniserstellung', category: 'school', icon: '🎓', url: '/app#school-certificates' },
|
||||
{ id: 'companion', name: 'Begleiter & Stunde', description: 'KI-Unterrichtsassistent', category: 'ai', icon: '🤖', url: '/app#companion' },
|
||||
{ id: 'alerts', name: 'Alerts', description: 'News & Benachrichtigungen', category: 'ai', icon: '🔔', url: '/app#alerts' },
|
||||
{ id: 'admin', name: 'Einstellungen', description: 'Systemeinstellungen', category: 'admin', icon: '⚙️', url: '/app#admin' },
|
||||
{ id: 'rbac-admin', name: 'Rollen & Rechte', description: 'Berechtigungsverwaltung', category: 'admin', icon: '🔐', url: '/app#rbac-admin' },
|
||||
{ id: 'abitur-docs-admin', name: 'Abitur Dokumente', description: 'Erwartungshorizonte', category: 'admin', icon: '📄', url: '/app#abitur-docs-admin' },
|
||||
{ id: 'system-info', name: 'System Info', description: 'Systeminformationen', category: 'admin', icon: '💻', url: '/app#system-info' },
|
||||
{ id: 'workflow', name: 'Workflow', description: 'Automatisierungen', category: 'admin', icon: '⚡', url: '/app#workflow' },
|
||||
]
|
||||
|
||||
const STUDIO_CONNECTIONS: ConnectionDef[] = [
|
||||
{ source: 'lehrer-onboarding', target: 'worksheets', label: 'Arbeitsblaetter' },
|
||||
{ source: 'lehrer-onboarding', target: 'klausur-korrektur', label: 'Abiturklausuren' },
|
||||
{ source: 'lehrer-onboarding', target: 'correction', label: 'Korrektur' },
|
||||
{ source: 'lehrer-onboarding', target: 'letters', label: 'Briefe' },
|
||||
{ source: 'lehrer-onboarding', target: 'school-classes', label: 'Klassen' },
|
||||
{ source: 'lehrer-onboarding', target: 'jitsi', label: 'Meet' },
|
||||
{ source: 'lehrer-onboarding', target: 'hilfe', label: 'Doku' },
|
||||
{ source: 'lehrer-onboarding', target: 'admin', label: 'Settings' },
|
||||
{ source: 'lehrer-dashboard', target: 'worksheets' },
|
||||
{ source: 'lehrer-dashboard', target: 'correction' },
|
||||
{ source: 'lehrer-dashboard', target: 'jitsi' },
|
||||
{ source: 'lehrer-dashboard', target: 'letters' },
|
||||
{ source: 'lehrer-dashboard', target: 'messenger' },
|
||||
{ source: 'lehrer-dashboard', target: 'klausur-korrektur' },
|
||||
{ source: 'lehrer-dashboard', target: 'companion' },
|
||||
{ source: 'lehrer-dashboard', target: 'alerts' },
|
||||
{ source: 'lehrer-dashboard', target: 'mail' },
|
||||
{ source: 'lehrer-dashboard', target: 'school-classes' },
|
||||
{ source: 'lehrer-dashboard', target: 'lehrer-onboarding', label: 'Sidebar' },
|
||||
{ source: 'school-classes', target: 'school-exams' },
|
||||
{ source: 'school-classes', target: 'school-grades' },
|
||||
{ source: 'school-grades', target: 'school-gradebook' },
|
||||
{ source: 'school-gradebook', target: 'school-certificates' },
|
||||
{ source: 'worksheets', target: 'content-creator' },
|
||||
{ source: 'worksheets', target: 'unit-creator' },
|
||||
{ source: 'content-creator', target: 'content-feed' },
|
||||
{ source: 'klausur-korrektur', target: 'abitur-docs-admin' },
|
||||
{ source: 'admin', target: 'rbac-admin' },
|
||||
{ source: 'admin', target: 'system-info' },
|
||||
{ source: 'admin', target: 'workflow' },
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// ADMIN v2 SCREENS (Port 3002)
|
||||
// Based on navigation.ts - Last updated: 2026-02-03
|
||||
// ============================================
|
||||
|
||||
const ADMIN_SCREENS: ScreenDefinition[] = [
|
||||
// === META / OVERVIEW ===
|
||||
{ id: 'admin-dashboard', name: 'Dashboard', description: 'Uebersicht & Statistiken', category: 'overview', icon: '🏠', url: '/dashboard' },
|
||||
{ id: 'admin-onboarding', name: 'Onboarding', description: 'Lern-Wizards fuer alle Module', category: 'overview', icon: '📖', url: '/onboarding' },
|
||||
{ id: 'admin-architecture', name: 'Architektur', description: 'Backend-Module & Datenfluss', category: 'overview', icon: '🏗️', url: '/architecture' },
|
||||
{ id: 'admin-backlog', name: 'Production Backlog', description: 'Go-Live Checkliste', category: 'overview', icon: '📝', url: '/backlog' },
|
||||
{ id: 'admin-rbac', name: 'RBAC', description: 'Rollen & Berechtigungen', category: 'overview', icon: '👥', url: '/rbac' },
|
||||
|
||||
// === COMPLIANCE SDK (Violet #8b5cf6) ===
|
||||
// DSGVO - Datenschutz & Betroffenenrechte
|
||||
{ id: 'admin-consent', name: 'Consent Verwaltung', description: 'Rechtliche Dokumente & Versionen', category: 'sdk', icon: '📄', url: '/sdk/consent-management' },
|
||||
{ id: 'admin-dsr', name: 'Datenschutzanfragen', description: 'DSGVO Art. 15-21', category: 'sdk', icon: '🔒', url: '/sdk/dsr' },
|
||||
{ id: 'admin-einwilligungen', name: 'Einwilligungen', description: 'Nutzer-Consent Uebersicht', category: 'sdk', icon: '✅', url: '/sdk/einwilligungen' },
|
||||
{ id: 'admin-vvt', name: 'VVT', description: 'Verarbeitungsverzeichnis Art. 30', category: 'sdk', icon: '📋', url: '/sdk/vvt' },
|
||||
{ id: 'admin-dsfa', name: 'DSFA', description: 'Datenschutz-Folgenabschaetzung', category: 'sdk', icon: '⚖️', url: '/sdk/dsfa' },
|
||||
{ id: 'admin-tom', name: 'TOMs', description: 'Technische & Org. Massnahmen', category: 'sdk', icon: '🛡️', url: '/sdk/tom' },
|
||||
{ id: 'admin-loeschfristen', name: 'Loeschfristen', description: 'Aufbewahrung & Deadlines', category: 'sdk', icon: '🗑️', url: '/sdk/loeschfristen' },
|
||||
{ id: 'admin-advisory-board', name: 'Advisory Board', description: 'KI-Use-Case Pruefung', category: 'sdk', icon: '🧑⚖️', url: '/sdk/advisory-board' },
|
||||
{ id: 'admin-escalations', name: 'Eskalations-Queue', description: 'DSB Review & Freigabe', category: 'sdk', icon: '🚨', url: '/sdk/escalations' },
|
||||
// Compliance - Audit, GRC & Regulatorik
|
||||
{ id: 'admin-compliance-hub', name: 'Compliance Hub', description: 'Zentrales GRC Dashboard', category: 'sdk', icon: '✅', url: '/sdk/compliance-hub' },
|
||||
{ id: 'admin-audit-checklist', name: 'Audit Checkliste', description: '476 Anforderungen pruefen', category: 'sdk', icon: '📋', url: '/sdk/audit-checklist' },
|
||||
{ id: 'admin-requirements', name: 'Requirements', description: '558+ aus 19 Verordnungen', category: 'sdk', icon: '📜', url: '/sdk/requirements' },
|
||||
{ id: 'admin-controls', name: 'Controls', description: '474 Control-Mappings', category: 'sdk', icon: '🎛️', url: '/sdk/controls' },
|
||||
{ id: 'admin-evidence', name: 'Evidence', description: 'Nachweise & Dokumentation', category: 'sdk', icon: '📎', url: '/sdk/evidence' },
|
||||
{ id: 'admin-risks', name: 'Risiken', description: 'Risk Matrix & Register', category: 'sdk', icon: '⚠️', url: '/sdk/risks' },
|
||||
{ id: 'admin-audit-report', name: 'Audit Report', description: 'PDF Audit-Berichte', category: 'sdk', icon: '📊', url: '/sdk/audit-report' },
|
||||
{ id: 'admin-modules', name: 'Service Registry', description: '30+ Service-Module', category: 'sdk', icon: '🔧', url: '/sdk/modules' },
|
||||
{ id: 'admin-dsms', name: 'DSMS', description: 'Datenschutz-Management-System', category: 'sdk', icon: '🏛️', url: '/sdk/dsms' },
|
||||
{ id: 'admin-compliance-workflow', name: 'Workflow', description: 'Freigabe-Workflows', category: 'sdk', icon: '🔄', url: '/sdk/workflow' },
|
||||
{ id: 'admin-source-policy', name: 'Quellen-Policy', description: 'Datenquellen & Compliance', category: 'sdk', icon: '📚', url: '/sdk/source-policy' },
|
||||
{ id: 'admin-ai-act', name: 'EU-AI-Act', description: 'KI-Risikoklassifizierung', category: 'sdk', icon: '🤖', url: '/sdk/ai-act' },
|
||||
{ id: 'admin-obligations', name: 'Pflichten', description: 'NIS2, DSGVO, AI Act', category: 'sdk', icon: '⚡', url: '/sdk/obligations' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG (Teal #14b8a6) ===
|
||||
{ id: 'admin-llm-compare', name: 'LLM Vergleich', description: 'KI-Provider Vergleich', category: 'ai', icon: '🤖', url: '/ai/llm-compare' },
|
||||
{ id: 'admin-rag', name: 'Daten & RAG', description: 'Training Data & RAG', category: 'ai', icon: '🗄️', url: '/ai/rag' },
|
||||
{ id: 'admin-ocr-labeling', name: 'OCR-Labeling', description: 'Handschrift-Training', category: 'ai', icon: '✍️', url: '/ai/ocr-labeling' },
|
||||
{ id: 'admin-magic-help', name: 'Magic Help', description: 'TrOCR Handschrift-OCR', category: 'ai', icon: '🪄', url: '/ai/magic-help' },
|
||||
{ id: 'admin-klausur-korrektur', name: 'Klausur-Korrektur', description: 'Abitur-Korrektur mit KI', category: 'ai', icon: '📝', url: '/ai/klausur-korrektur' },
|
||||
{ id: 'admin-quality', name: 'Qualitaet & Audit', description: 'Compliance-Audit & Traceability', category: 'ai', icon: '✨', url: '/ai/quality' },
|
||||
{ id: 'admin-test-quality', name: 'Test Quality (BQAS)', description: 'Golden Suite & Synthetic Tests', category: 'ai', icon: '🧪', url: '/ai/test-quality' },
|
||||
{ id: 'admin-agents', name: 'Agent Management', description: 'Multi-Agent & SOUL-Editor', category: 'ai', icon: '🧠', url: '/ai/agents' },
|
||||
|
||||
// === INFRASTRUKTUR (Orange #f97316) ===
|
||||
{ id: 'admin-gpu', name: 'GPU Infrastruktur', description: 'vast.ai GPU Management', category: 'infrastructure', icon: '🖥️', url: '/infrastructure/gpu' },
|
||||
{ id: 'admin-middleware', name: 'Middleware', description: 'Stack & API Gateway', category: 'infrastructure', icon: '🔧', url: '/infrastructure/middleware' },
|
||||
{ id: 'admin-security', name: 'Security', description: 'DevSecOps & Scans', category: 'infrastructure', icon: '🔐', url: '/infrastructure/security' },
|
||||
{ id: 'admin-sbom', name: 'SBOM', description: 'Software Bill of Materials', category: 'infrastructure', icon: '📦', url: '/infrastructure/sbom' },
|
||||
{ id: 'admin-cicd', name: 'CI/CD', description: 'Pipelines & Deployments', category: 'infrastructure', icon: '🔄', url: '/infrastructure/ci-cd' },
|
||||
{ id: 'admin-tests', name: 'Test Dashboard', description: '195+ Tests & Coverage', category: 'infrastructure', icon: '🧪', url: '/infrastructure/tests' },
|
||||
|
||||
// === BILDUNG (Blue #3b82f6) ===
|
||||
{ id: 'admin-edu-search', name: 'Education Search', description: 'Bildungsquellen & Crawler', category: 'education', icon: '🔍', url: '/education/edu-search' },
|
||||
{ id: 'admin-zeugnisse', name: 'Zeugnisse-Crawler', description: 'Zeugnis-Daten', category: 'education', icon: '📜', url: '/education/zeugnisse-crawler' },
|
||||
{ id: 'admin-rag-pipeline', name: 'RAG Pipeline', description: 'Bildungsdokumente indexieren', category: 'ai', icon: '🔗', url: '/ai/rag-pipeline' },
|
||||
{ id: 'admin-foerderantrag', name: 'Foerderantrag-Wizard', description: 'DigitalPakt & Landesfoerderung', category: 'education', icon: '💰', url: '/education/foerderantrag' },
|
||||
|
||||
// === KOMMUNIKATION (Green #22c55e) ===
|
||||
{ id: 'admin-video', name: 'Video & Chat', description: 'Matrix & Jitsi Monitoring', category: 'communication', icon: '🎥', url: '/communication/video-chat' },
|
||||
{ id: 'admin-matrix', name: 'Voice Service', description: 'Voice-First Interface', category: 'communication', icon: '🎙️', url: '/communication/matrix' },
|
||||
{ id: 'admin-mail', name: 'Unified Inbox', description: 'E-Mail & KI-Analyse', category: 'communication', icon: '📧', url: '/communication/mail' },
|
||||
{ id: 'admin-alerts', name: 'Alerts Monitoring', description: 'Google Alerts & Feeds', category: 'communication', icon: '🔔', url: '/communication/alerts' },
|
||||
|
||||
// === ENTWICKLUNG (Slate #64748b) ===
|
||||
{ id: 'admin-workflow', name: 'Dev Workflow', description: 'Git, CI/CD & Team-Regeln', category: 'development', icon: '⚡', url: '/development/workflow' },
|
||||
{ id: 'admin-game', name: 'Breakpilot Drive', description: 'Lernspiel Management', category: 'development', icon: '🎮', url: '/development/game' },
|
||||
{ id: 'admin-unity', name: 'Unity Bridge', description: 'Unity Editor Steuerung', category: 'development', icon: '🎯', url: '/development/unity-bridge' },
|
||||
{ id: 'admin-companion', name: 'Companion Dev', description: 'Lesson-Modus Entwicklung', category: 'development', icon: '📚', url: '/development/companion' },
|
||||
{ id: 'admin-docs', name: 'Developer Docs', description: 'API & Architektur', category: 'development', icon: '📖', url: '/development/docs' },
|
||||
{ id: 'admin-brandbook', name: 'Brandbook', description: 'Corporate Design', category: 'development', icon: '🎨', url: '/development/brandbook' },
|
||||
{ id: 'admin-screen-flow', name: 'Screen Flow', description: 'UI Screen-Verbindungen', category: 'development', icon: '🔀', url: '/development/screen-flow' },
|
||||
{ id: 'admin-content', name: 'Uebersetzungen', description: 'Website Content & Sprachen', category: 'development', icon: '🌐', url: '/development/content' },
|
||||
]
|
||||
|
||||
const ADMIN_CONNECTIONS: ConnectionDef[] = [
|
||||
// === OVERVIEW/META FLOWS ===
|
||||
{ source: 'admin-dashboard', target: 'admin-onboarding', label: 'Erste Schritte' },
|
||||
{ source: 'admin-dashboard', target: 'admin-architecture', label: 'System' },
|
||||
{ source: 'admin-dashboard', target: 'admin-backlog', label: 'Go-Live' },
|
||||
{ source: 'admin-dashboard', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||
{ source: 'admin-onboarding', target: 'admin-consent' },
|
||||
{ source: 'admin-onboarding', target: 'admin-llm-compare' },
|
||||
{ source: 'admin-rbac', target: 'admin-consent' },
|
||||
|
||||
// === DSGVO FLOW ===
|
||||
{ source: 'admin-consent', target: 'admin-einwilligungen', label: 'Nutzer' },
|
||||
{ source: 'admin-consent', target: 'admin-dsr' },
|
||||
{ source: 'admin-dsr', target: 'admin-loeschfristen' },
|
||||
{ source: 'admin-vvt', target: 'admin-tom' },
|
||||
{ source: 'admin-vvt', target: 'admin-dsfa' },
|
||||
{ source: 'admin-dsfa', target: 'admin-tom' },
|
||||
{ source: 'admin-advisory-board', target: 'admin-escalations', label: 'Eskalation' },
|
||||
{ source: 'admin-advisory-board', target: 'admin-dsfa', label: 'Risiko' },
|
||||
|
||||
// === COMPLIANCE FLOW ===
|
||||
{ source: 'admin-compliance-hub', target: 'admin-audit-checklist', label: 'Audit' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-requirements', label: 'Anforderungen' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-risks', label: 'Risiken' },
|
||||
{ source: 'admin-compliance-hub', target: 'admin-ai-act', label: 'AI Act' },
|
||||
{ source: 'admin-requirements', target: 'admin-controls' },
|
||||
{ source: 'admin-controls', target: 'admin-evidence' },
|
||||
{ source: 'admin-audit-checklist', target: 'admin-audit-report', label: 'Report' },
|
||||
{ source: 'admin-risks', target: 'admin-controls' },
|
||||
{ source: 'admin-modules', target: 'admin-controls' },
|
||||
{ source: 'admin-source-policy', target: 'admin-rag' },
|
||||
{ source: 'admin-obligations', target: 'admin-requirements' },
|
||||
{ source: 'admin-dsms', target: 'admin-compliance-workflow' },
|
||||
|
||||
// === KI & AUTOMATISIERUNG FLOW ===
|
||||
{ source: 'admin-llm-compare', target: 'admin-rag', label: 'Daten' },
|
||||
{ source: 'admin-rag', target: 'admin-quality' },
|
||||
{ source: 'admin-rag', target: 'admin-agents' },
|
||||
{ source: 'admin-ocr-labeling', target: 'admin-magic-help', label: 'Training' },
|
||||
{ source: 'admin-magic-help', target: 'admin-klausur-korrektur', label: 'Korrektur' },
|
||||
{ source: 'admin-quality', target: 'admin-test-quality' },
|
||||
{ source: 'admin-agents', target: 'admin-test-quality', label: 'BQAS' },
|
||||
{ source: 'admin-klausur-korrektur', target: 'admin-quality', label: 'Audit' },
|
||||
|
||||
// === INFRASTRUKTUR FLOW ===
|
||||
{ source: 'admin-security', target: 'admin-sbom', label: 'Dependencies' },
|
||||
{ source: 'admin-sbom', target: 'admin-tests' },
|
||||
{ source: 'admin-tests', target: 'admin-cicd', label: 'Pipeline' },
|
||||
{ source: 'admin-cicd', target: 'admin-middleware' },
|
||||
{ source: 'admin-middleware', target: 'admin-gpu', label: 'GPU' },
|
||||
{ source: 'admin-security', target: 'admin-compliance-hub', label: 'Compliance' },
|
||||
|
||||
// === BILDUNG FLOW ===
|
||||
{ source: 'admin-edu-search', target: 'admin-rag', label: 'Quellen' },
|
||||
{ source: 'admin-edu-search', target: 'admin-zeugnisse' },
|
||||
{ source: 'admin-training', target: 'admin-onboarding' },
|
||||
{ source: 'admin-foerderantrag', target: 'admin-docs', label: 'Docs' },
|
||||
|
||||
// === KOMMUNIKATION FLOW ===
|
||||
{ source: 'admin-video', target: 'admin-matrix', label: 'Voice' },
|
||||
{ source: 'admin-mail', target: 'admin-alerts' },
|
||||
{ source: 'admin-alerts', target: 'admin-mail', label: 'Inbox' },
|
||||
|
||||
// === ENTWICKLUNG FLOW ===
|
||||
{ source: 'admin-workflow', target: 'admin-cicd', label: 'Pipeline' },
|
||||
{ source: 'admin-workflow', target: 'admin-docs' },
|
||||
{ source: 'admin-game', target: 'admin-unity', label: 'Editor' },
|
||||
{ source: 'admin-companion', target: 'admin-agents', label: 'Agents' },
|
||||
{ source: 'admin-brandbook', target: 'admin-screen-flow' },
|
||||
{ source: 'admin-docs', target: 'admin-architecture' },
|
||||
{ source: 'admin-content', target: 'admin-brandbook' },
|
||||
]
|
||||
|
||||
// ============================================
|
||||
// CATEGORY COLORS
|
||||
// ============================================
|
||||
|
||||
const STUDIO_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
navigation: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' },
|
||||
content: { bg: '#dcfce7', border: '#22c55e', text: '#166534' },
|
||||
communication: { bg: '#fef3c7', border: '#f59e0b', text: '#92400e' },
|
||||
school: { bg: '#fce7f3', border: '#ec4899', text: '#9d174d' },
|
||||
admin: { bg: '#f3e8ff', border: '#a855f7', text: '#6b21a8' },
|
||||
ai: { bg: '#cffafe', border: '#06b6d4', text: '#0e7490' },
|
||||
}
|
||||
|
||||
// Colors from navigation.ts
|
||||
const ADMIN_COLORS: Record<string, { bg: string; border: string; text: string }> = {
|
||||
overview: { bg: '#e0f2fe', border: '#0ea5e9', text: '#0369a1' }, // Sky (Meta)
|
||||
dsgvo: { bg: '#ede9fe', border: '#7c3aed', text: '#5b21b6' }, // Violet
|
||||
compliance: { bg: '#f3e8ff', border: '#9333ea', text: '#6b21a8' }, // Purple
|
||||
ai: { bg: '#ccfbf1', border: '#14b8a6', text: '#0f766e' }, // Teal
|
||||
infrastructure: { bg: '#ffedd5', border: '#f97316', text: '#c2410c' },// Orange
|
||||
education: { bg: '#dbeafe', border: '#3b82f6', text: '#1e40af' }, // Blue
|
||||
communication: { bg: '#dcfce7', border: '#22c55e', text: '#166534' }, // Green
|
||||
development: { bg: '#f1f5f9', border: '#64748b', text: '#334155' }, // Slate
|
||||
}
|
||||
|
||||
const STUDIO_LABELS: Record<string, string> = {
|
||||
navigation: 'Navigation',
|
||||
content: 'Content & Tools',
|
||||
communication: 'Kommunikation',
|
||||
school: 'Schulverwaltung',
|
||||
admin: 'Administration',
|
||||
ai: 'KI & Assistent',
|
||||
}
|
||||
|
||||
// Labels from navigation.ts
|
||||
const ADMIN_LABELS: Record<string, string> = {
|
||||
overview: 'Uebersicht & Meta',
|
||||
dsgvo: 'DSGVO',
|
||||
compliance: 'Compliance & GRC',
|
||||
ai: 'KI & Automatisierung',
|
||||
infrastructure: 'Infrastruktur & DevOps',
|
||||
education: 'Bildung & Schule',
|
||||
communication: 'Kommunikation & Alerts',
|
||||
development: 'Entwicklung & Produkte',
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// HELPER: Find all connected nodes (recursive)
|
||||
// ============================================
|
||||
|
||||
function findConnectedNodes(
|
||||
startNodeId: string,
|
||||
connections: ConnectionDef[],
|
||||
direction: 'children' | 'parents' | 'both' = 'children'
|
||||
): Set<string> {
|
||||
const connected = new Set<string>()
|
||||
connected.add(startNodeId)
|
||||
|
||||
const queue = [startNodeId]
|
||||
while (queue.length > 0) {
|
||||
const current = queue.shift()!
|
||||
|
||||
connections.forEach(conn => {
|
||||
if ((direction === 'children' || direction === 'both') && conn.source === current) {
|
||||
if (!connected.has(conn.target)) {
|
||||
connected.add(conn.target)
|
||||
queue.push(conn.target)
|
||||
}
|
||||
}
|
||||
if ((direction === 'parents' || direction === 'both') && conn.target === current) {
|
||||
if (!connected.has(conn.source)) {
|
||||
connected.add(conn.source)
|
||||
queue.push(conn.source)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return connected
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// LAYOUT HELPERS
|
||||
// ============================================
|
||||
|
||||
const getNodePosition = (
|
||||
id: string,
|
||||
category: string,
|
||||
screens: ScreenDefinition[],
|
||||
flowType: FlowType
|
||||
) => {
|
||||
const studioPositions: Record<string, { x: number; y: number }> = {
|
||||
navigation: { x: 400, y: 50 },
|
||||
content: { x: 50, y: 250 },
|
||||
communication: { x: 750, y: 250 },
|
||||
school: { x: 50, y: 500 },
|
||||
admin: { x: 750, y: 500 },
|
||||
ai: { x: 400, y: 380 },
|
||||
}
|
||||
|
||||
const adminPositions: Record<string, { x: number; y: number }> = {
|
||||
overview: { x: 400, y: 30 },
|
||||
dsgvo: { x: 50, y: 150 },
|
||||
compliance: { x: 700, y: 150 },
|
||||
ai: { x: 50, y: 350 },
|
||||
communication: { x: 400, y: 350 },
|
||||
infrastructure: { x: 700, y: 350 },
|
||||
education: { x: 50, y: 550 },
|
||||
development: { x: 400, y: 550 },
|
||||
}
|
||||
|
||||
const positions = flowType === 'studio' ? studioPositions : adminPositions
|
||||
const base = positions[category] || { x: 400, y: 300 }
|
||||
const categoryScreens = screens.filter(s => s.category === category)
|
||||
const categoryIndex = categoryScreens.findIndex(s => s.id === id)
|
||||
|
||||
const cols = Math.ceil(Math.sqrt(categoryScreens.length + 1))
|
||||
const row = Math.floor(categoryIndex / cols)
|
||||
const col = categoryIndex % cols
|
||||
|
||||
return {
|
||||
x: base.x + col * 160,
|
||||
y: base.y + row * 90,
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// MAIN COMPONENT
|
||||
// ============================================
|
||||
|
||||
export default function ScreenFlowPage() {
|
||||
const [flowType, setFlowType] = useState<FlowType>('admin')
|
||||
const [selectedCategory, setSelectedCategory] = useState<string | null>(null)
|
||||
const [selectedNode, setSelectedNode] = useState<string | null>(null)
|
||||
const [previewScreen, setPreviewScreen] = useState<ScreenDefinition | null>(null)
|
||||
|
||||
// Get data based on flow type
|
||||
const screens = flowType === 'studio' ? STUDIO_SCREENS : ADMIN_SCREENS
|
||||
const connections = flowType === 'studio' ? STUDIO_CONNECTIONS : ADMIN_CONNECTIONS
|
||||
const colors = flowType === 'studio' ? STUDIO_COLORS : ADMIN_COLORS
|
||||
const labels = flowType === 'studio' ? STUDIO_LABELS : ADMIN_LABELS
|
||||
const baseUrl = flowType === 'studio' ? 'http://macmini:8000' : 'http://macmini:3002'
|
||||
|
||||
// Calculate connected nodes
|
||||
const connectedNodes = useMemo(() => {
|
||||
if (!selectedNode) return new Set<string>()
|
||||
return findConnectedNodes(selectedNode, connections, 'children')
|
||||
}, [selectedNode, connections])
|
||||
|
||||
// Create nodes with useMemo
|
||||
const initialNodes = useMemo((): Node[] => {
|
||||
return screens.map((screen) => {
|
||||
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
|
||||
const position = getNodePosition(screen.id, screen.category, screens, flowType)
|
||||
|
||||
// Determine opacity
|
||||
let opacity = 1
|
||||
if (selectedNode) {
|
||||
opacity = connectedNodes.has(screen.id) ? 1 : 0.2
|
||||
} else if (selectedCategory) {
|
||||
opacity = screen.category === selectedCategory ? 1 : 0.2
|
||||
}
|
||||
|
||||
const isSelected = selectedNode === screen.id
|
||||
|
||||
return {
|
||||
id: screen.id,
|
||||
type: 'default',
|
||||
position,
|
||||
data: {
|
||||
label: (
|
||||
<div className="text-center p-1">
|
||||
<div className="text-lg mb-1">{screen.icon}</div>
|
||||
<div className="font-medium text-xs leading-tight">{screen.name}</div>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
style: {
|
||||
background: isSelected ? catColors.border : catColors.bg,
|
||||
color: isSelected ? 'white' : catColors.text,
|
||||
border: `2px solid ${catColors.border}`,
|
||||
borderRadius: '12px',
|
||||
padding: '6px',
|
||||
minWidth: '110px',
|
||||
opacity,
|
||||
cursor: 'pointer',
|
||||
boxShadow: isSelected ? `0 0 20px ${catColors.border}` : 'none',
|
||||
},
|
||||
}
|
||||
})
|
||||
}, [screens, colors, flowType, selectedCategory, selectedNode, connectedNodes])
|
||||
|
||||
// Create edges with useMemo
|
||||
const initialEdges = useMemo((): Edge[] => {
|
||||
return connections.map((conn, index) => {
|
||||
const isHighlighted = selectedNode && (conn.source === selectedNode || conn.target === selectedNode)
|
||||
const isInSubtree = selectedNode && connectedNodes.has(conn.source) && connectedNodes.has(conn.target)
|
||||
|
||||
return {
|
||||
id: `e-${conn.source}-${conn.target}-${index}`,
|
||||
source: conn.source,
|
||||
target: conn.target,
|
||||
label: conn.label,
|
||||
type: 'smoothstep',
|
||||
animated: isHighlighted || false,
|
||||
style: {
|
||||
stroke: isHighlighted ? '#3b82f6' : (isInSubtree ? '#94a3b8' : '#e2e8f0'),
|
||||
strokeWidth: isHighlighted ? 3 : 1.5,
|
||||
opacity: selectedNode ? (isInSubtree ? 1 : 0.15) : 1,
|
||||
},
|
||||
labelStyle: { fontSize: 9, fill: '#64748b' },
|
||||
labelBgStyle: { fill: '#f8fafc' },
|
||||
markerEnd: { type: MarkerType.ArrowClosed, color: isHighlighted ? '#3b82f6' : '#94a3b8', width: 15, height: 15 },
|
||||
}
|
||||
})
|
||||
}, [connections, selectedNode, connectedNodes])
|
||||
|
||||
const [nodes, setNodes, onNodesChange] = useNodesState([])
|
||||
const [edges, setEdges, onEdgesChange] = useEdgesState([])
|
||||
|
||||
// Update nodes/edges when dependencies change
|
||||
useEffect(() => {
|
||||
setNodes(initialNodes)
|
||||
setEdges(initialEdges)
|
||||
}, [initialNodes, initialEdges, setNodes, setEdges])
|
||||
|
||||
// Reset when flow type changes
|
||||
const handleFlowTypeChange = useCallback((newType: FlowType) => {
|
||||
setFlowType(newType)
|
||||
setSelectedNode(null)
|
||||
setSelectedCategory(null)
|
||||
setPreviewScreen(null)
|
||||
}, [])
|
||||
|
||||
// Handle node click
|
||||
const onNodeClick = useCallback((_event: React.MouseEvent, node: Node) => {
|
||||
const screen = screens.find(s => s.id === node.id)
|
||||
|
||||
if (selectedNode === node.id) {
|
||||
// Double-click: open in new tab
|
||||
if (screen?.url) {
|
||||
window.open(`${baseUrl}${screen.url}`, '_blank')
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
setSelectedNode(node.id)
|
||||
setSelectedCategory(null)
|
||||
|
||||
if (screen) {
|
||||
setPreviewScreen(screen)
|
||||
}
|
||||
}, [screens, baseUrl, selectedNode])
|
||||
|
||||
// Handle background click - deselect
|
||||
const onPaneClick = useCallback(() => {
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}, [])
|
||||
|
||||
// Stats
|
||||
const stats = {
|
||||
totalScreens: screens.length,
|
||||
totalConnections: connections.length,
|
||||
connectedCount: connectedNodes.size,
|
||||
}
|
||||
|
||||
const categories = Object.keys(labels)
|
||||
|
||||
// Connected screens list
|
||||
const connectedScreens = selectedNode
|
||||
? screens.filter(s => connectedNodes.has(s.id))
|
||||
: []
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Flow Type Selector */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<button
|
||||
onClick={() => handleFlowTypeChange('studio')}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
flowType === 'studio'
|
||||
? 'border-green-500 bg-green-50 shadow-lg'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
|
||||
flowType === 'studio' ? 'bg-green-500 text-white' : 'bg-slate-100'
|
||||
}`}>
|
||||
🎓
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-lg">Studio (Port 8000)</div>
|
||||
<div className="text-sm text-slate-500">Lehrer-Oberflaeche</div>
|
||||
<div className="text-xs text-slate-400 mt-1">{STUDIO_SCREENS.length} Screens</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
<button
|
||||
onClick={() => handleFlowTypeChange('admin')}
|
||||
className={`p-6 rounded-xl border-2 transition-all ${
|
||||
flowType === 'admin'
|
||||
? 'border-primary-500 bg-primary-50 shadow-lg'
|
||||
: 'border-slate-200 bg-white hover:border-slate-300'
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-14 h-14 rounded-xl flex items-center justify-center text-2xl ${
|
||||
flowType === 'admin' ? 'bg-primary-500 text-white' : 'bg-slate-100'
|
||||
}`}>
|
||||
⚙️
|
||||
</div>
|
||||
<div className="text-left">
|
||||
<div className="font-bold text-lg">Admin v2 (Port 3002)</div>
|
||||
<div className="text-sm text-slate-500">Admin Panel</div>
|
||||
<div className="text-xs text-slate-400 mt-1">{ADMIN_SCREENS.length} Screens</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{/* Stats & Selection Info */}
|
||||
<div className="grid grid-cols-4 gap-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||
<div className="text-3xl font-bold text-slate-800">{stats.totalScreens}</div>
|
||||
<div className="text-sm text-slate-500">Screens</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||
<div className="text-3xl font-bold text-primary-600">{stats.totalConnections}</div>
|
||||
<div className="text-sm text-slate-500">Verbindungen</div>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm col-span-2">
|
||||
{selectedNode ? (
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="text-3xl">{previewScreen?.icon}</div>
|
||||
<div>
|
||||
<div className="font-bold text-slate-800">{previewScreen?.name}</div>
|
||||
<div className="text-sm text-slate-500">
|
||||
{stats.connectedCount} verbundene Screen{stats.connectedCount !== 1 ? 's' : ''}
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}}
|
||||
className="ml-auto px-3 py-1 text-sm bg-slate-100 hover:bg-slate-200 rounded-lg"
|
||||
>
|
||||
Zuruecksetzen
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-slate-500 text-sm">
|
||||
Klicke auf einen Screen um den Subtree zu sehen
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Category Filter */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
setSelectedCategory(null)
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}}
|
||||
className={`px-4 py-2 rounded-lg text-sm font-medium transition-colors ${
|
||||
selectedCategory === null && !selectedNode
|
||||
? 'bg-slate-800 text-white'
|
||||
: 'bg-slate-100 text-slate-700 hover:bg-slate-200'
|
||||
}`}
|
||||
>
|
||||
Alle ({screens.length})
|
||||
</button>
|
||||
{categories.map((key) => {
|
||||
const count = screens.filter(s => s.category === key).length
|
||||
const catColors = colors[key] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => {
|
||||
setSelectedCategory(selectedCategory === key ? null : key)
|
||||
setSelectedNode(null)
|
||||
setPreviewScreen(null)
|
||||
}}
|
||||
className="px-4 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2"
|
||||
style={{
|
||||
background: selectedCategory === key ? catColors.border : catColors.bg,
|
||||
color: selectedCategory === key ? 'white' : catColors.text,
|
||||
}}
|
||||
>
|
||||
<span className="w-3 h-3 rounded-full" style={{ background: catColors.border }} />
|
||||
{labels[key]} ({count})
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Connected Screens List */}
|
||||
{selectedNode && connectedScreens.length > 1 && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 shadow-sm">
|
||||
<div className="text-sm font-medium text-slate-700 mb-3">Verbundene Screens:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{connectedScreens.map((screen) => {
|
||||
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
|
||||
const isCurrentNode = screen.id === selectedNode
|
||||
return (
|
||||
<button
|
||||
key={screen.id}
|
||||
onClick={() => {
|
||||
if (screen.url) {
|
||||
window.open(`${baseUrl}${screen.url}`, '_blank')
|
||||
}
|
||||
}}
|
||||
className={`px-3 py-2 rounded-lg text-sm font-medium transition-all flex items-center gap-2 ${
|
||||
isCurrentNode ? 'ring-2 ring-primary-500' : ''
|
||||
}`}
|
||||
style={{
|
||||
background: isCurrentNode ? catColors.border : catColors.bg,
|
||||
color: isCurrentNode ? 'white' : catColors.text,
|
||||
}}
|
||||
>
|
||||
<span>{screen.icon}</span>
|
||||
{screen.name}
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Flow Diagram */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden" style={{ height: '500px' }}>
|
||||
<ReactFlow
|
||||
nodes={nodes}
|
||||
edges={edges}
|
||||
onNodesChange={onNodesChange}
|
||||
onEdgesChange={onEdgesChange}
|
||||
onNodeClick={onNodeClick}
|
||||
onPaneClick={onPaneClick}
|
||||
fitView
|
||||
fitViewOptions={{ padding: 0.2 }}
|
||||
attributionPosition="bottom-left"
|
||||
>
|
||||
<Controls />
|
||||
<MiniMap
|
||||
nodeColor={(node) => {
|
||||
const screen = screens.find(s => s.id === node.id)
|
||||
const catColors = screen ? colors[screen.category] : null
|
||||
return catColors?.border || '#94a3b8'
|
||||
}}
|
||||
maskColor="rgba(0, 0, 0, 0.1)"
|
||||
/>
|
||||
<Background variant={BackgroundVariant.Dots} gap={12} size={1} />
|
||||
|
||||
<Panel position="top-left" className="bg-white/95 p-3 rounded-lg shadow-lg text-xs">
|
||||
<div className="font-medium text-slate-700 mb-2">
|
||||
{flowType === 'studio' ? '🎓 Studio' : '⚙️ Admin v2'}
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
{categories.slice(0, 4).map((key) => {
|
||||
const catColors = colors[key] || { bg: '#f1f5f9', border: '#94a3b8' }
|
||||
return (
|
||||
<div key={key} className="flex items-center gap-2">
|
||||
<span
|
||||
className="w-3 h-3 rounded"
|
||||
style={{ background: catColors.bg, border: `1px solid ${catColors.border}` }}
|
||||
/>
|
||||
<span className="text-slate-600">{labels[key]}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
<div className="mt-2 pt-2 border-t text-slate-400">
|
||||
Klick = Subtree<br/>
|
||||
Doppelklick = Oeffnen
|
||||
</div>
|
||||
</Panel>
|
||||
</ReactFlow>
|
||||
</div>
|
||||
|
||||
{/* Screen List */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 border-b flex items-center justify-between">
|
||||
<h3 className="font-medium text-slate-700">
|
||||
Alle Screens ({screens.length})
|
||||
</h3>
|
||||
<span className="text-xs text-slate-400">{baseUrl}</span>
|
||||
</div>
|
||||
<div className="divide-y max-h-80 overflow-y-auto">
|
||||
{screens
|
||||
.filter(s => !selectedCategory || s.category === selectedCategory)
|
||||
.map((screen) => {
|
||||
const catColors = colors[screen.category] || { bg: '#f1f5f9', border: '#94a3b8', text: '#475569' }
|
||||
return (
|
||||
<button
|
||||
key={screen.id}
|
||||
onClick={() => {
|
||||
setSelectedNode(screen.id)
|
||||
setSelectedCategory(null)
|
||||
setPreviewScreen(screen)
|
||||
}}
|
||||
className="w-full flex items-center gap-4 p-3 hover:bg-slate-50 transition-colors text-left"
|
||||
>
|
||||
<span
|
||||
className="w-9 h-9 rounded-lg flex items-center justify-center text-lg"
|
||||
style={{ background: catColors.bg }}
|
||||
>
|
||||
{screen.icon}
|
||||
</span>
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="font-medium text-slate-800 text-sm">{screen.name}</div>
|
||||
<div className="text-xs text-slate-500 truncate">{screen.description}</div>
|
||||
</div>
|
||||
<span
|
||||
className="px-2 py-1 rounded text-xs font-medium shrink-0"
|
||||
style={{ background: catColors.bg, color: catColors.text }}
|
||||
>
|
||||
{labels[screen.category]}
|
||||
</span>
|
||||
</button>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -19,8 +19,7 @@ import {
|
||||
Eye,
|
||||
Download,
|
||||
AlertTriangle,
|
||||
Info,
|
||||
Container
|
||||
Info
|
||||
} from 'lucide-react'
|
||||
|
||||
interface WorkflowStep {
|
||||
@@ -89,14 +88,6 @@ export default function WorkflowPage() {
|
||||
},
|
||||
{
|
||||
id: 6,
|
||||
title: 'Integration Tests',
|
||||
description: 'Docker Compose Test-Umgebung mit Backend, DB und Consent-Service fuer vollstaendige E2E-Tests.',
|
||||
command: 'docker compose -f docker-compose.test.yml up -d',
|
||||
icon: <Container className="h-6 w-6" />,
|
||||
location: 'macmini'
|
||||
},
|
||||
{
|
||||
id: 7,
|
||||
title: 'Frontend testen',
|
||||
description: 'Teste die Änderungen im Browser auf dem Mac Mini.',
|
||||
command: 'http://macmini:3000',
|
||||
@@ -167,8 +158,8 @@ export default function WorkflowPage() {
|
||||
<span>Browser für Frontend-Tests</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<AlertTriangle className="h-4 w-4 text-amber-500" />
|
||||
<span>Backup manuell (MacBook nachts aus)</span>
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>Tägliches Backup (automatisch)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -201,10 +192,6 @@ export default function WorkflowPage() {
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>PostgreSQL Datenbank</span>
|
||||
</li>
|
||||
<li className="flex items-center gap-2">
|
||||
<CheckCircle2 className="h-4 w-4 text-green-500" />
|
||||
<span>Automatisches Backup (02:00 Uhr lokal)</span>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
@@ -327,18 +314,17 @@ export default function WorkflowPage() {
|
||||
Backup & Sicherheit
|
||||
</h2>
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
|
||||
{/* Mac Mini - Automatisches lokales Backup */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div className="bg-green-50 rounded-xl p-5 border border-green-200">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Clock className="h-5 w-5 text-green-600" />
|
||||
<h3 className="font-semibold text-green-900">Mac Mini (Auto)</h3>
|
||||
<h3 className="font-semibold text-green-900">Tägliches Backup</h3>
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm text-green-800">
|
||||
<li>• Automatisch um 02:00 Uhr</li>
|
||||
<li>• PostgreSQL-Dump lokal</li>
|
||||
<li>• Git Repository gesichert</li>
|
||||
<li>• 7 Tage Aufbewahrung</li>
|
||||
<li>• Läuft automatisch um 02:00 Uhr</li>
|
||||
<li>• Git Repository wird synchronisiert</li>
|
||||
<li>• PostgreSQL-Dump wird erstellt</li>
|
||||
<li>• Backups werden 7 Tage aufbewahrt</li>
|
||||
</ul>
|
||||
<div className="mt-4 p-3 bg-green-100 rounded-lg">
|
||||
<code className="text-xs text-green-700 font-mono">
|
||||
@@ -347,29 +333,10 @@ export default function WorkflowPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* MacBook - Manuelles Backup */}
|
||||
<div className="bg-amber-50 rounded-xl p-5 border border-amber-200">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
||||
<h3 className="font-semibold text-amber-900">MacBook (Manuell)</h3>
|
||||
</div>
|
||||
<ul className="space-y-2 text-sm text-amber-800">
|
||||
<li>• MacBook nachts aus (02:00)</li>
|
||||
<li>• Keine Auto-Synchronisation</li>
|
||||
<li>• Backup manuell anstoßen</li>
|
||||
</ul>
|
||||
<div className="mt-4 p-3 bg-amber-100 rounded-lg">
|
||||
<code className="text-xs text-amber-700 font-mono">
|
||||
rsync -avz macmini:~/Projekte/ ~/Projekte/
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Manuelles Backup starten */}
|
||||
<div className="bg-blue-50 rounded-xl p-5 border border-blue-200">
|
||||
<div className="flex items-center gap-3 mb-3">
|
||||
<Download className="h-5 w-5 text-blue-600" />
|
||||
<h3 className="font-semibold text-blue-900">Backup Script</h3>
|
||||
<h3 className="font-semibold text-blue-900">Manuelles Backup</h3>
|
||||
</div>
|
||||
<p className="text-sm text-blue-800 mb-3">
|
||||
Backup jederzeit manuell starten:
|
||||
@@ -523,118 +490,6 @@ export default function WorkflowPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* CI/CD Infrastruktur - Automatisierte OAuth Integration */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900 mb-4 flex items-center gap-2">
|
||||
<Shield className="h-5 w-5 text-indigo-600" />
|
||||
CI/CD Infrastruktur (Automatisiert)
|
||||
</h2>
|
||||
|
||||
<div className="bg-blue-50 rounded-xl p-4 mb-6 border border-blue-200">
|
||||
<div className="flex items-start gap-3">
|
||||
<Info className="h-5 w-5 text-blue-600 flex-shrink-0 mt-0.5" />
|
||||
<div>
|
||||
<h4 className="font-medium text-blue-900">Warum automatisiert?</h4>
|
||||
<p className="text-sm text-blue-800 mt-1">
|
||||
Die OAuth-Integration zwischen Woodpecker und Gitea ist vollautomatisiert.
|
||||
Dies ist eine DevSecOps Best Practice: Credentials werden in HashiCorp Vault gespeichert
|
||||
und können bei Bedarf automatisch regeneriert werden.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
{/* Architektur */}
|
||||
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
|
||||
<h3 className="font-semibold text-slate-900 mb-3">Architektur</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="flex items-center gap-3 p-2 bg-white rounded-lg border">
|
||||
<div className="w-3 h-3 bg-green-500 rounded-full" />
|
||||
<span className="font-medium">Gitea</span>
|
||||
<span className="text-slate-500">Port 3003</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">Git Server</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<ArrowRight className="h-4 w-4 text-slate-400 rotate-90" />
|
||||
<span className="text-xs text-slate-500 ml-2">OAuth 2.0</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-2 bg-white rounded-lg border">
|
||||
<div className="w-3 h-3 bg-blue-500 rounded-full" />
|
||||
<span className="font-medium">Woodpecker</span>
|
||||
<span className="text-slate-500">Port 8090</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">CI/CD Server</span>
|
||||
</div>
|
||||
<div className="flex items-center justify-center">
|
||||
<ArrowRight className="h-4 w-4 text-slate-400 rotate-90" />
|
||||
<span className="text-xs text-slate-500 ml-2">Credentials</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-3 p-2 bg-white rounded-lg border">
|
||||
<div className="w-3 h-3 bg-purple-500 rounded-full" />
|
||||
<span className="font-medium">Vault</span>
|
||||
<span className="text-slate-500">Port 8200</span>
|
||||
<span className="text-xs text-slate-400 ml-auto">Secrets Manager</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Credentials Speicherort */}
|
||||
<div className="bg-slate-50 rounded-xl p-5 border border-slate-200">
|
||||
<h3 className="font-semibold text-slate-900 mb-3">Credentials Speicherorte</h3>
|
||||
<div className="space-y-3 text-sm">
|
||||
<div className="p-3 bg-white rounded-lg border">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Database className="h-4 w-4 text-purple-500" />
|
||||
<span className="font-medium">HashiCorp Vault</span>
|
||||
</div>
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded">
|
||||
secret/cicd/woodpecker
|
||||
</code>
|
||||
<p className="text-xs text-slate-500 mt-1">Client ID + Secret (Quelle der Wahrheit)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded-lg border">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<FileCode className="h-4 w-4 text-blue-500" />
|
||||
<span className="font-medium">.env Datei</span>
|
||||
</div>
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded">
|
||||
WOODPECKER_GITEA_CLIENT/SECRET
|
||||
</code>
|
||||
<p className="text-xs text-slate-500 mt-1">Für Docker Compose (aus Vault geladen)</p>
|
||||
</div>
|
||||
<div className="p-3 bg-white rounded-lg border">
|
||||
<div className="flex items-center gap-2 mb-1">
|
||||
<Database className="h-4 w-4 text-green-500" />
|
||||
<span className="font-medium">Gitea PostgreSQL</span>
|
||||
</div>
|
||||
<code className="text-xs bg-slate-100 px-2 py-1 rounded">
|
||||
oauth2_application
|
||||
</code>
|
||||
<p className="text-xs text-slate-500 mt-1">OAuth App Registration (gehashtes Secret)</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Troubleshooting */}
|
||||
<div className="mt-6 bg-amber-50 rounded-xl p-5 border border-amber-200">
|
||||
<h3 className="font-semibold text-amber-900 mb-3 flex items-center gap-2">
|
||||
<AlertTriangle className="h-5 w-5 text-amber-600" />
|
||||
Troubleshooting: OAuth Fehler beheben
|
||||
</h3>
|
||||
<p className="text-sm text-amber-800 mb-3">
|
||||
Falls der Fehler "Client ID not registered" oder "user does not exist" auftritt:
|
||||
</p>
|
||||
<div className="bg-slate-800 rounded-lg p-4 font-mono text-sm">
|
||||
<p className="text-slate-400"># Credentials automatisch regenerieren</p>
|
||||
<p className="text-green-400">./scripts/sync-woodpecker-credentials.sh --regenerate</p>
|
||||
<p className="text-slate-400 mt-2"># Oder manuell: Vault → Gitea → .env → Restart</p>
|
||||
<p className="text-green-400">rsync .env macmini:~/Projekte/breakpilot-pwa/</p>
|
||||
<p className="text-green-400">ssh macmini "cd ~/Projekte/breakpilot-pwa && docker compose up -d --force-recreate woodpecker-server"</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Team Members Info */}
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h2 className="text-xl font-semibold text-slate-900 mb-4 flex items-center gap-2">
|
||||
|
||||
+77
-30
@@ -1,13 +1,14 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* UCCA System Documentation Page (SDK Version)
|
||||
* UCCA - System Documentation Page
|
||||
*
|
||||
* Displays architecture documentation, auditor information,
|
||||
* and transparency data for the UCCA compliance system.
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
import Link from 'next/link'
|
||||
|
||||
// ============================================================================
|
||||
@@ -44,6 +45,16 @@ interface Control {
|
||||
effort?: string
|
||||
}
|
||||
|
||||
interface LegalCorpusStats {
|
||||
total_chunks: number
|
||||
regulations: {
|
||||
code: string
|
||||
name: string
|
||||
chunks: number
|
||||
type: string
|
||||
}[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// API Configuration
|
||||
// ============================================================================
|
||||
@@ -60,12 +71,15 @@ export default function DocumentationPage() {
|
||||
const [patterns, setPatterns] = useState<Pattern[]>([])
|
||||
const [controls, setControls] = useState<Control[]>([])
|
||||
const [policyVersion, setPolicyVersion] = useState<string>('')
|
||||
const [legalStats, setLegalStats] = useState<LegalCorpusStats | null>(null)
|
||||
const [loading, setLoading] = useState(false)
|
||||
|
||||
// Fetch rules, patterns, and controls for transparency
|
||||
useEffect(() => {
|
||||
const fetchData = async () => {
|
||||
setLoading(true)
|
||||
try {
|
||||
// Fetch rules
|
||||
const rulesRes = await fetch(`${API_BASE}/sdk/v1/ucca/rules`, {
|
||||
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000000' }
|
||||
})
|
||||
@@ -75,6 +89,7 @@ export default function DocumentationPage() {
|
||||
setPolicyVersion(rulesData.policy_version || '')
|
||||
}
|
||||
|
||||
// Fetch patterns
|
||||
const patternsRes = await fetch(`${API_BASE}/sdk/v1/ucca/patterns`, {
|
||||
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000000' }
|
||||
})
|
||||
@@ -83,6 +98,7 @@ export default function DocumentationPage() {
|
||||
setPatterns(patternsData.patterns || [])
|
||||
}
|
||||
|
||||
// Fetch controls
|
||||
const controlsRes = await fetch(`${API_BASE}/sdk/v1/ucca/controls`, {
|
||||
headers: { 'X-Tenant-ID': '00000000-0000-0000-0000-000000000000' }
|
||||
})
|
||||
@@ -108,13 +124,15 @@ export default function DocumentationPage() {
|
||||
<div className="space-y-6">
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<div className="text-4xl mb-3">📋</div>
|
||||
<h3 className="font-semibold text-slate-800 mb-2">Deterministische Regeln</h3>
|
||||
<div className="text-3xl font-bold text-purple-600">{rules.length}</div>
|
||||
<div className="text-3xl font-bold text-primary-600">{rules.length}</div>
|
||||
<p className="text-sm text-slate-500 mt-2">
|
||||
Alle Entscheidungen basieren auf transparenten, nachvollziehbaren Regeln.
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<div className="text-4xl mb-3">🏗️</div>
|
||||
<h3 className="font-semibold text-slate-800 mb-2">Architektur-Patterns</h3>
|
||||
<div className="text-3xl font-bold text-green-600">{patterns.length}</div>
|
||||
<p className="text-sm text-slate-500 mt-2">
|
||||
@@ -122,6 +140,7 @@ export default function DocumentationPage() {
|
||||
</p>
|
||||
</div>
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6 shadow-sm">
|
||||
<div className="text-4xl mb-3">🛡️</div>
|
||||
<h3 className="font-semibold text-slate-800 mb-2">Compliance-Kontrollen</h3>
|
||||
<div className="text-3xl font-bold text-blue-600">{controls.length}</div>
|
||||
<p className="text-sm text-slate-500 mt-2">
|
||||
@@ -130,15 +149,15 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-gradient-to-br from-purple-50 to-blue-50 rounded-xl border border-purple-200 p-6">
|
||||
<h3 className="font-semibold text-purple-800 text-lg mb-4">Was ist UCCA?</h3>
|
||||
<div className="bg-gradient-to-br from-primary-50 to-blue-50 rounded-xl border border-primary-200 p-6">
|
||||
<h3 className="font-semibold text-primary-800 text-lg mb-4">Was ist UCCA?</h3>
|
||||
<div className="prose prose-sm max-w-none text-slate-700">
|
||||
<p>
|
||||
<strong>UCCA (Use-Case Compliance & Feasibility Advisor)</strong> ist ein deterministisches
|
||||
Compliance-Pruefwerkzeug, das Organisationen bei der Bewertung geplanter KI-Anwendungsfaelle
|
||||
hinsichtlich ihrer datenschutzrechtlichen Zulaessigkeit unterstuetzt.
|
||||
</p>
|
||||
<h4 className="text-purple-700 mt-4">Kernprinzipien</h4>
|
||||
<h4 className="text-primary-700 mt-4">Kernprinzipien</h4>
|
||||
<ul className="space-y-2">
|
||||
<li>
|
||||
<strong>Determinismus:</strong> Alle Entscheidungen basieren auf transparenten Regeln.
|
||||
@@ -159,7 +178,8 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-xl p-6">
|
||||
<h3 className="font-semibold text-amber-800 mb-3">
|
||||
<h3 className="font-semibold text-amber-800 mb-3 flex items-center gap-2">
|
||||
<span>⚠️</span>
|
||||
Wichtiger Hinweis zur KI-Nutzung
|
||||
</h3>
|
||||
<p className="text-amber-700">
|
||||
@@ -177,14 +197,15 @@ export default function DocumentationPage() {
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-6">
|
||||
<h3 className="font-semibold text-slate-800 text-lg mb-4">Systemarchitektur</h3>
|
||||
|
||||
{/* ASCII Diagram */}
|
||||
<div className="bg-slate-900 text-green-400 p-6 rounded-lg font-mono text-sm overflow-x-auto">
|
||||
<pre>{`
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ Frontend (Next.js) │
|
||||
│ admin-v2:3000/sdk/advisory-board │
|
||||
└───────────────────────────────────┬─────────────────────────────────┘
|
||||
│ HTTPS
|
||||
▼
|
||||
│ admin-v2:3000/dsgvo/advisory-board │
|
||||
└───────────────────────────────┬─────────────────────────────────────┘
|
||||
│ HTTPS
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────────────┐
|
||||
│ AI Compliance SDK (Go) │
|
||||
│ Port 8090 │
|
||||
@@ -305,7 +326,8 @@ export default function DocumentationPage() {
|
||||
<h4 className="font-medium text-slate-800 mb-2">1. Zweck des Systems</h4>
|
||||
<p className="text-sm text-slate-600">
|
||||
UCCA ist ein Compliance-Pruefwerkzeug zur Bewertung geplanter KI-Anwendungsfaelle
|
||||
hinsichtlich ihrer datenschutzrechtlichen Zulaessigkeit.
|
||||
hinsichtlich ihrer datenschutzrechtlichen Zulaessigkeit. Es unterstuetzt Organisationen
|
||||
bei der Einhaltung der DSGVO und des AI Acts.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -391,6 +413,27 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-4">
|
||||
<a
|
||||
href="/api/ucca/documentation/architecture.md"
|
||||
download
|
||||
className="flex-1 p-4 bg-primary-50 rounded-lg border border-primary-200 text-center hover:bg-primary-100"
|
||||
>
|
||||
<div className="text-2xl mb-2">📄</div>
|
||||
<div className="font-medium text-primary-800">ARCHITECTURE.md herunterladen</div>
|
||||
<div className="text-sm text-primary-600">Technische Dokumentation</div>
|
||||
</a>
|
||||
<a
|
||||
href="/api/ucca/documentation/auditor.md"
|
||||
download
|
||||
className="flex-1 p-4 bg-green-50 rounded-lg border border-green-200 text-center hover:bg-green-100"
|
||||
>
|
||||
<div className="text-2xl mb-2">📋</div>
|
||||
<div className="font-medium text-green-800">AUDITOR_DOCUMENTATION.md</div>
|
||||
<div className="text-sm text-green-600">Art. 30 DSGVO konform</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
@@ -410,6 +453,7 @@ export default function DocumentationPage() {
|
||||
<div className="text-center py-8 text-slate-500">Lade Regeln...</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
{/* Group by category */}
|
||||
{Array.from(new Set(rules.map(r => r.category))).map(category => (
|
||||
<div key={category} className="bg-white rounded-xl border border-slate-200 overflow-hidden">
|
||||
<div className="px-4 py-3 bg-slate-50 border-b border-slate-200">
|
||||
@@ -495,7 +539,7 @@ export default function DocumentationPage() {
|
||||
<h3 className="font-semibold text-slate-800 mb-4">Verwendung im System</h3>
|
||||
<div className="space-y-3">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 text-purple-700 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
1
|
||||
</div>
|
||||
<div>
|
||||
@@ -506,7 +550,7 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 text-purple-700 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
2
|
||||
</div>
|
||||
<div>
|
||||
@@ -517,7 +561,7 @@ export default function DocumentationPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-8 h-8 bg-purple-100 text-purple-700 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
<div className="w-8 h-8 bg-primary-100 text-primary-700 rounded-full flex items-center justify-center flex-shrink-0">
|
||||
3
|
||||
</div>
|
||||
<div>
|
||||
@@ -536,12 +580,12 @@ export default function DocumentationPage() {
|
||||
// Tabs Configuration
|
||||
// ============================================================================
|
||||
|
||||
const tabs: { id: DocTab; label: string }[] = [
|
||||
{ id: 'overview', label: 'Uebersicht' },
|
||||
{ id: 'architecture', label: 'Architektur' },
|
||||
{ id: 'auditor', label: 'Fuer Auditoren' },
|
||||
{ id: 'rules', label: 'Regel-Katalog' },
|
||||
{ id: 'legal-corpus', label: 'Legal RAG' },
|
||||
const tabs: { id: DocTab; label: string; icon: string }[] = [
|
||||
{ id: 'overview', label: 'Uebersicht', icon: '🏠' },
|
||||
{ id: 'architecture', label: 'Architektur', icon: '🏗️' },
|
||||
{ id: 'auditor', label: 'Fuer Auditoren', icon: '📋' },
|
||||
{ id: 'rules', label: 'Regel-Katalog', icon: '📜' },
|
||||
{ id: 'legal-corpus', label: 'Legal RAG', icon: '⚖️' },
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
@@ -551,17 +595,19 @@ export default function DocumentationPage() {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="bg-white rounded-xl shadow-sm border p-6 flex-1">
|
||||
<h1 className="text-2xl font-bold text-slate-900">UCCA System-Dokumentation</h1>
|
||||
<p className="text-slate-500 mt-1">
|
||||
Transparente Dokumentation des UCCA-Systems fuer Entwickler, Auditoren und Datenschutzbeauftragte.
|
||||
</p>
|
||||
</div>
|
||||
<PagePurpose
|
||||
title="System-Dokumentation"
|
||||
purpose="Transparente Dokumentation des UCCA-Systems fuer Entwickler, Auditoren und Datenschutzbeauftragte. Alle Regeln, Kontrollen und Architektur-Details sind hier einsehbar."
|
||||
audience={['Entwickler', 'DSB', 'Externe Auditoren', 'Rechtsabteilung']}
|
||||
gdprArticles={['Art. 30', 'Art. 32', 'Art. 35']}
|
||||
collapsible={true}
|
||||
defaultCollapsed={true}
|
||||
/>
|
||||
<Link
|
||||
href="/sdk/advisory-board"
|
||||
className="ml-4 px-4 py-2 text-sm text-slate-600 hover:text-slate-800 border border-slate-200 rounded-lg hover:bg-slate-50"
|
||||
href="/dsgvo/advisory-board"
|
||||
className="px-4 py-2 text-sm text-slate-600 hover:text-slate-800 border border-slate-200 rounded-lg hover:bg-slate-50"
|
||||
>
|
||||
Zurueck zum Advisory Board
|
||||
← Zurueck zum Advisory Board
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -574,10 +620,11 @@ export default function DocumentationPage() {
|
||||
onClick={() => setActiveTab(tab.id)}
|
||||
className={`flex items-center gap-2 px-4 py-3 text-sm font-medium whitespace-nowrap transition-colors ${
|
||||
activeTab === tab.id
|
||||
? 'text-purple-600 border-b-2 border-purple-600 bg-purple-50'
|
||||
? 'text-primary-600 border-b-2 border-primary-600 bg-primary-50'
|
||||
: 'text-slate-600 hover:text-slate-800 hover:bg-slate-50'
|
||||
}`}
|
||||
>
|
||||
<span>{tab.icon}</span>
|
||||
{tab.label}
|
||||
</button>
|
||||
))}
|
||||
@@ -0,0 +1,992 @@
|
||||
/**
|
||||
* UCCA Legal Metadata - Kompositorisches Bewertungssystem
|
||||
*
|
||||
* Jedes Feld trägt seine eigene Rechtsgrundlage.
|
||||
* Das Ergebnis ist die Aggregation aller ausgewählten Felder.
|
||||
* Bei Problemen werden Lösungsvorschläge angezeigt.
|
||||
*/
|
||||
|
||||
// ============================================================================
|
||||
// Types
|
||||
// ============================================================================
|
||||
|
||||
export interface LegalReference {
|
||||
article: string // z.B. "Art. 9 DSGVO"
|
||||
title: string // z.B. "Besondere Kategorien personenbezogener Daten"
|
||||
relevance: string // Warum relevant für dieses Feld
|
||||
}
|
||||
|
||||
export interface RequiredControl {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
effort: 'low' | 'medium' | 'high'
|
||||
}
|
||||
|
||||
export interface FieldMetadata {
|
||||
// Identifikation
|
||||
id: string
|
||||
label: string
|
||||
labelSimple: string // Einfache Sprache
|
||||
|
||||
// Rechtliche Einordnung
|
||||
legalRefs: LegalReference[]
|
||||
|
||||
// Risikobewertung
|
||||
riskScore: number // 0-30 pro Feld
|
||||
severity: 'INFO' | 'WARN' | 'BLOCK'
|
||||
|
||||
// Erforderliche Maßnahmen wenn ausgewählt
|
||||
requiredControls: string[]
|
||||
|
||||
// Erklärungen
|
||||
explanation: string // Fachsprache
|
||||
explanationSimple: string // Einfache Sprache
|
||||
|
||||
// Hinweis für Nutzer
|
||||
userHint?: string
|
||||
}
|
||||
|
||||
export interface ProblemSolution {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
// Was ändert sich wenn Lösung akzeptiert wird
|
||||
removes_fields?: string[] // Diese Felder werden "entschärft"
|
||||
adds_controls?: string[] // Diese Kontrollen werden hinzugefügt
|
||||
new_risk_score?: number // Neuer Risiko-Beitrag (meist 0)
|
||||
effort: 'low' | 'medium' | 'high'
|
||||
// Frage an das Team
|
||||
team_question: string
|
||||
}
|
||||
|
||||
export interface Problem {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
severity: 'WARN' | 'BLOCK'
|
||||
// Welche Feld-Kombination löst das Problem aus
|
||||
triggered_by: {
|
||||
all_of?: string[] // Alle müssen ausgewählt sein
|
||||
any_of?: string[] // Mindestens eins muss ausgewählt sein
|
||||
none_of?: string[] // Keins darf ausgewählt sein (z.B. fehlende Einwilligung)
|
||||
}
|
||||
// Rechtliche Grundlage
|
||||
legalRefs: LegalReference[]
|
||||
// Mögliche Lösungen
|
||||
solutions: ProblemSolution[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Erforderliche Kontrollen / Maßnahmen
|
||||
// ============================================================================
|
||||
|
||||
export const CONTROLS: Record<string, RequiredControl> = {
|
||||
explicit_consent: {
|
||||
id: 'explicit_consent',
|
||||
title: 'Ausdrückliche Einwilligung',
|
||||
description: 'Betroffene müssen aktiv und informiert einwilligen (Opt-in, keine vorausgefüllten Checkboxen).',
|
||||
effort: 'medium',
|
||||
},
|
||||
parental_consent: {
|
||||
id: 'parental_consent',
|
||||
title: 'Einwilligung der Erziehungsberechtigten',
|
||||
description: 'Bei Minderjährigen muss die Einwilligung der Eltern/Erziehungsberechtigten eingeholt werden.',
|
||||
effort: 'high',
|
||||
},
|
||||
age_verification: {
|
||||
id: 'age_verification',
|
||||
title: 'Altersverifikation',
|
||||
description: 'Mechanismus zur Prüfung des Alters der Nutzer implementieren.',
|
||||
effort: 'medium',
|
||||
},
|
||||
dsfa: {
|
||||
id: 'dsfa',
|
||||
title: 'Datenschutz-Folgenabschätzung (DSFA)',
|
||||
description: 'Formale DSFA nach Art. 35 DSGVO durchführen und dokumentieren.',
|
||||
effort: 'high',
|
||||
},
|
||||
human_in_the_loop: {
|
||||
id: 'human_in_the_loop',
|
||||
title: 'Menschliche Überprüfung (HITL)',
|
||||
description: 'Jede automatisierte Entscheidung muss von einem Menschen überprüft werden können.',
|
||||
effort: 'medium',
|
||||
},
|
||||
contestation_right: {
|
||||
id: 'contestation_right',
|
||||
title: 'Anfechtungsrecht',
|
||||
description: 'Betroffene müssen automatisierte Entscheidungen anfechten können.',
|
||||
effort: 'low',
|
||||
},
|
||||
data_minimization: {
|
||||
id: 'data_minimization',
|
||||
title: 'Datenminimierung',
|
||||
description: 'Nur die unbedingt notwendigen Daten erheben und verarbeiten.',
|
||||
effort: 'low',
|
||||
},
|
||||
anonymization: {
|
||||
id: 'anonymization',
|
||||
title: 'Anonymisierung',
|
||||
description: 'Personenbezogene Daten vor der Verarbeitung anonymisieren.',
|
||||
effort: 'medium',
|
||||
},
|
||||
pseudonymization: {
|
||||
id: 'pseudonymization',
|
||||
title: 'Pseudonymisierung',
|
||||
description: 'Direkte Identifikatoren durch Pseudonyme ersetzen.',
|
||||
effort: 'medium',
|
||||
},
|
||||
encryption: {
|
||||
id: 'encryption',
|
||||
title: 'Verschlüsselung',
|
||||
description: 'Daten bei Übertragung und Speicherung verschlüsseln.',
|
||||
effort: 'low',
|
||||
},
|
||||
access_logging: {
|
||||
id: 'access_logging',
|
||||
title: 'Zugriffs-Protokollierung',
|
||||
description: 'Alle Zugriffe auf personenbezogene Daten protokollieren.',
|
||||
effort: 'low',
|
||||
},
|
||||
retention_policy: {
|
||||
id: 'retention_policy',
|
||||
title: 'Löschkonzept',
|
||||
description: 'Automatische Löschung nach definierter Aufbewahrungsfrist.',
|
||||
effort: 'medium',
|
||||
},
|
||||
scc: {
|
||||
id: 'scc',
|
||||
title: 'Standardvertragsklauseln (SCC)',
|
||||
description: 'EU-Standardvertragsklauseln mit Drittland-Anbieter abschließen.',
|
||||
effort: 'medium',
|
||||
},
|
||||
tia: {
|
||||
id: 'tia',
|
||||
title: 'Transfer Impact Assessment',
|
||||
description: 'Bewertung der Datenschutzrisiken bei Drittlandtransfer.',
|
||||
effort: 'high',
|
||||
},
|
||||
purpose_limitation: {
|
||||
id: 'purpose_limitation',
|
||||
title: 'Zweckbindung dokumentieren',
|
||||
description: 'Verarbeitungszweck klar definieren und dokumentieren.',
|
||||
effort: 'low',
|
||||
},
|
||||
transparency: {
|
||||
id: 'transparency',
|
||||
title: 'Transparenz-Information',
|
||||
description: 'Betroffene über die Verarbeitung informieren (Datenschutzerklärung).',
|
||||
effort: 'low',
|
||||
},
|
||||
pixelization: {
|
||||
id: 'pixelization',
|
||||
title: 'Verpixelung/Unkenntlichmachung',
|
||||
description: 'Identifizierende Merkmale (Gesichter, Kennzeichen) automatisch verpixeln.',
|
||||
effort: 'medium',
|
||||
},
|
||||
no_training: {
|
||||
id: 'no_training',
|
||||
title: 'Kein KI-Training mit Daten',
|
||||
description: 'Daten dürfen nur für Inferenz, nicht für Training verwendet werden.',
|
||||
effort: 'low',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feld-Metadaten: Datentypen
|
||||
// ============================================================================
|
||||
|
||||
export const DATA_TYPE_METADATA: Record<string, FieldMetadata> = {
|
||||
personal_data: {
|
||||
id: 'personal_data',
|
||||
label: 'Personenbezogene Daten',
|
||||
labelSimple: 'Namen, E-Mails, Adressen',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 4(1) DSGVO', title: 'Definition personenbezogener Daten', relevance: 'Grundlegende Definition' },
|
||||
{ article: 'Art. 6 DSGVO', title: 'Rechtmäßigkeit der Verarbeitung', relevance: 'Rechtsgrundlage erforderlich' },
|
||||
],
|
||||
riskScore: 10,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['purpose_limitation', 'transparency'],
|
||||
explanation: 'Personenbezogene Daten erfordern eine Rechtsgrundlage nach Art. 6 DSGVO.',
|
||||
explanationSimple: 'Wenn Sie Daten verarbeiten, mit denen man Personen identifizieren kann, brauchen Sie einen guten Grund dafür.',
|
||||
},
|
||||
|
||||
article_9_data: {
|
||||
id: 'article_9_data',
|
||||
label: 'Besondere Kategorien (Art. 9)',
|
||||
labelSimple: 'Gesundheit, Religion, politische Meinung',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 9 DSGVO', title: 'Besondere Kategorien personenbezogener Daten', relevance: 'Grundsätzliches Verarbeitungsverbot' },
|
||||
{ article: 'Art. 9(2) DSGVO', title: 'Ausnahmen vom Verbot', relevance: 'Ausdrückliche Einwilligung oder andere Ausnahme erforderlich' },
|
||||
],
|
||||
riskScore: 25,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'dsfa', 'encryption'],
|
||||
explanation: 'Besondere Kategorien personenbezogener Daten sind grundsätzlich verboten. Ausnahmen nur bei ausdrücklicher Einwilligung oder anderen Art. 9(2) Gründen.',
|
||||
explanationSimple: 'Gesundheitsdaten, religiöse Überzeugungen und ähnlich sensible Daten dürfen nur in Ausnahmefällen verarbeitet werden.',
|
||||
userHint: '⚠️ Hohes Risiko - DSFA wahrscheinlich erforderlich',
|
||||
},
|
||||
|
||||
minor_data: {
|
||||
id: 'minor_data',
|
||||
label: 'Daten von Minderjährigen',
|
||||
labelSimple: 'Daten von Kindern/Jugendlichen (unter 18)',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 8 DSGVO', title: 'Bedingungen für die Einwilligung eines Kindes', relevance: 'Besondere Anforderungen an Einwilligung' },
|
||||
{ article: 'ErwGr. 38', title: 'Besonderer Schutz für Kinder', relevance: 'Kinder verdienen besonderen Schutz' },
|
||||
],
|
||||
riskScore: 20,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['parental_consent', 'age_verification', 'data_minimization'],
|
||||
explanation: 'Daten von Minderjährigen erfordern besondere Schutzmaßnahmen. Bei Onlinediensten: Einwilligung ab 16 Jahren (in DE), darunter Elterneinwilligung.',
|
||||
explanationSimple: 'Bei Kindern und Jugendlichen gelten strengere Regeln. Oft müssen die Eltern zustimmen.',
|
||||
userHint: '👶 Besonderer Schutz für Minderjährige erforderlich',
|
||||
},
|
||||
|
||||
license_plates: {
|
||||
id: 'license_plates',
|
||||
label: 'KFZ-Kennzeichen',
|
||||
labelSimple: 'Auto-Kennzeichen',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 4(1) DSGVO', title: 'Personenbezogene Daten', relevance: 'Kennzeichen ermöglichen Identifikation des Halters' },
|
||||
{ article: 'Art. 6 DSGVO', title: 'Rechtmäßigkeit', relevance: 'Rechtsgrundlage für Verarbeitung erforderlich' },
|
||||
],
|
||||
riskScore: 15,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['purpose_limitation', 'retention_policy'],
|
||||
explanation: 'KFZ-Kennzeichen sind personenbezogene Daten, da sie die Identifikation des Halters ermöglichen.',
|
||||
explanationSimple: 'Über ein Kennzeichen kann man den Fahrzeughalter herausfinden - daher sind es persönliche Daten.',
|
||||
userHint: '🚗 Kennzeichen = personenbezogene Daten',
|
||||
},
|
||||
|
||||
images: {
|
||||
id: 'images',
|
||||
label: 'Bilder von Personen',
|
||||
labelSimple: 'Fotos mit erkennbaren Gesichtern',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 4(14) DSGVO', title: 'Biometrische Daten', relevance: 'Gesichtsbilder können biometrische Daten sein' },
|
||||
{ article: '§ 22 KUG', title: 'Recht am eigenen Bild', relevance: 'Einwilligung für Bildveröffentlichung' },
|
||||
],
|
||||
riskScore: 15,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'purpose_limitation'],
|
||||
explanation: 'Bilder von Personen sind personenbezogene Daten. Bei Gesichtserkennung: biometrische Daten (Art. 9).',
|
||||
explanationSimple: 'Fotos von Menschen brauchen deren Erlaubnis. Gesichtserkennung hat noch strengere Regeln.',
|
||||
},
|
||||
|
||||
audio: {
|
||||
id: 'audio',
|
||||
label: 'Sprachaufnahmen',
|
||||
labelSimple: 'Gespräche, Telefonate, Sprachnachrichten',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 4(1) DSGVO', title: 'Personenbezogene Daten', relevance: 'Stimme ermöglicht Identifikation' },
|
||||
{ article: '§ 201 StGB', title: 'Vertraulichkeit des Wortes', relevance: 'Heimliche Aufnahmen sind strafbar' },
|
||||
],
|
||||
riskScore: 15,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'transparency'],
|
||||
explanation: 'Sprachaufnahmen sind personenbezogene Daten. Heimliche Aufnahmen können strafbar sein.',
|
||||
explanationSimple: 'Gespräche aufzunehmen erfordert die Zustimmung aller Beteiligten.',
|
||||
userHint: '🎤 Aufnahme nur mit Wissen der Betroffenen',
|
||||
},
|
||||
|
||||
location_data: {
|
||||
id: 'location_data',
|
||||
label: 'Standortdaten',
|
||||
labelSimple: 'GPS, Aufenthaltsorte, Bewegungsdaten',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 4(1) DSGVO', title: 'Personenbezogene Daten', relevance: 'Standorte ermöglichen Profilbildung' },
|
||||
{ article: 'ErwGr. 75', title: 'Risiken für Betroffene', relevance: 'Bewegungsprofile sind risikobehaftet' },
|
||||
],
|
||||
riskScore: 20,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'data_minimization', 'retention_policy'],
|
||||
explanation: 'Standortdaten ermöglichen detaillierte Bewegungsprofile. Hohes Risiko für Betroffene.',
|
||||
explanationSimple: 'Standortdaten zeigen, wo jemand wann war. Das ist sehr persönlich.',
|
||||
},
|
||||
|
||||
biometric_data: {
|
||||
id: 'biometric_data',
|
||||
label: 'Biometrische Daten',
|
||||
labelSimple: 'Fingerabdrücke, Gesichtserkennung',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 9(1) DSGVO', title: 'Besondere Kategorien', relevance: 'Biometrische Daten zur Identifikation' },
|
||||
{ article: 'Art. 4(14) DSGVO', title: 'Definition biometrischer Daten', relevance: 'Technische Verarbeitung physischer Merkmale' },
|
||||
],
|
||||
riskScore: 30,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'dsfa', 'encryption', 'access_logging'],
|
||||
explanation: 'Biometrische Daten zur eindeutigen Identifikation fallen unter Art. 9 DSGVO.',
|
||||
explanationSimple: 'Fingerabdrücke und Gesichtserkennung sind besonders geschützt.',
|
||||
userHint: '⚠️ Art. 9 DSGVO - Besondere Kategorie',
|
||||
},
|
||||
|
||||
financial_data: {
|
||||
id: 'financial_data',
|
||||
label: 'Finanzdaten',
|
||||
labelSimple: 'Gehälter, Kontodaten, Kreditwürdigkeit',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 6 DSGVO', title: 'Rechtmäßigkeit', relevance: 'Rechtsgrundlage erforderlich' },
|
||||
{ article: '§ 31 BDSG', title: 'Schutz des Wirtschaftsverkehrs', relevance: 'Scoring-Regelungen' },
|
||||
],
|
||||
riskScore: 15,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['encryption', 'access_logging', 'purpose_limitation'],
|
||||
explanation: 'Finanzdaten erfordern besondere Sicherheitsmaßnahmen.',
|
||||
explanationSimple: 'Kontodaten und Gehälter müssen besonders geschützt werden.',
|
||||
},
|
||||
|
||||
employee_data: {
|
||||
id: 'employee_data',
|
||||
label: 'Mitarbeiterdaten',
|
||||
labelSimple: 'Personalakten, Bewertungen, Gehälter',
|
||||
legalRefs: [
|
||||
{ article: '§ 26 BDSG', title: 'Beschäftigtendatenschutz', relevance: 'Besondere Regelungen für Arbeitsverhältnisse' },
|
||||
{ article: 'Art. 88 DSGVO', title: 'Datenverarbeitung im Beschäftigungskontext', relevance: 'Nationale Regelungen möglich' },
|
||||
],
|
||||
riskScore: 15,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['purpose_limitation', 'access_logging', 'transparency'],
|
||||
explanation: 'Beschäftigtendaten unterliegen dem § 26 BDSG. Betriebsrat ggf. einzubinden.',
|
||||
explanationSimple: 'Bei Mitarbeiterdaten gelten besondere Regeln. Der Betriebsrat hat Mitspracherecht.',
|
||||
userHint: '👔 Betriebsrat einbinden',
|
||||
},
|
||||
|
||||
customer_data: {
|
||||
id: 'customer_data',
|
||||
label: 'Kundendaten',
|
||||
labelSimple: 'Bestellungen, Kontaktdaten, Kaufhistorie',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 6(1)(b) DSGVO', title: 'Vertragserfüllung', relevance: 'Oft Rechtsgrundlage für Kundendaten' },
|
||||
],
|
||||
riskScore: 10,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['transparency', 'retention_policy'],
|
||||
explanation: 'Kundendaten können oft auf Basis der Vertragserfüllung verarbeitet werden.',
|
||||
explanationSimple: 'Kundendaten brauchen Sie für Bestellungen - das ist meist erlaubt.',
|
||||
},
|
||||
|
||||
public_data: {
|
||||
id: 'public_data',
|
||||
label: 'Nur öffentliche Daten',
|
||||
labelSimple: 'Keine personenbezogenen Daten',
|
||||
legalRefs: [
|
||||
{ article: 'ErwGr. 26', title: 'Anonyme Informationen', relevance: 'DSGVO gilt nicht für anonyme Daten' },
|
||||
],
|
||||
riskScore: 0,
|
||||
severity: 'INFO',
|
||||
requiredControls: [],
|
||||
explanation: 'Wenn keine personenbezogenen Daten verarbeitet werden, ist die DSGVO nicht anwendbar.',
|
||||
explanationSimple: 'Ohne persönliche Daten gelten die strengen Regeln nicht.',
|
||||
userHint: '✅ Geringes Risiko',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feld-Metadaten: Automatisierung
|
||||
// ============================================================================
|
||||
|
||||
export const AUTOMATION_METADATA: Record<string, FieldMetadata> = {
|
||||
assistive: {
|
||||
id: 'assistive',
|
||||
label: 'Assistierend (KI macht Vorschläge)',
|
||||
labelSimple: 'KI macht Vorschläge, Mensch entscheidet',
|
||||
legalRefs: [],
|
||||
riskScore: 0,
|
||||
severity: 'INFO',
|
||||
requiredControls: [],
|
||||
explanation: 'Bei assistierender KI bleibt der Mensch Entscheider. Art. 22 DSGVO nicht betroffen.',
|
||||
explanationSimple: 'Die KI schlägt vor, Sie entscheiden. Das ist die sicherste Variante.',
|
||||
userHint: '✅ Empfohlen',
|
||||
},
|
||||
|
||||
semi_automated: {
|
||||
id: 'semi_automated',
|
||||
label: 'Teilautomatisiert (Mensch prüft)',
|
||||
labelSimple: 'KI filtert vor, Mensch prüft',
|
||||
legalRefs: [
|
||||
{ article: 'ErwGr. 71', title: 'Profiling und automatisierte Entscheidungen', relevance: 'Menschliche Überprüfung empfohlen' },
|
||||
],
|
||||
riskScore: 10,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['human_in_the_loop'],
|
||||
explanation: 'Teilautomatisierung mit menschlicher Kontrolle ist meist unproblematisch.',
|
||||
explanationSimple: 'Die KI arbeitet vor, aber ein Mensch schaut drüber.',
|
||||
},
|
||||
|
||||
fully_automated: {
|
||||
id: 'fully_automated',
|
||||
label: 'Vollautomatisiert (keine menschliche Prüfung)',
|
||||
labelSimple: 'KI entscheidet alleine',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 22(1) DSGVO', title: 'Automatisierte Einzelentscheidungen', relevance: 'Grundsätzliches Verbot bei rechtlicher Wirkung' },
|
||||
{ article: 'Art. 22(2) DSGVO', title: 'Ausnahmen', relevance: 'Erlaubt bei Vertrag, Gesetz oder Einwilligung' },
|
||||
],
|
||||
riskScore: 25,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['human_in_the_loop', 'contestation_right', 'transparency'],
|
||||
explanation: 'Vollautomatisierte Entscheidungen mit rechtlicher Wirkung sind nach Art. 22 DSGVO grundsätzlich verboten.',
|
||||
explanationSimple: 'Wenn die KI alleine entscheidet und das Auswirkungen auf Menschen hat, ist das problematisch.',
|
||||
userHint: '⚠️ Art. 22 DSGVO beachten',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feld-Metadaten: Zweck
|
||||
// ============================================================================
|
||||
|
||||
export const PURPOSE_METADATA: Record<string, FieldMetadata> = {
|
||||
customer_support: {
|
||||
id: 'customer_support',
|
||||
label: 'Kundenservice',
|
||||
labelSimple: 'Fragen beantworten, Hilfe anbieten',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 6(1)(b) DSGVO', title: 'Vertragserfüllung', relevance: 'Oft Rechtsgrundlage' },
|
||||
],
|
||||
riskScore: 5,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['transparency'],
|
||||
explanation: 'Kundenservice kann meist auf Vertragserfüllung gestützt werden.',
|
||||
explanationSimple: 'Kunden zu helfen ist meist erlaubt.',
|
||||
},
|
||||
|
||||
evaluation_scoring: {
|
||||
id: 'evaluation_scoring',
|
||||
label: 'Bewertung/Scoring von Personen',
|
||||
labelSimple: 'Personen bewerten, Punkte vergeben, einstufen',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 22 DSGVO', title: 'Automatisierte Einzelentscheidungen', relevance: 'Bei automatischem Scoring relevant' },
|
||||
{ article: '§ 31 BDSG', title: 'Scoring', relevance: 'Besondere Regelungen für Scoring' },
|
||||
],
|
||||
riskScore: 20,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['transparency', 'contestation_right', 'dsfa'],
|
||||
explanation: 'Scoring von Personen unterliegt strengen Anforderungen. Bei automatisierten Entscheidungen: Art. 22.',
|
||||
explanationSimple: 'Menschen zu bewerten oder einzustufen ist sensibel. Betroffene müssen das anfechten können.',
|
||||
userHint: '⚠️ Scoring ist risikobehaftet',
|
||||
},
|
||||
|
||||
decision_making: {
|
||||
id: 'decision_making',
|
||||
label: 'Automatisierte Entscheidungen',
|
||||
labelSimple: 'Genehmigungen, Ablehnungen, Zugang',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 22 DSGVO', title: 'Automatisierte Einzelentscheidungen', relevance: 'Kernartikel für automatisierte Entscheidungen' },
|
||||
],
|
||||
riskScore: 25,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['human_in_the_loop', 'contestation_right', 'transparency'],
|
||||
explanation: 'Automatisierte Entscheidungen mit rechtlicher Wirkung erfordern besondere Schutzmaßnahmen.',
|
||||
explanationSimple: 'Wenn die KI über Menschen entscheidet (Kredit, Bewerbung, etc.), gelten strenge Regeln.',
|
||||
userHint: '⚠️ Art. 22 DSGVO prüfen',
|
||||
},
|
||||
|
||||
profiling: {
|
||||
id: 'profiling',
|
||||
label: 'Profiling',
|
||||
labelSimple: 'Personenprofile erstellen, Verhalten analysieren',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 4(4) DSGVO', title: 'Definition Profiling', relevance: 'Automatisierte Verarbeitung zur Bewertung' },
|
||||
{ article: 'Art. 22 DSGVO', title: 'Automatisierte Entscheidungen einschl. Profiling', relevance: 'Bei Entscheidungen aufgrund von Profiling' },
|
||||
],
|
||||
riskScore: 20,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['transparency', 'dsfa'],
|
||||
explanation: 'Profiling ist die automatisierte Bewertung persönlicher Aspekte. Erfordert Transparenz und oft DSFA.',
|
||||
explanationSimple: 'Profile über Menschen zu erstellen erfordert besondere Vorsicht.',
|
||||
},
|
||||
|
||||
marketing: {
|
||||
id: 'marketing',
|
||||
label: 'Marketing/Werbung',
|
||||
labelSimple: 'Werbung, Newsletter, Kampagnen',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 6(1)(f) DSGVO', title: 'Berechtigte Interessen', relevance: 'Direktwerbung kann berechtigtes Interesse sein' },
|
||||
{ article: '§ 7 UWG', title: 'Unzumutbare Belästigung', relevance: 'E-Mail-Werbung nur mit Einwilligung' },
|
||||
],
|
||||
riskScore: 10,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['explicit_consent', 'transparency'],
|
||||
explanation: 'E-Mail-Marketing erfordert i.d.R. Einwilligung (Opt-in).',
|
||||
explanationSimple: 'Für Werbe-E-Mails brauchen Sie die Erlaubnis der Empfänger.',
|
||||
},
|
||||
|
||||
analytics: {
|
||||
id: 'analytics',
|
||||
label: 'Analyse/Statistik',
|
||||
labelSimple: 'Auswertungen, Berichte, Trends',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 6(1)(f) DSGVO', title: 'Berechtigte Interessen', relevance: 'Analysen oft auf berechtigtes Interesse stützbar' },
|
||||
],
|
||||
riskScore: 5,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['data_minimization'],
|
||||
explanation: 'Statistische Analysen sind oft auf berechtigtes Interesse stützbar, wenn datenminimiert.',
|
||||
explanationSimple: 'Auswertungen für interne Zwecke sind meist unproblematisch.',
|
||||
},
|
||||
|
||||
research: {
|
||||
id: 'research',
|
||||
label: 'Forschung',
|
||||
labelSimple: 'Wissenschaftliche Untersuchungen',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 89 DSGVO', title: 'Garantien für Forschungszwecke', relevance: 'Privilegierung von Forschung' },
|
||||
],
|
||||
riskScore: 5,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['data_minimization', 'pseudonymization'],
|
||||
explanation: 'Forschung genießt gewisse Privilegien, erfordert aber Schutzmaßnahmen.',
|
||||
explanationSimple: 'Forschung hat Sonderregeln, wenn die Daten geschützt werden.',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feld-Metadaten: Hosting
|
||||
// ============================================================================
|
||||
|
||||
export const HOSTING_METADATA: Record<string, FieldMetadata> = {
|
||||
eu: {
|
||||
id: 'eu',
|
||||
label: 'EU/EWR',
|
||||
labelSimple: 'In Deutschland oder EU',
|
||||
legalRefs: [],
|
||||
riskScore: 0,
|
||||
severity: 'INFO',
|
||||
requiredControls: [],
|
||||
explanation: 'Hosting in der EU ist datenschutzrechtlich unproblematisch.',
|
||||
explanationSimple: 'Daten in Europa zu speichern ist die einfachste Lösung.',
|
||||
userHint: '✅ Empfohlen',
|
||||
},
|
||||
|
||||
third_country: {
|
||||
id: 'third_country',
|
||||
label: 'Drittland (außerhalb EU)',
|
||||
labelSimple: 'USA, Schweiz, UK, andere',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 44 DSGVO', title: 'Grundsatz für Übermittlung', relevance: 'Besondere Anforderungen an Drittlandtransfer' },
|
||||
{ article: 'Art. 46 DSGVO', title: 'Geeignete Garantien', relevance: 'SCC oder andere Garantien erforderlich' },
|
||||
],
|
||||
riskScore: 15,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['scc', 'tia'],
|
||||
explanation: 'Drittlandtransfer erfordert zusätzliche Garantien (z.B. SCC) und ein Transfer Impact Assessment.',
|
||||
explanationSimple: 'Daten außerhalb der EU zu speichern braucht extra Verträge und Prüfungen.',
|
||||
userHint: '⚠️ Zusätzliche Maßnahmen erforderlich',
|
||||
},
|
||||
|
||||
on_prem: {
|
||||
id: 'on_prem',
|
||||
label: 'On-Premise (eigene Server)',
|
||||
labelSimple: 'Auf unseren eigenen Servern',
|
||||
legalRefs: [],
|
||||
riskScore: 0,
|
||||
severity: 'INFO',
|
||||
requiredControls: ['encryption'],
|
||||
explanation: 'On-Premise bietet volle Kontrolle, erfordert aber eigene Sicherheitsmaßnahmen.',
|
||||
explanationSimple: 'Eigene Server geben volle Kontrolle, aber Sie sind für die Sicherheit verantwortlich.',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Feld-Metadaten: Modell-Nutzung
|
||||
// ============================================================================
|
||||
|
||||
export const MODEL_USAGE_METADATA: Record<string, FieldMetadata> = {
|
||||
rag: {
|
||||
id: 'rag',
|
||||
label: 'RAG (Dokumentensuche)',
|
||||
labelSimple: 'KI durchsucht meine Dokumente',
|
||||
legalRefs: [],
|
||||
riskScore: 5,
|
||||
severity: 'INFO',
|
||||
requiredControls: [],
|
||||
explanation: 'RAG-Ansätze sind datenschutzfreundlich, da keine Daten ins Modell fließen.',
|
||||
explanationSimple: 'Die KI sucht in Ihren Dokumenten, lernt aber nicht daraus. Das ist sicher.',
|
||||
userHint: '✅ Datenschutzfreundlich',
|
||||
},
|
||||
|
||||
inference: {
|
||||
id: 'inference',
|
||||
label: 'Nur Inferenz',
|
||||
labelSimple: 'KI nur nutzen, ohne eigene Daten',
|
||||
legalRefs: [],
|
||||
riskScore: 0,
|
||||
severity: 'INFO',
|
||||
requiredControls: [],
|
||||
explanation: 'Reine Inferenz ohne Datenspeicherung ist unproblematisch.',
|
||||
explanationSimple: 'Die KI nutzen ohne eigene Daten einzugeben ist sicher.',
|
||||
userHint: '✅ Geringes Risiko',
|
||||
},
|
||||
|
||||
finetune: {
|
||||
id: 'finetune',
|
||||
label: 'Fine-Tuning',
|
||||
labelSimple: 'KI mit meinen Daten anpassen',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 5(1)(b) DSGVO', title: 'Zweckbindung', relevance: 'Training ist neuer Zweck' },
|
||||
],
|
||||
riskScore: 20,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'purpose_limitation', 'no_training'],
|
||||
explanation: 'Fine-Tuning mit personenbezogenen Daten erfordert eigene Rechtsgrundlage.',
|
||||
explanationSimple: 'Wenn die KI aus Ihren Daten lernt, ist das ein eigener Verarbeitungsschritt.',
|
||||
userHint: '⚠️ Eigene Rechtsgrundlage erforderlich',
|
||||
},
|
||||
|
||||
training: {
|
||||
id: 'training',
|
||||
label: 'Vollständiges Training',
|
||||
labelSimple: 'KI komplett mit meinen Daten trainieren',
|
||||
legalRefs: [
|
||||
{ article: 'Art. 5(1)(b) DSGVO', title: 'Zweckbindung', relevance: 'Training ist neuer Zweck' },
|
||||
{ article: 'Art. 6 DSGVO', title: 'Rechtmäßigkeit', relevance: 'Eigene Rechtsgrundlage erforderlich' },
|
||||
],
|
||||
riskScore: 25,
|
||||
severity: 'WARN',
|
||||
requiredControls: ['explicit_consent', 'dsfa', 'purpose_limitation'],
|
||||
explanation: 'KI-Training mit personenbezogenen Daten ist ein eigenständiger Verarbeitungszweck.',
|
||||
explanationSimple: 'Die KI komplett mit Ihren Daten zu trainieren braucht klare Einwilligung.',
|
||||
userHint: '⚠️ Hohes Risiko',
|
||||
},
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Probleme & Lösungen
|
||||
// ============================================================================
|
||||
|
||||
export const PROBLEMS: Problem[] = [
|
||||
// KFZ-Kennzeichen ohne Einwilligung
|
||||
{
|
||||
id: 'license_plates_no_consent',
|
||||
title: 'KFZ-Kennzeichen ohne Einwilligung',
|
||||
description: 'Sie möchten KFZ-Kennzeichen verarbeiten, aber haben keine Einwilligung der Fahrzeughalter.',
|
||||
severity: 'BLOCK',
|
||||
triggered_by: {
|
||||
all_of: ['license_plates'],
|
||||
none_of: ['explicit_consent_obtained'],
|
||||
},
|
||||
legalRefs: [
|
||||
{ article: 'Art. 6 DSGVO', title: 'Rechtmäßigkeit', relevance: 'Keine Rechtsgrundlage vorhanden' },
|
||||
],
|
||||
solutions: [
|
||||
{
|
||||
id: 'pixelize_plates',
|
||||
title: 'Kennzeichen automatisch verpixeln',
|
||||
description: 'Die Kennzeichen werden vor der Speicherung automatisch unkenntlich gemacht. Dadurch sind es keine personenbezogenen Daten mehr.',
|
||||
removes_fields: ['license_plates'],
|
||||
adds_controls: ['pixelization'],
|
||||
new_risk_score: 0,
|
||||
effort: 'medium',
|
||||
team_question: 'Ist das Projekt auch mit verpixelten Kennzeichen (nicht lesbar) sinnvoll?',
|
||||
},
|
||||
{
|
||||
id: 'obtain_consent',
|
||||
title: 'Einwilligung einholen',
|
||||
description: 'Die Fahrzeughalter um Einwilligung bitten (z.B. bei Parkhausbetreibern mit Dauerparker-Verträgen).',
|
||||
adds_controls: ['explicit_consent'],
|
||||
new_risk_score: 10,
|
||||
effort: 'high',
|
||||
team_question: 'Können Sie die Einwilligung der Fahrzeughalter einholen?',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Gesichtserkennung ohne Einwilligung
|
||||
{
|
||||
id: 'biometrics_no_consent',
|
||||
title: 'Biometrische Daten ohne Einwilligung',
|
||||
description: 'Sie möchten biometrische Daten (z.B. Gesichtserkennung) verarbeiten, aber haben keine ausdrückliche Einwilligung.',
|
||||
severity: 'BLOCK',
|
||||
triggered_by: {
|
||||
all_of: ['biometric_data'],
|
||||
none_of: ['explicit_consent_obtained'],
|
||||
},
|
||||
legalRefs: [
|
||||
{ article: 'Art. 9(1) DSGVO', title: 'Verarbeitungsverbot', relevance: 'Biometrische Daten sind besondere Kategorie' },
|
||||
{ article: 'Art. 9(2)(a) DSGVO', title: 'Ausdrückliche Einwilligung', relevance: 'Einwilligung als Ausnahme' },
|
||||
],
|
||||
solutions: [
|
||||
{
|
||||
id: 'anonymize_faces',
|
||||
title: 'Gesichter automatisch verpixeln/anonymisieren',
|
||||
description: 'Gesichter werden vor der Speicherung automatisch unkenntlich gemacht.',
|
||||
removes_fields: ['biometric_data'],
|
||||
adds_controls: ['pixelization'],
|
||||
new_risk_score: 0,
|
||||
effort: 'medium',
|
||||
team_question: 'Funktioniert Ihr Projekt auch ohne erkennbare Gesichter?',
|
||||
},
|
||||
{
|
||||
id: 'explicit_biometric_consent',
|
||||
title: 'Ausdrückliche Einwilligung einholen',
|
||||
description: 'Betroffene müssen aktiv und informiert in die Gesichtserkennung einwilligen.',
|
||||
adds_controls: ['explicit_consent', 'dsfa'],
|
||||
new_risk_score: 20,
|
||||
effort: 'high',
|
||||
team_question: 'Können Sie eine ausdrückliche Einwilligung aller Betroffenen sicherstellen?',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Minderjährige + automatisiertes Scoring
|
||||
{
|
||||
id: 'minor_automated_scoring',
|
||||
title: 'Automatisiertes Scoring von Minderjährigen',
|
||||
description: 'Sie möchten Minderjährige automatisiert bewerten oder einstufen. Das ist besonders problematisch.',
|
||||
severity: 'BLOCK',
|
||||
triggered_by: {
|
||||
all_of: ['minor_data', 'evaluation_scoring', 'fully_automated'],
|
||||
},
|
||||
legalRefs: [
|
||||
{ article: 'Art. 22(1) DSGVO', title: 'Verbot automatisierter Entscheidungen', relevance: 'Grundsätzliches Verbot' },
|
||||
{ article: 'Art. 8 DSGVO', title: 'Schutz von Kindern', relevance: 'Besonderer Schutz für Minderjährige' },
|
||||
],
|
||||
solutions: [
|
||||
{
|
||||
id: 'add_human_review',
|
||||
title: 'Menschliche Überprüfung einführen',
|
||||
description: 'Jede Bewertung wird von einem Menschen geprüft bevor sie wirksam wird.',
|
||||
removes_fields: ['fully_automated'],
|
||||
adds_controls: ['human_in_the_loop'],
|
||||
new_risk_score: 15,
|
||||
effort: 'medium',
|
||||
team_question: 'Können Sie sicherstellen, dass ein Mensch jede Bewertung prüft?',
|
||||
},
|
||||
{
|
||||
id: 'remove_scoring',
|
||||
title: 'Auf Scoring verzichten',
|
||||
description: 'Statt Scoring nur informative Auswertungen ohne Entscheidungscharakter.',
|
||||
removes_fields: ['evaluation_scoring'],
|
||||
new_risk_score: 10,
|
||||
effort: 'low',
|
||||
team_question: 'Funktioniert Ihr Projekt auch ohne Bewertung/Scoring der Minderjährigen?',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// Drittland + sensible Daten
|
||||
{
|
||||
id: 'third_country_sensitive',
|
||||
title: 'Sensible Daten im Drittland',
|
||||
description: 'Sie möchten besonders sensible Daten außerhalb der EU verarbeiten. Das erfordert umfangreiche Schutzmaßnahmen.',
|
||||
severity: 'WARN',
|
||||
triggered_by: {
|
||||
all_of: ['third_country'],
|
||||
any_of: ['article_9_data', 'biometric_data', 'minor_data'],
|
||||
},
|
||||
legalRefs: [
|
||||
{ article: 'Art. 44 DSGVO', title: 'Drittlandtransfer', relevance: 'Besondere Anforderungen' },
|
||||
{ article: 'Art. 9 DSGVO', title: 'Sensible Daten', relevance: 'Zusätzlicher Schutz erforderlich' },
|
||||
],
|
||||
solutions: [
|
||||
{
|
||||
id: 'move_to_eu',
|
||||
title: 'Hosting in der EU',
|
||||
description: 'Wählen Sie einen Anbieter mit Rechenzentren in der EU.',
|
||||
removes_fields: ['third_country'],
|
||||
new_risk_score: 0,
|
||||
effort: 'medium',
|
||||
team_question: 'Können Sie zu einem EU-Anbieter wechseln?',
|
||||
},
|
||||
{
|
||||
id: 'implement_safeguards',
|
||||
title: 'Umfangreiche Schutzmaßnahmen implementieren',
|
||||
description: 'SCC, TIA, zusätzliche technische Maßnahmen implementieren.',
|
||||
adds_controls: ['scc', 'tia', 'encryption'],
|
||||
new_risk_score: 15,
|
||||
effort: 'high',
|
||||
team_question: 'Können Sie die erforderlichen Verträge und Maßnahmen umsetzen?',
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
// KI-Training mit personenbezogenen Daten
|
||||
{
|
||||
id: 'training_with_pii',
|
||||
title: 'KI-Training mit personenbezogenen Daten',
|
||||
description: 'Sie möchten ein KI-Modell mit personenbezogenen Daten trainieren. Das erfordert besondere Rechtsgrundlagen.',
|
||||
severity: 'WARN',
|
||||
triggered_by: {
|
||||
all_of: ['training'],
|
||||
any_of: ['personal_data', 'article_9_data', 'employee_data', 'customer_data'],
|
||||
},
|
||||
legalRefs: [
|
||||
{ article: 'Art. 5(1)(b) DSGVO', title: 'Zweckbindung', relevance: 'Training ist eigener Zweck' },
|
||||
{ article: 'Art. 6 DSGVO', title: 'Rechtmäßigkeit', relevance: 'Eigene Rechtsgrundlage erforderlich' },
|
||||
],
|
||||
solutions: [
|
||||
{
|
||||
id: 'use_rag_instead',
|
||||
title: 'RAG statt Training verwenden',
|
||||
description: 'Statt Training: Dokumente in Vektordatenbank ablegen und bei Anfragen durchsuchen.',
|
||||
removes_fields: ['training'],
|
||||
new_risk_score: 5,
|
||||
effort: 'low',
|
||||
team_question: 'Reicht es, wenn die KI Ihre Dokumente durchsuchen kann statt daraus zu lernen?',
|
||||
},
|
||||
{
|
||||
id: 'anonymize_training_data',
|
||||
title: 'Trainingsdaten anonymisieren',
|
||||
description: 'Personenbezogene Daten vor dem Training vollständig anonymisieren.',
|
||||
adds_controls: ['anonymization'],
|
||||
new_risk_score: 5,
|
||||
effort: 'high',
|
||||
team_question: 'Können die Trainingsdaten vor dem Training anonymisiert werden?',
|
||||
},
|
||||
{
|
||||
id: 'get_training_consent',
|
||||
title: 'Einwilligung für Training einholen',
|
||||
description: 'Betroffene explizit um Einwilligung für das KI-Training bitten.',
|
||||
adds_controls: ['explicit_consent', 'dsfa'],
|
||||
new_risk_score: 15,
|
||||
effort: 'high',
|
||||
team_question: 'Können Sie die Einwilligung aller Betroffenen für das KI-Training einholen?',
|
||||
},
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
// ============================================================================
|
||||
// Hilfsfunktionen
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Aggregiert alle ausgewählten Felder und berechnet das Ergebnis
|
||||
*/
|
||||
export function evaluateSelection(selection: {
|
||||
dataTypes: string[]
|
||||
automation: string
|
||||
purposes: string[]
|
||||
hosting: string
|
||||
modelUsage: string[]
|
||||
acceptedSolutions: string[]
|
||||
}): {
|
||||
totalRiskScore: number
|
||||
allLegalRefs: LegalReference[]
|
||||
allRequiredControls: string[]
|
||||
problems: Problem[]
|
||||
severity: 'INFO' | 'WARN' | 'BLOCK'
|
||||
} {
|
||||
const allLegalRefs: LegalReference[] = []
|
||||
const allRequiredControls: Set<string> = new Set()
|
||||
let totalRiskScore = 0
|
||||
let maxSeverity: 'INFO' | 'WARN' | 'BLOCK' = 'INFO'
|
||||
|
||||
// Aggregiere Datentypen
|
||||
for (const dt of selection.dataTypes) {
|
||||
const meta = DATA_TYPE_METADATA[dt]
|
||||
if (meta) {
|
||||
totalRiskScore += meta.riskScore
|
||||
allLegalRefs.push(...meta.legalRefs)
|
||||
meta.requiredControls.forEach(c => allRequiredControls.add(c))
|
||||
if (meta.severity === 'BLOCK') maxSeverity = 'BLOCK'
|
||||
else if (meta.severity === 'WARN' && maxSeverity !== 'BLOCK') maxSeverity = 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregiere Automatisierung
|
||||
const autoMeta = AUTOMATION_METADATA[selection.automation]
|
||||
if (autoMeta) {
|
||||
totalRiskScore += autoMeta.riskScore
|
||||
allLegalRefs.push(...autoMeta.legalRefs)
|
||||
autoMeta.requiredControls.forEach(c => allRequiredControls.add(c))
|
||||
if (autoMeta.severity === 'BLOCK') maxSeverity = 'BLOCK'
|
||||
else if (autoMeta.severity === 'WARN' && maxSeverity !== 'BLOCK') maxSeverity = 'WARN'
|
||||
}
|
||||
|
||||
// Aggregiere Zwecke
|
||||
for (const p of selection.purposes) {
|
||||
const meta = PURPOSE_METADATA[p]
|
||||
if (meta) {
|
||||
totalRiskScore += meta.riskScore
|
||||
allLegalRefs.push(...meta.legalRefs)
|
||||
meta.requiredControls.forEach(c => allRequiredControls.add(c))
|
||||
if (meta.severity === 'BLOCK') maxSeverity = 'BLOCK'
|
||||
else if (meta.severity === 'WARN' && maxSeverity !== 'BLOCK') maxSeverity = 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregiere Hosting
|
||||
const hostMeta = HOSTING_METADATA[selection.hosting]
|
||||
if (hostMeta) {
|
||||
totalRiskScore += hostMeta.riskScore
|
||||
allLegalRefs.push(...hostMeta.legalRefs)
|
||||
hostMeta.requiredControls.forEach(c => allRequiredControls.add(c))
|
||||
if (hostMeta.severity === 'BLOCK') maxSeverity = 'BLOCK'
|
||||
else if (hostMeta.severity === 'WARN' && maxSeverity !== 'BLOCK') maxSeverity = 'WARN'
|
||||
}
|
||||
|
||||
// Aggregiere Model Usage
|
||||
for (const mu of selection.modelUsage) {
|
||||
const meta = MODEL_USAGE_METADATA[mu]
|
||||
if (meta) {
|
||||
totalRiskScore += meta.riskScore
|
||||
allLegalRefs.push(...meta.legalRefs)
|
||||
meta.requiredControls.forEach(c => allRequiredControls.add(c))
|
||||
if (meta.severity === 'BLOCK') maxSeverity = 'BLOCK'
|
||||
else if (meta.severity === 'WARN' && maxSeverity !== 'BLOCK') maxSeverity = 'WARN'
|
||||
}
|
||||
}
|
||||
|
||||
// Finde zutreffende Probleme
|
||||
const allSelectedFields = [
|
||||
...selection.dataTypes,
|
||||
selection.automation,
|
||||
...selection.purposes,
|
||||
selection.hosting,
|
||||
...selection.modelUsage,
|
||||
]
|
||||
|
||||
const triggeredProblems = PROBLEMS.filter(problem => {
|
||||
// Prüfe all_of: alle müssen ausgewählt sein
|
||||
if (problem.triggered_by.all_of) {
|
||||
if (!problem.triggered_by.all_of.every(f => allSelectedFields.includes(f))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe any_of: mindestens eins muss ausgewählt sein
|
||||
if (problem.triggered_by.any_of) {
|
||||
if (!problem.triggered_by.any_of.some(f => allSelectedFields.includes(f))) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe none_of: keins darf ausgewählt sein (außer durch Lösung)
|
||||
if (problem.triggered_by.none_of) {
|
||||
const hasNoneOf = problem.triggered_by.none_of.some(f =>
|
||||
allSelectedFields.includes(f) || selection.acceptedSolutions.includes(f)
|
||||
)
|
||||
if (hasNoneOf) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
// Prüfe ob Problem durch akzeptierte Lösung gelöst wurde
|
||||
const isSolved = problem.solutions.some(solution =>
|
||||
selection.acceptedSolutions.includes(solution.id)
|
||||
)
|
||||
|
||||
return !isSolved
|
||||
})
|
||||
|
||||
// Probleme beeinflussen Severity
|
||||
for (const problem of triggeredProblems) {
|
||||
if (problem.severity === 'BLOCK') maxSeverity = 'BLOCK'
|
||||
else if (problem.severity === 'WARN' && maxSeverity !== 'BLOCK') maxSeverity = 'WARN'
|
||||
}
|
||||
|
||||
// Dedupliziere Legal Refs
|
||||
const uniqueLegalRefs = allLegalRefs.filter((ref, index, self) =>
|
||||
index === self.findIndex(r => r.article === ref.article)
|
||||
)
|
||||
|
||||
return {
|
||||
totalRiskScore: Math.min(totalRiskScore, 100),
|
||||
allLegalRefs: uniqueLegalRefs,
|
||||
allRequiredControls: Array.from(allRequiredControls),
|
||||
problems: triggeredProblems,
|
||||
severity: maxSeverity,
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
+59
-289
@@ -1,7 +1,7 @@
|
||||
'use client'
|
||||
|
||||
/**
|
||||
* Consent Management Page (SDK Version)
|
||||
* Consent Admin Panel
|
||||
*
|
||||
* Admin interface for managing:
|
||||
* - Documents (AGB, Privacy, etc.)
|
||||
@@ -12,9 +12,7 @@
|
||||
*/
|
||||
|
||||
import { useState, useEffect } from 'react'
|
||||
import Link from 'next/link'
|
||||
import { useSDK } from '@/lib/sdk'
|
||||
import StepHeader from '@/components/sdk/StepHeader/StepHeader'
|
||||
import { PagePurpose } from '@/components/common/PagePurpose'
|
||||
|
||||
// API Proxy URL (avoids CORS issues)
|
||||
const API_BASE = '/api/admin/consent'
|
||||
@@ -42,15 +40,7 @@ interface Version {
|
||||
created_at: string
|
||||
}
|
||||
|
||||
// Email template editor types
|
||||
interface EmailTemplateData {
|
||||
key: string
|
||||
subject: string
|
||||
body: string
|
||||
}
|
||||
|
||||
export default function ConsentManagementPage() {
|
||||
const { state } = useSDK()
|
||||
export default function ConsentPage() {
|
||||
const [activeTab, setActiveTab] = useState<Tab>('documents')
|
||||
const [documents, setDocuments] = useState<Document[]>([])
|
||||
const [versions, setVersions] = useState<Version[]>([])
|
||||
@@ -58,33 +48,15 @@ export default function ConsentManagementPage() {
|
||||
const [error, setError] = useState<string | null>(null)
|
||||
const [selectedDocument, setSelectedDocument] = useState<string>('')
|
||||
|
||||
// Stats state
|
||||
const [consentStats, setConsentStats] = useState<{ activeConsents: number; documentCount: number; openDSRs: number }>({ activeConsents: 0, documentCount: 0, openDSRs: 0 })
|
||||
|
||||
// GDPR tab state
|
||||
const [dsrCounts, setDsrCounts] = useState<Record<string, number>>({})
|
||||
const [dsrOverview, setDsrOverview] = useState<{ open: number; completed: number; in_progress: number; overdue: number }>({ open: 0, completed: 0, in_progress: 0, overdue: 0 })
|
||||
|
||||
// Email template editor state
|
||||
const [editingTemplate, setEditingTemplate] = useState<EmailTemplateData | null>(null)
|
||||
const [previewTemplate, setPreviewTemplate] = useState<EmailTemplateData | null>(null)
|
||||
const [savedTemplates, setSavedTemplates] = useState<Record<string, EmailTemplateData>>({})
|
||||
|
||||
// Auth token (in production, get from auth context)
|
||||
const [authToken, setAuthToken] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
// Get token from localStorage
|
||||
const token = localStorage.getItem('bp_admin_token')
|
||||
if (token) {
|
||||
setAuthToken(token)
|
||||
}
|
||||
// Load saved email templates from localStorage
|
||||
try {
|
||||
const saved = localStorage.getItem('sdk-email-templates')
|
||||
if (saved) {
|
||||
setSavedTemplates(JSON.parse(saved))
|
||||
}
|
||||
} catch { /* ignore */ }
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
@@ -92,10 +64,6 @@ export default function ConsentManagementPage() {
|
||||
loadDocuments()
|
||||
} else if (activeTab === 'versions' && selectedDocument) {
|
||||
loadVersions(selectedDocument)
|
||||
} else if (activeTab === 'stats') {
|
||||
loadStats()
|
||||
} else if (activeTab === 'gdpr') {
|
||||
loadGDPRData()
|
||||
}
|
||||
}, [activeTab, selectedDocument, authToken])
|
||||
|
||||
@@ -113,7 +81,7 @@ export default function ConsentManagementPage() {
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
setError(errorData.error || 'Fehler beim Laden der Dokumente')
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
setError('Verbindungsfehler zum Server')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
@@ -134,118 +102,13 @@ export default function ConsentManagementPage() {
|
||||
const errorData = await res.json().catch(() => ({}))
|
||||
setError(errorData.error || 'Fehler beim Laden der Versionen')
|
||||
}
|
||||
} catch {
|
||||
} catch (err) {
|
||||
setError('Verbindungsfehler zum Server')
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
try {
|
||||
const token = localStorage.getItem('bp_admin_token')
|
||||
const [statsRes, docsRes] = await Promise.all([
|
||||
fetch(`${API_BASE}/stats`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}),
|
||||
fetch(`${API_BASE}/documents`, {
|
||||
headers: token ? { 'Authorization': `Bearer ${token}` } : {}
|
||||
}),
|
||||
])
|
||||
|
||||
let activeConsents = 0
|
||||
let documentCount = 0
|
||||
let openDSRs = 0
|
||||
|
||||
if (statsRes.ok) {
|
||||
const statsData = await statsRes.json()
|
||||
activeConsents = statsData.total_consents || statsData.active_consents || 0
|
||||
}
|
||||
|
||||
if (docsRes.ok) {
|
||||
const docsData = await docsRes.json()
|
||||
documentCount = (docsData.documents || []).length
|
||||
}
|
||||
|
||||
// Try to get DSR count
|
||||
try {
|
||||
const dsrRes = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
||||
headers: {
|
||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||
}
|
||||
})
|
||||
if (dsrRes.ok) {
|
||||
const dsrData = await dsrRes.json()
|
||||
const dsrs = dsrData.dsrs || []
|
||||
const now = new Date()
|
||||
openDSRs = dsrs.filter((r: any) => r.status !== 'completed' && r.status !== 'rejected').length
|
||||
}
|
||||
} catch { /* DSR endpoint might not be available */ }
|
||||
|
||||
setConsentStats({ activeConsents, documentCount, openDSRs })
|
||||
} catch (err) {
|
||||
console.error('Failed to load stats:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function loadGDPRData() {
|
||||
try {
|
||||
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
|
||||
headers: {
|
||||
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
|
||||
'X-User-ID': localStorage.getItem('bp_user_id') || '',
|
||||
}
|
||||
})
|
||||
if (!res.ok) return
|
||||
|
||||
const data = await res.json()
|
||||
const dsrs = data.dsrs || []
|
||||
const now = new Date()
|
||||
|
||||
// Count per article type
|
||||
const counts: Record<string, number> = {}
|
||||
const typeMapping: Record<string, string> = {
|
||||
'access': '15',
|
||||
'rectification': '16',
|
||||
'erasure': '17',
|
||||
'restriction': '18',
|
||||
'portability': '20',
|
||||
'objection': '21',
|
||||
}
|
||||
|
||||
for (const dsr of dsrs) {
|
||||
if (dsr.status === 'completed' || dsr.status === 'rejected') continue
|
||||
const article = typeMapping[dsr.request_type]
|
||||
if (article) {
|
||||
counts[article] = (counts[article] || 0) + 1
|
||||
}
|
||||
}
|
||||
setDsrCounts(counts)
|
||||
|
||||
// Calculate overview
|
||||
const open = dsrs.filter((r: any) => r.status === 'received' || r.status === 'verified').length
|
||||
const completed = dsrs.filter((r: any) => r.status === 'completed').length
|
||||
const in_progress = dsrs.filter((r: any) => r.status === 'in_progress').length
|
||||
const overdue = dsrs.filter((r: any) => {
|
||||
if (r.status === 'completed' || r.status === 'rejected') return false
|
||||
const deadline = r.extended_deadline_at ? new Date(r.extended_deadline_at) : new Date(r.deadline_at)
|
||||
return deadline < now
|
||||
}).length
|
||||
|
||||
setDsrOverview({ open, completed, in_progress, overdue })
|
||||
} catch (err) {
|
||||
console.error('Failed to load GDPR data:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function saveEmailTemplate(template: EmailTemplateData) {
|
||||
const updated = { ...savedTemplates, [template.key]: template }
|
||||
setSavedTemplates(updated)
|
||||
localStorage.setItem('sdk-email-templates', JSON.stringify(updated))
|
||||
setEditingTemplate(null)
|
||||
}
|
||||
|
||||
const tabs: { id: Tab; label: string }[] = [
|
||||
{ id: 'documents', label: 'Dokumente' },
|
||||
{ id: 'versions', label: 'Versionen' },
|
||||
@@ -256,20 +119,25 @@ export default function ConsentManagementPage() {
|
||||
|
||||
// 16 Lifecycle Email Templates
|
||||
const emailTemplates = [
|
||||
// Onboarding
|
||||
{ name: 'Willkommens-E-Mail', key: 'welcome', category: 'onboarding', description: 'Wird bei Registrierung versendet' },
|
||||
{ name: 'E-Mail Bestaetigung', key: 'email_verification', category: 'onboarding', description: 'Bestaetigungslink fuer E-Mail-Adresse' },
|
||||
{ name: 'Konto aktiviert', key: 'account_activated', category: 'onboarding', description: 'Bestaetigung der Kontoaktivierung' },
|
||||
// Security
|
||||
{ name: 'Passwort zuruecksetzen', key: 'password_reset', category: 'security', description: 'Link zum Zuruecksetzen des Passworts' },
|
||||
{ name: 'Passwort geaendert', key: 'password_changed', category: 'security', description: 'Bestaetigung der Passwortaenderung' },
|
||||
{ name: 'Neue Anmeldung', key: 'new_login', category: 'security', description: 'Benachrichtigung ueber Anmeldung von neuem Geraet' },
|
||||
{ name: '2FA aktiviert', key: '2fa_enabled', category: 'security', description: 'Bestaetigung der 2FA-Aktivierung' },
|
||||
// Consent & Legal
|
||||
{ name: 'Neue Dokumentversion', key: 'new_document_version', category: 'consent', description: 'Info ueber neue Dokumentversion zur Zustimmung' },
|
||||
{ name: 'Zustimmung erteilt', key: 'consent_given', category: 'consent', description: 'Bestaetigung der erteilten Zustimmung' },
|
||||
{ name: 'Zustimmung widerrufen', key: 'consent_withdrawn', category: 'consent', description: 'Bestaetigung des Widerrufs' },
|
||||
// Data Subject Rights (GDPR)
|
||||
{ name: 'DSR Anfrage erhalten', key: 'dsr_received', category: 'gdpr', description: 'Bestaetigung des Eingangs einer DSGVO-Anfrage' },
|
||||
{ name: 'Datenexport bereit', key: 'export_ready', category: 'gdpr', description: 'Benachrichtigung ueber fertigen Datenexport' },
|
||||
{ name: 'Daten geloescht', key: 'data_deleted', category: 'gdpr', description: 'Bestaetigung der Datenloeschung' },
|
||||
{ name: 'Daten berichtigt', key: 'data_rectified', category: 'gdpr', description: 'Bestaetigung der Datenberichtigung' },
|
||||
// Account Lifecycle
|
||||
{ name: 'Konto deaktiviert', key: 'account_deactivated', category: 'lifecycle', description: 'Konto wurde deaktiviert' },
|
||||
{ name: 'Konto geloescht', key: 'account_deleted', category: 'lifecycle', description: 'Bestaetigung der Kontoloeschung' },
|
||||
]
|
||||
@@ -282,6 +150,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Recht auf Bestaetigung und Auskunft ueber verarbeitete personenbezogene Daten',
|
||||
actions: ['Datenexport generieren', 'Verarbeitungszwecke auflisten', 'Empfaenger auflisten'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '16',
|
||||
@@ -289,6 +158,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Recht auf Berichtigung unrichtiger personenbezogener Daten',
|
||||
actions: ['Daten bearbeiten', 'Aenderungshistorie fuehren', 'Benachrichtigung senden'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '17',
|
||||
@@ -296,6 +166,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Recht auf Loeschung personenbezogener Daten unter bestimmten Voraussetzungen',
|
||||
actions: ['Loeschantrag pruefen', 'Daten loeschen', 'Aufbewahrungsfristen pruefen', 'Loeschbestaetigung senden'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '18',
|
||||
@@ -303,6 +174,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Recht auf Markierung von Daten zur eingeschraenkten Verarbeitung',
|
||||
actions: ['Daten markieren', 'Verarbeitung einschraenken', 'Benachrichtigung bei Aufhebung'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '19',
|
||||
@@ -310,6 +182,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Pflicht zur Mitteilung von Berichtigung, Loeschung oder Einschraenkung an Empfaenger',
|
||||
actions: ['Empfaenger ermitteln', 'Mitteilungen versenden', 'Protokollierung'],
|
||||
sla: 'Unverzueglich',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '20',
|
||||
@@ -317,6 +190,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Recht auf Erhalt der Daten in strukturiertem, maschinenlesbarem Format',
|
||||
actions: ['JSON/CSV Export', 'API-Schnittstelle', 'Direkte Uebertragung'],
|
||||
sla: '30 Tage',
|
||||
status: 'active'
|
||||
},
|
||||
{
|
||||
article: '21',
|
||||
@@ -324,6 +198,7 @@ export default function ConsentManagementPage() {
|
||||
description: 'Recht auf Widerspruch gegen Verarbeitung aus berechtigtem Interesse oder Direktwerbung',
|
||||
actions: ['Widerspruch erfassen', 'Verarbeitung stoppen', 'Marketing-Opt-out'],
|
||||
sla: 'Unverzueglich',
|
||||
status: 'active'
|
||||
},
|
||||
]
|
||||
|
||||
@@ -336,12 +211,29 @@ export default function ConsentManagementPage() {
|
||||
]
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<StepHeader stepId="consent-management" showProgress={true} />
|
||||
<div>
|
||||
{/* Page Purpose */}
|
||||
<PagePurpose
|
||||
title="Consent Verwaltung"
|
||||
purpose="Verwalten Sie rechtliche Dokumente (AGB, Datenschutz, Cookie-Richtlinien) und deren Versionen. Jede Einwilligung eines Benutzers basiert auf diesen Dokumenten und muss nachvollziehbar sein."
|
||||
audience={['DSB', 'Entwickler', 'Compliance Officer']}
|
||||
gdprArticles={['Art. 7 (Einwilligung)', 'Art. 13/14 (Informationspflichten)']}
|
||||
architecture={{
|
||||
services: ['consent-service (Go)', 'backend (Python)'],
|
||||
databases: ['PostgreSQL'],
|
||||
}}
|
||||
relatedPages={[
|
||||
{ name: 'DSR-Verwaltung', href: '/compliance/dsr', description: 'Datenschutzanfragen bearbeiten' },
|
||||
{ name: 'DSGVO-Audit', href: '/compliance/audit', description: 'Audit-Dokumentation erstellen' },
|
||||
{ name: 'Workflow', href: '/compliance/workflow', description: 'Freigabe-Prozesse konfigurieren' },
|
||||
]}
|
||||
collapsible={true}
|
||||
defaultCollapsed={false}
|
||||
/>
|
||||
|
||||
{/* Token Input */}
|
||||
{!authToken && (
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4">
|
||||
<div className="bg-white rounded-xl border border-slate-200 p-4 mb-6">
|
||||
<label className="block text-sm font-medium text-slate-700 mb-2">
|
||||
Admin Token
|
||||
</label>
|
||||
@@ -358,7 +250,7 @@ export default function ConsentManagementPage() {
|
||||
)}
|
||||
|
||||
{/* Tabs */}
|
||||
<div>
|
||||
<div className="mb-6">
|
||||
<div className="flex gap-1 bg-slate-100 p-1 rounded-lg w-fit">
|
||||
{tabs.map((tab) => (
|
||||
<button
|
||||
@@ -588,6 +480,11 @@ export default function ConsentManagementPage() {
|
||||
>
|
||||
<div className="flex items-center gap-4">
|
||||
<div className={`w-10 h-10 rounded-lg ${category.color} flex items-center justify-center text-lg`}>
|
||||
{category.key === 'onboarding' && ''}
|
||||
{category.key === 'security' && ''}
|
||||
{category.key === 'consent' && ''}
|
||||
{category.key === 'gdpr' && ''}
|
||||
{category.key === 'lifecycle' && ''}
|
||||
</div>
|
||||
<div>
|
||||
<h4 className="font-medium text-slate-900">{template.name}</h4>
|
||||
@@ -595,33 +492,11 @@ export default function ConsentManagementPage() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">
|
||||
{savedTemplates[template.key] ? 'Angepasst' : 'Aktiv'}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => {
|
||||
const existing = savedTemplates[template.key]
|
||||
setEditingTemplate({
|
||||
key: template.key,
|
||||
subject: existing?.subject || `Betreff: ${template.name}`,
|
||||
body: existing?.body || `Sehr geehrte(r) {{name}},\n\n${template.description}\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team`,
|
||||
})
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
|
||||
>
|
||||
<span className="px-2 py-1 bg-green-100 text-green-700 rounded text-xs">Aktiv</span>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
|
||||
Bearbeiten
|
||||
</button>
|
||||
<button
|
||||
onClick={() => {
|
||||
const existing = savedTemplates[template.key]
|
||||
setPreviewTemplate({
|
||||
key: template.key,
|
||||
subject: existing?.subject || `Betreff: ${template.name}`,
|
||||
body: existing?.body || `Sehr geehrte(r) {{name}},\n\n${template.description}\n\nMit freundlichen Gruessen\nIhr Datenschutz-Team`,
|
||||
})
|
||||
}}
|
||||
className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400"
|
||||
>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
|
||||
Vorschau
|
||||
</button>
|
||||
</div>
|
||||
@@ -694,19 +569,16 @@ export default function ConsentManagementPage() {
|
||||
</span>
|
||||
<span className="text-slate-300">|</span>
|
||||
<span className="text-slate-500">
|
||||
Offene Anfragen: <span className={`font-medium ${(dsrCounts[process.article] || 0) > 0 ? 'text-orange-600' : 'text-slate-700'}`}>{dsrCounts[process.article] || 0}</span>
|
||||
Offene Anfragen: <span className="font-medium text-slate-700">0</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex flex-col gap-2">
|
||||
<Link
|
||||
href={`/sdk/dsr?type=${process.article === '15' ? 'access' : process.article === '16' ? 'rectification' : process.article === '17' ? 'erasure' : process.article === '18' ? 'restriction' : process.article === '20' ? 'portability' : 'objection'}`}
|
||||
className="px-3 py-1.5 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg text-center"
|
||||
>
|
||||
<button className="px-3 py-1.5 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg">
|
||||
Anfragen
|
||||
</Link>
|
||||
</button>
|
||||
<button className="px-3 py-1.5 text-sm text-slate-600 hover:text-slate-900 border border-slate-300 rounded-lg hover:border-slate-400">
|
||||
Vorlage
|
||||
</button>
|
||||
@@ -721,19 +593,19 @@ export default function ConsentManagementPage() {
|
||||
<h3 className="text-sm font-semibold text-slate-700 uppercase tracking-wider mb-4">DSR Uebersicht</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4 text-center">
|
||||
<div className={`text-2xl font-bold ${dsrOverview.open > 0 ? 'text-blue-600' : 'text-slate-900'}`}>{dsrOverview.open}</div>
|
||||
<div className="text-2xl font-bold text-slate-900">0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Offen</div>
|
||||
</div>
|
||||
<div className="bg-green-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-green-700">{dsrOverview.completed}</div>
|
||||
<div className="text-2xl font-bold text-green-700">0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Erledigt</div>
|
||||
</div>
|
||||
<div className="bg-yellow-50 rounded-lg p-4 text-center">
|
||||
<div className="text-2xl font-bold text-yellow-700">{dsrOverview.in_progress}</div>
|
||||
<div className="text-2xl font-bold text-yellow-700">0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">In Bearbeitung</div>
|
||||
</div>
|
||||
<div className="bg-red-50 rounded-lg p-4 text-center">
|
||||
<div className={`text-2xl font-bold ${dsrOverview.overdue > 0 ? 'text-red-700' : 'text-slate-400'}`}>{dsrOverview.overdue}</div>
|
||||
<div className="text-2xl font-bold text-red-700">0</div>
|
||||
<div className="text-xs text-slate-500 mt-1">Ueberfaellig</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -748,131 +620,29 @@ export default function ConsentManagementPage() {
|
||||
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
|
||||
<div className="bg-slate-50 rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-slate-900">{consentStats.activeConsents}</div>
|
||||
<div className="text-3xl font-bold text-slate-900">0</div>
|
||||
<div className="text-sm text-slate-500 mt-1">Aktive Zustimmungen</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-6">
|
||||
<div className="text-3xl font-bold text-slate-900">{consentStats.documentCount}</div>
|
||||
<div className="text-3xl font-bold text-slate-900">0</div>
|
||||
<div className="text-sm text-slate-500 mt-1">Dokumente</div>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-xl p-6">
|
||||
<div className={`text-3xl font-bold ${consentStats.openDSRs > 0 ? 'text-orange-600' : 'text-slate-900'}`}>
|
||||
{consentStats.openDSRs}
|
||||
</div>
|
||||
<div className="text-3xl font-bold text-slate-900">0</div>
|
||||
<div className="text-sm text-slate-500 mt-1">Offene DSR-Anfragen</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="border border-slate-200 rounded-lg p-6">
|
||||
<h3 className="font-semibold text-slate-900 mb-4">Zustimmungsrate nach Dokument</h3>
|
||||
<div className="text-center py-8 text-slate-400 text-sm">
|
||||
Diagramm wird in einer zukuenftigen Version verfuegbar sein
|
||||
<div className="text-center py-8 text-slate-500">
|
||||
Noch keine Daten verfuegbar
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
{/* Email Template Edit Modal */}
|
||||
{editingTemplate && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-900">E-Mail Vorlage bearbeiten</h3>
|
||||
<button onClick={() => setEditingTemplate(null)} className="text-slate-400 hover:text-slate-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Betreff</label>
|
||||
<input
|
||||
type="text"
|
||||
value={editingTemplate.subject}
|
||||
onChange={(e) => setEditingTemplate({ ...editingTemplate, subject: e.target.value })}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="block text-sm font-medium text-slate-700 mb-1">Inhalt</label>
|
||||
<textarea
|
||||
value={editingTemplate.body}
|
||||
onChange={(e) => setEditingTemplate({ ...editingTemplate, body: e.target.value })}
|
||||
rows={12}
|
||||
className="w-full px-3 py-2 border border-slate-300 rounded-lg text-sm font-mono"
|
||||
/>
|
||||
</div>
|
||||
<div className="bg-slate-50 rounded-lg p-3">
|
||||
<div className="text-xs font-medium text-slate-500 mb-1">Verfuegbare Platzhalter:</div>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
{['{{name}}', '{{email}}', '{{referenceNumber}}', '{{date}}', '{{deadline}}', '{{company}}'].map(v => (
|
||||
<span key={v} className="px-2 py-0.5 bg-purple-100 text-purple-700 rounded text-xs font-mono">{v}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-slate-200 flex justify-end gap-3">
|
||||
<button onClick={() => setEditingTemplate(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||
Abbrechen
|
||||
</button>
|
||||
<button
|
||||
onClick={() => saveEmailTemplate(editingTemplate)}
|
||||
className="px-4 py-2 text-sm text-white bg-purple-600 hover:bg-purple-700 rounded-lg font-medium"
|
||||
>
|
||||
Speichern
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Email Template Preview Modal */}
|
||||
{previewTemplate && (
|
||||
<div className="fixed inset-0 bg-black/50 flex items-center justify-center z-50">
|
||||
<div className="bg-white rounded-xl shadow-xl max-w-2xl w-full mx-4 max-h-[90vh] overflow-y-auto">
|
||||
<div className="px-6 py-4 border-b border-slate-200 flex items-center justify-between">
|
||||
<h3 className="font-semibold text-slate-900">Vorschau</h3>
|
||||
<button onClick={() => setPreviewTemplate(null)} className="text-slate-400 hover:text-slate-600">
|
||||
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
||||
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div className="p-6 space-y-4">
|
||||
<div className="bg-slate-50 rounded-lg p-4">
|
||||
<div className="text-xs text-slate-500 mb-1">Betreff:</div>
|
||||
<div className="font-medium text-slate-900">
|
||||
{previewTemplate.subject
|
||||
.replace(/\{\{name\}\}/g, 'Max Mustermann')
|
||||
.replace(/\{\{email\}\}/g, 'max@example.de')
|
||||
.replace(/\{\{referenceNumber\}\}/g, 'DSR-2025-000001')
|
||||
.replace(/\{\{date\}\}/g, new Date().toLocaleDateString('de-DE'))
|
||||
.replace(/\{\{deadline\}\}/g, '30 Tage')
|
||||
.replace(/\{\{company\}\}/g, 'BreakPilot GmbH')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="border border-slate-200 rounded-lg p-4 whitespace-pre-wrap text-sm text-slate-700">
|
||||
{previewTemplate.body
|
||||
.replace(/\{\{name\}\}/g, 'Max Mustermann')
|
||||
.replace(/\{\{email\}\}/g, 'max@example.de')
|
||||
.replace(/\{\{referenceNumber\}\}/g, 'DSR-2025-000001')
|
||||
.replace(/\{\{date\}\}/g, new Date().toLocaleDateString('de-DE'))
|
||||
.replace(/\{\{deadline\}\}/g, '30 Tage')
|
||||
.replace(/\{\{company\}\}/g, 'BreakPilot GmbH')
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
<div className="px-6 py-4 border-t border-slate-200 flex justify-end">
|
||||
<button onClick={() => setPreviewTemplate(null)} className="px-4 py-2 text-sm text-slate-600 hover:bg-slate-100 rounded-lg">
|
||||
Schliessen
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user