diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 4ebe520..5cfd173 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -90,6 +90,35 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: --- +## Drei Docker Compose Projekte (WICHTIG!) + +Das System besteht aus **drei separaten Docker Compose Projekten** auf dem Mac Mini: + +| Projekt | Pfad | Container-Prefix | Beschreibung | +|---------|------|-------------------|--------------| +| **breakpilot-pwa** | `/Users/benjaminadmin/Projekte/breakpilot-pwa/` | `breakpilot-pwa-*` | Haupt-Repo: Studio, Admin, Backend, alle Services | +| **breakpilot-core** | `/Users/benjaminadmin/Projekte/breakpilot-core/` | `bp-core-*` | Nginx Reverse Proxy (`bp-core-nginx`) | +| **breakpilot-compliance** | `/Users/benjaminadmin/Projekte/breakpilot-compliance/` | `bp-compliance-*` | Compliance-System: Developer Portal, Admin, Backend, AI SDK | + +### Wichtige Hinweise zu den Compose-Projekten + +- **Nginx** (`bp-core-nginx`) läuft in `breakpilot-core`, NICHT in `breakpilot-pwa` +- **Developer Portal** (`bp-compliance-developer-portal`) läuft in `breakpilot-compliance` +- Wenn ein Container in `breakpilot-pwa` nicht existiert, prüfe die anderen Projekte! + +```bash +# breakpilot-pwa Container verwalten +ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml " + +# breakpilot-core Container verwalten (Nginx) +ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-core/docker-compose.yml " + +# breakpilot-compliance Container verwalten (Developer Portal, Compliance) +ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml " +``` + +--- + ## Haupt-URLs (HTTPS via Nginx) | URL | Service | Beschreibung | @@ -115,6 +144,19 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: | https://macmini:3002/developers | Developer Portal | API-Dokumentation für Kunden | | https://macmini:8093/ | SDK API | Backend-API für SDK | +### Developer Portal (Compliance-Dokumentation) + +| URL | Beschreibung | +|-----|--------------| +| https://macmini:3006/ | Developer Portal Startseite | +| https://macmini:3006/development/docs | **Systemdokumentation Compliance Service** | +| https://macmini:3006/sdk | SDK Dokumentation | +| https://macmini:3006/api | API Referenz | +| https://macmini:3006/guides | Guides | +| https://macmini:3006/changelog | Changelog | + +**Hinweis:** Das Developer Portal läuft als `bp-compliance-developer-portal` im Compose-Projekt `breakpilot-compliance` auf Port 3006 (via `bp-core-nginx`). + ### Interne Dienste | URL | Service | @@ -150,7 +192,7 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: --- -## Services (49 Container) +## Services ### Kern-Applikationen @@ -169,7 +211,6 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: | `klausur-service` | Python/FastAPI | 8086 | Prüfungen, OCR, RAG | | `school-service` | Python | 8082 | Schulverwaltung | | `edu-search-service` | Python | 8088 | Bildungssuche | -| `breakpilot-drive` | Node.js | 8087 | Dateiablage (IPFS) | | `geo-service` | Python | 8084 | Geo-Daten (PostGIS) | | `voice-service` | Python | 8091 | Spracheingabe | @@ -182,6 +223,15 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: | `paddleocr-service` | Python | - | OCR für Dokumente | | `transcription-worker` | Python | - | Audio-Transkription | +### Compliance (breakpilot-compliance Projekt) + +| Service | Tech | Port | Container | +|---------|------|------|-----------| +| `developer-portal` | Next.js | 3006 | `bp-compliance-developer-portal` | +| `compliance-admin` | Next.js | - | `bp-compliance-admin` | +| `compliance-backend` | Go | - | `bp-compliance-backend` | +| `compliance-ai-sdk` | Go | 8090 | `bp-compliance-ai-sdk` | + ### Kommunikation | Service | Tech | Port | Beschreibung | @@ -206,7 +256,7 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: | Service | Tech | Port | Beschreibung | |---------|------|------|--------------| -| `nginx` | Nginx | 80/443 | Reverse Proxy + TLS | +| `nginx` | Nginx | 80/443 | Reverse Proxy + TLS (in breakpilot-core!) | | `vault` | HashiCorp Vault | 8200 | Secrets Management | | `vault-agent` | Vault | - | Zertifikatserneuerung | | `gitea` | Gitea | 3003 | Git-Server | @@ -215,14 +265,13 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: | `night-scheduler` | Python/FastAPI | 8096 | Auto-Shutdown/Startup | | `mailpit` | Mailpit | 8025/1025 | E-Mail (Dev) | -### ERP & Billing +### ERP | Service | Tech | Port | Beschreibung | |---------|------|------|--------------| | `erpnext-frontend` | ERPNext | 8009 | ERP Frontend | | `erpnext-backend` | ERPNext | - | ERP Backend | | `erpnext-db` | MariaDB | - | ERP Datenbank | -| `billing-service` | Python | - | Abrechnungsservice | ### DSMS (Data Sharing) @@ -258,9 +307,9 @@ Alle Security-Tools müssen nach der Pipeline durchlaufen: - `studio-v2`: Next.js 15, React, TailwindCSS - `admin-v2`: Next.js 15, React, TailwindCSS - `website`: Next.js 14 +- `developer-portal`: Next.js, React, TailwindCSS (in breakpilot-compliance) ### Node.js -- `breakpilot-drive`: Express, IPFS - `dsms-node`: IPFS - `dsms-gateway`: Express @@ -286,15 +335,16 @@ breakpilot-pwa/ ├── admin-v2/ # Admin Dashboard (Next.js) ├── studio-v2/ # Lehrer-/Schüler-Studio (Next.js) ├── website/ # Öffentliche Website (Next.js) +├── developer-portal/ # Developer Portal (Next.js, auch in breakpilot-compliance) ├── backend/ # Python Backend (FastAPI) ├── consent-service/ # Go Consent Service ├── klausur-service/ # Klausur/OCR Service ├── ai-compliance-sdk/ # KI-Compliance SDK +├── breakpilot-compliance-sdk/ # Compliance SDK (Monorepo) ├── voice-service/ # Spracheingabe ├── geo-service/ # Geo-Daten ├── school-service/ # Schulverwaltung ├── edu-search-service/ # Bildungssuche -├── breakpilot-drive/ # Dateiablage ├── night-scheduler/ # Auto-Shutdown ├── nginx/ # Reverse Proxy Config ├── vault/ # Vault Config @@ -304,6 +354,10 @@ breakpilot-pwa/ └── mkdocs.yml # MKDocs Config ``` +**Entfernte/nicht mehr aktive Verzeichnisse (in .gitignore blockiert):** +- `BreakpilotDrive/` — altes Unity-Projekt, nicht mehr in Entwicklung +- `billing-service/` — nicht benötigt + --- ## Dokumentation (MKDocs) @@ -339,7 +393,7 @@ mkdocs build ### Docker (via SSH auf Mac Mini) ```bash -# Alle Services starten +# Alle Services starten (breakpilot-pwa) ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml up -d" # Einzelnen Service neu bauen & starten @@ -351,6 +405,13 @@ ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/brea # Status aller Container ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-pwa/docker-compose.yml ps" + +# Developer Portal (in breakpilot-compliance!) +ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml build --no-cache developer-portal" +ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-compliance/docker-compose.yml up -d developer-portal" + +# Nginx (in breakpilot-core!) +ssh macmini "/usr/local/bin/docker compose -f /Users/benjaminadmin/Projekte/breakpilot-core/docker-compose.yml restart nginx" ``` **WICHTIG:** Docker-Pfad auf Mac Mini ist `/usr/local/bin/docker` (nicht im Standard-PATH bei SSH). @@ -368,9 +429,12 @@ ssh macmini "cd /Users/benjaminadmin/Projekte/breakpilot-pwa/backend && source v ### Git ```bash -# Remote ist localhost:3003 (Gitea laeuft als Container auf Mac Mini) -# Vom MacBook aus: http://macmini:3003/pilotadmin/breakpilot-pwa.git -# Vom Mac Mini aus: http://localhost:3003/pilotadmin/breakpilot-pwa.git +# Zwei Remotes konfiguriert - IMMER zu beiden pushen! +# origin: http://macmini:3003/pilotadmin/breakpilot-pwa.git (lokale Gitea auf Mac Mini) +# gitea: git@gitea.meghsakha.com:Benjamin_Boenisch/breakpilot-pwa.git (externer Gitea-Server) + +# Push zu beiden Remotes (PFLICHT bei jedem Push): +git push origin main && git push gitea main # Git-Befehle auf Mac Mini ausfuehren (ohne cd): ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-pwa status" @@ -416,6 +480,15 @@ ssh macmini "git -C /Users/benjaminadmin/Projekte/breakpilot-pwa pull --no-rebas - Vault-Tokens - SSL-Zertifikate +**NIEMALS ins Git laden (via .gitignore blockiert):** +- `*.pdf`, `*.docx`, `*.xlsx`, `*.pptx` — Dokumente bleiben nur lokal auf dem Mac Mini +- Kompilierte Go-Binaries (`consent-service/server`, etc.) +- Große Mediendateien (Videos, Audio, Bilder >1 MB) +- `BreakpilotDrive/` — altes Unity-Projekt +- `billing-service/` — nicht benötigt + +**Hinweis:** Die Git-History wurde am 2026-02-12 mit `git-filter-repo` bereinigt. Alle PDFs, Word-/Excel-Dateien, BreakpilotDrive/ und billing-service/ wurden aus der gesamten History entfernt. Das Repo ging dadurch von 1.7 GB auf 11 MB. + --- ## Ansprechpartner diff --git a/admin-v2/ai-compliance-sdk/cmd/server/main.go b/admin-v2/ai-compliance-sdk/cmd/server/main.go index 15f3e1a..838c5c9 100644 --- a/admin-v2/ai-compliance-sdk/cmd/server/main.go +++ b/admin-v2/ai-compliance-sdk/cmd/server/main.go @@ -96,6 +96,43 @@ func main() { checkpointHandler := api.NewCheckpointHandler() v1.GET("/checkpoints", checkpointHandler.GetAll) v1.POST("/checkpoints/validate", checkpointHandler.Validate) + + // Academy (Compliance E-Learning) + academyHandler := api.NewAcademyHandler(dbPool, llmService, ragService) + academy := v1.Group("/academy") + { + // Course CRUD + academy.GET("/courses", academyHandler.ListCourses) + academy.GET("/courses/:id", academyHandler.GetCourse) + academy.POST("/courses", academyHandler.CreateCourse) + academy.PUT("/courses/:id", academyHandler.UpdateCourse) + academy.DELETE("/courses/:id", academyHandler.DeleteCourse) + + // Statistics + academy.GET("/statistics", academyHandler.GetStatistics) + + // Enrollments + academy.GET("/enrollments", academyHandler.ListEnrollments) + academy.POST("/enrollments", academyHandler.EnrollUser) + academy.PUT("/enrollments/:id/progress", academyHandler.UpdateProgress) + academy.POST("/enrollments/:id/complete", academyHandler.CompleteEnrollment) + + // Quiz + academy.POST("/lessons/:id/quiz", academyHandler.SubmitQuiz) + + // Certificates + academy.POST("/enrollments/:id/certificate", academyHandler.GenerateCertificateEndpoint) + academy.GET("/certificates/:id", academyHandler.GetCertificate) + academy.GET("/certificates/:id/pdf", academyHandler.DownloadCertificatePDF) + + // AI Course Generation + academy.POST("/courses/generate", academyHandler.GenerateCourse) + academy.POST("/lessons/:id/regenerate", academyHandler.RegenerateLesson) + + // Video Generation + academy.POST("/courses/:id/generate-videos", academyHandler.GenerateVideos) + academy.GET("/courses/:id/video-status", academyHandler.GetVideoStatus) + } } // Create server diff --git a/admin-v2/ai-compliance-sdk/go.mod b/admin-v2/ai-compliance-sdk/go.mod index 8a833e4..dbd39b0 100644 --- a/admin-v2/ai-compliance-sdk/go.mod +++ b/admin-v2/ai-compliance-sdk/go.mod @@ -1,11 +1,45 @@ module github.com/breakpilot/ai-compliance-sdk -go 1.21 +go 1.23 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 + github.com/jung-kurt/gofpdf v1.16.2 +) + +require ( + github.com/bytedance/sonic v1.11.6 // indirect + github.com/bytedance/sonic/loader v0.1.1 // indirect + github.com/cloudwego/base64x v0.1.4 // indirect + github.com/cloudwego/iasm v0.2.0 // indirect + github.com/gabriel-vasile/mimetype v1.4.3 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.20.0 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/jackc/pgpassfile v1.0.0 // indirect + github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a // indirect + github.com/jackc/puddle/v2 v2.2.1 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.7 // indirect + github.com/kr/text v0.2.0 // indirect + github.com/leodido/go-urn v1.4.0 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.2.2 // indirect + github.com/rogpeppe/go-internal v1.14.1 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.12 // indirect + golang.org/x/arch v0.8.0 // indirect + golang.org/x/crypto v0.23.0 // indirect + golang.org/x/net v0.25.0 // indirect + golang.org/x/sync v0.1.0 // indirect + golang.org/x/sys v0.26.0 // indirect + golang.org/x/text v0.15.0 // indirect + google.golang.org/protobuf v1.34.1 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/admin-v2/ai-compliance-sdk/go.sum b/admin-v2/ai-compliance-sdk/go.sum new file mode 100644 index 0000000..d5a107d --- /dev/null +++ b/admin-v2/ai-compliance-sdk/go.sum @@ -0,0 +1,119 @@ +github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= +github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0= +github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4= +github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM= +github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU= +github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y= +github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w= +github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg= +github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY= +github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0= +github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU= +github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8= +github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= +github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a h1:bbPeKD0xmW/Y25WS6cokEszi5g+S0QxI/d45PkRi7Nk= +github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= +github.com/jackc/pgx/v5 v5.5.1 h1:5I9etrGkLrN+2XPCsi6XLlV5DITbSL/xBZdmAxFcXPI= +github.com/jackc/pgx/v5 v5.5.1/go.mod h1:Ig06C2Vu0t5qXC60W8sqIthScaEnFvojjj9dSljmHRA= +github.com/jackc/puddle/v2 v2.2.1 h1:RhxXJtFG022u4ibrCSMSiu5aOq1i77R3OHKNJj77OAk= +github.com/jackc/puddle/v2 v2.2.1/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= +github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= +github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= +github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= +github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= +github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= +github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= +github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= +github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= +github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM= +github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= +github.com/phpdave11/gofpdi v1.0.7/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= +github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= +github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE= +github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc= +golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.23.0 h1:dIJU/v2J8Mdglj/8rJ6UUOM3Zc9zLZxVZwwxMooUSAI= +golang.org/x/crypto v0.23.0/go.mod h1:CKFgDieR+mRhux2Lsu27y0fO304Db0wZe70UKqHu0v8= +golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= +golang.org/x/net v0.25.0 h1:d/OCCoBEUq33pjydKrGQhw7IlUPI2Oylr+8qLx49kac= +golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= +golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= +golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.15.0 h1:h1V/4gjBv8v9cjcR6+AR5+/cIYK5N/WAgiv4xlsEtAk= +golang.org/x/text v0.15.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.34.1 h1:9ddQBjfCyZPOHPUiPxpYESBLc+T8P3E+Vo4IbKZgFWg= +google.golang.org/protobuf v1.34.1/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/admin-v2/ai-compliance-sdk/internal/academy/certificate_pdf.go b/admin-v2/ai-compliance-sdk/internal/academy/certificate_pdf.go new file mode 100644 index 0000000..0c9d048 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/academy/certificate_pdf.go @@ -0,0 +1,152 @@ +package academy + +import ( + "bytes" + "fmt" + "time" + + "github.com/jung-kurt/gofpdf" +) + +// CertificateData holds all data needed to generate a certificate PDF +type CertificateData struct { + CertificateID string + UserName string + CourseName string + CompanyName string + Score int + IssuedAt time.Time + ValidUntil time.Time +} + +// GenerateCertificatePDF generates a PDF certificate and returns the bytes +func GenerateCertificatePDF(data CertificateData) ([]byte, error) { + pdf := gofpdf.New("L", "mm", "A4", "") // Landscape A4 + pdf.SetAutoPageBreak(false, 0) + pdf.AddPage() + + pageWidth, pageHeight := pdf.GetPageSize() + + // Background color - light gray + pdf.SetFillColor(250, 250, 252) + pdf.Rect(0, 0, pageWidth, pageHeight, "F") + + // Border - decorative + pdf.SetDrawColor(79, 70, 229) // Purple/Indigo + pdf.SetLineWidth(3) + pdf.Rect(10, 10, pageWidth-20, pageHeight-20, "D") + pdf.SetLineWidth(1) + pdf.Rect(14, 14, pageWidth-28, pageHeight-28, "D") + + // Header - Company/BreakPilot Logo area + companyName := data.CompanyName + if companyName == "" { + companyName = "BreakPilot Compliance" + } + + pdf.SetFont("Helvetica", "", 12) + pdf.SetTextColor(120, 120, 120) + pdf.SetXY(0, 25) + pdf.CellFormat(pageWidth, 10, companyName, "", 0, "C", false, 0, "") + + // Title + pdf.SetFont("Helvetica", "B", 32) + pdf.SetTextColor(30, 30, 30) + pdf.SetXY(0, 42) + pdf.CellFormat(pageWidth, 15, "SCHULUNGSZERTIFIKAT", "", 0, "C", false, 0, "") + + // Decorative line + pdf.SetDrawColor(79, 70, 229) + pdf.SetLineWidth(1.5) + lineY := 62.0 + pdf.Line(pageWidth/2-60, lineY, pageWidth/2+60, lineY) + + // "Hiermit wird bescheinigt, dass" + pdf.SetFont("Helvetica", "", 13) + pdf.SetTextColor(80, 80, 80) + pdf.SetXY(0, 72) + pdf.CellFormat(pageWidth, 8, "Hiermit wird bescheinigt, dass", "", 0, "C", false, 0, "") + + // Name + pdf.SetFont("Helvetica", "B", 26) + pdf.SetTextColor(30, 30, 30) + pdf.SetXY(0, 85) + pdf.CellFormat(pageWidth, 12, data.UserName, "", 0, "C", false, 0, "") + + // "die folgende Compliance-Schulung erfolgreich abgeschlossen hat:" + pdf.SetFont("Helvetica", "", 13) + pdf.SetTextColor(80, 80, 80) + pdf.SetXY(0, 103) + pdf.CellFormat(pageWidth, 8, "die folgende Compliance-Schulung erfolgreich abgeschlossen hat:", "", 0, "C", false, 0, "") + + // Course Name + pdf.SetFont("Helvetica", "B", 20) + pdf.SetTextColor(79, 70, 229) + pdf.SetXY(0, 116) + pdf.CellFormat(pageWidth, 10, data.CourseName, "", 0, "C", false, 0, "") + + // Score + if data.Score > 0 { + pdf.SetFont("Helvetica", "", 12) + pdf.SetTextColor(80, 80, 80) + pdf.SetXY(0, 130) + pdf.CellFormat(pageWidth, 8, fmt.Sprintf("Testergebnis: %d%%", data.Score), "", 0, "C", false, 0, "") + } + + // Bottom section - Dates and Signature + bottomY := 148.0 + + // Left: Issued Date + pdf.SetFont("Helvetica", "", 10) + pdf.SetTextColor(100, 100, 100) + pdf.SetXY(40, bottomY) + pdf.CellFormat(80, 6, fmt.Sprintf("Abschlussdatum: %s", data.IssuedAt.Format("02.01.2006")), "", 0, "L", false, 0, "") + + // Center: Valid Until + pdf.SetXY(pageWidth/2-40, bottomY) + pdf.CellFormat(80, 6, fmt.Sprintf("Gueltig bis: %s", data.ValidUntil.Format("02.01.2006")), "", 0, "C", false, 0, "") + + // Right: Certificate ID + pdf.SetXY(pageWidth-120, bottomY) + pdf.CellFormat(80, 6, fmt.Sprintf("Zertifikats-Nr.: %s", data.CertificateID[:min(12, len(data.CertificateID))]), "", 0, "R", false, 0, "") + + // Signature line + sigY := 162.0 + pdf.SetDrawColor(150, 150, 150) + pdf.SetLineWidth(0.5) + + // Left signature + pdf.Line(50, sigY, 130, sigY) + pdf.SetFont("Helvetica", "", 9) + pdf.SetTextColor(120, 120, 120) + pdf.SetXY(50, sigY+2) + pdf.CellFormat(80, 5, "Datenschutzbeauftragter", "", 0, "C", false, 0, "") + + // Right signature + pdf.Line(pageWidth-130, sigY, pageWidth-50, sigY) + pdf.SetXY(pageWidth-130, sigY+2) + pdf.CellFormat(80, 5, "Geschaeftsfuehrung", "", 0, "C", false, 0, "") + + // Footer + pdf.SetFont("Helvetica", "", 8) + pdf.SetTextColor(160, 160, 160) + pdf.SetXY(0, pageHeight-22) + pdf.CellFormat(pageWidth, 5, "Dieses Zertifikat wurde elektronisch erstellt und ist ohne Unterschrift gueltig.", "", 0, "C", false, 0, "") + pdf.SetXY(0, pageHeight-17) + pdf.CellFormat(pageWidth, 5, fmt.Sprintf("Verifizierung unter: https://compliance.breakpilot.de/verify/%s", data.CertificateID), "", 0, "C", false, 0, "") + + // Generate PDF bytes + var buf bytes.Buffer + if err := pdf.Output(&buf); err != nil { + return nil, fmt.Errorf("failed to generate PDF: %w", err) + } + + return buf.Bytes(), nil +} + +func min(a, b int) int { + if a < b { + return a + } + return b +} diff --git a/admin-v2/ai-compliance-sdk/internal/academy/elevenlabs_client.go b/admin-v2/ai-compliance-sdk/internal/academy/elevenlabs_client.go new file mode 100644 index 0000000..3901c02 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/academy/elevenlabs_client.go @@ -0,0 +1,105 @@ +package academy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +// ElevenLabsClient handles text-to-speech via the ElevenLabs API +type ElevenLabsClient struct { + apiKey string + voiceID string + client *http.Client +} + +// NewElevenLabsClient creates a new ElevenLabs client +func NewElevenLabsClient() *ElevenLabsClient { + apiKey := os.Getenv("ELEVENLABS_API_KEY") + voiceID := os.Getenv("ELEVENLABS_VOICE_ID") + if voiceID == "" { + voiceID = "EXAVITQu4vr4xnSDxMaL" // Default: "Sarah" voice + } + + return &ElevenLabsClient{ + apiKey: apiKey, + voiceID: voiceID, + client: &http.Client{ + Timeout: 120 * time.Second, + }, + } +} + +// IsConfigured returns true if API key is set +func (c *ElevenLabsClient) IsConfigured() bool { + return c.apiKey != "" +} + +// TextToSpeechRequest represents the API request +type TextToSpeechRequest struct { + Text string `json:"text"` + ModelID string `json:"model_id"` + VoiceSettings VoiceSettings `json:"voice_settings"` +} + +// VoiceSettings controls voice parameters +type VoiceSettings struct { + Stability float64 `json:"stability"` + SimilarityBoost float64 `json:"similarity_boost"` + Style float64 `json:"style"` +} + +// TextToSpeech converts text to speech audio (MP3) +func (c *ElevenLabsClient) TextToSpeech(text string) ([]byte, error) { + if !c.IsConfigured() { + return nil, fmt.Errorf("ElevenLabs API key not configured") + } + + url := fmt.Sprintf("https://api.elevenlabs.io/v1/text-to-speech/%s", c.voiceID) + + reqBody := TextToSpeechRequest{ + Text: text, + ModelID: "eleven_multilingual_v2", + VoiceSettings: VoiceSettings{ + Stability: 0.5, + SimilarityBoost: 0.75, + Style: 0.5, + }, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("xi-api-key", c.apiKey) + req.Header.Set("Accept", "audio/mpeg") + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("ElevenLabs API request failed: %w", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + body, _ := io.ReadAll(resp.Body) + return nil, fmt.Errorf("ElevenLabs API error %d: %s", resp.StatusCode, string(body)) + } + + audioData, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read audio response: %w", err) + } + + return audioData, nil +} diff --git a/admin-v2/ai-compliance-sdk/internal/academy/heygen_client.go b/admin-v2/ai-compliance-sdk/internal/academy/heygen_client.go new file mode 100644 index 0000000..b0435e3 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/academy/heygen_client.go @@ -0,0 +1,184 @@ +package academy + +import ( + "bytes" + "encoding/json" + "fmt" + "io" + "net/http" + "os" + "time" +) + +// HeyGenClient handles avatar video generation via the HeyGen API +type HeyGenClient struct { + apiKey string + avatarID string + client *http.Client +} + +// NewHeyGenClient creates a new HeyGen client +func NewHeyGenClient() *HeyGenClient { + apiKey := os.Getenv("HEYGEN_API_KEY") + avatarID := os.Getenv("HEYGEN_AVATAR_ID") + if avatarID == "" { + avatarID = "josh_lite3_20230714" // Default avatar + } + + return &HeyGenClient{ + apiKey: apiKey, + avatarID: avatarID, + client: &http.Client{ + Timeout: 300 * time.Second, // Video generation can take time + }, + } +} + +// IsConfigured returns true if API key is set +func (c *HeyGenClient) IsConfigured() bool { + return c.apiKey != "" +} + +// CreateVideoRequest represents the HeyGen API request +type CreateVideoRequest struct { + VideoInputs []VideoInput `json:"video_inputs"` + Dimension Dimension `json:"dimension"` +} + +// VideoInput represents a single video segment +type VideoInput struct { + Character Character `json:"character"` + Voice VideoVoice `json:"voice"` +} + +// Character represents the avatar +type Character struct { + Type string `json:"type"` + AvatarID string `json:"avatar_id"` +} + +// VideoVoice represents the voice/audio source +type VideoVoice struct { + Type string `json:"type"` // "audio" for pre-generated audio + AudioURL string `json:"audio_url,omitempty"` + InputText string `json:"input_text,omitempty"` +} + +// Dimension represents video dimensions +type Dimension struct { + Width int `json:"width"` + Height int `json:"height"` +} + +// CreateVideoResponse represents the HeyGen API response +type CreateVideoResponse struct { + Data struct { + VideoID string `json:"video_id"` + } `json:"data"` + Error interface{} `json:"error"` +} + +// HeyGenVideoStatus represents video status from HeyGen +type HeyGenVideoStatus struct { + Data struct { + Status string `json:"status"` // processing, completed, failed + VideoURL string `json:"video_url"` + } `json:"data"` +} + +// CreateVideo creates a video with the avatar and audio +func (c *HeyGenClient) CreateVideo(audioURL string) (*CreateVideoResponse, error) { + if !c.IsConfigured() { + return nil, fmt.Errorf("HeyGen API key not configured") + } + + url := "https://api.heygen.com/v2/video/generate" + + reqBody := CreateVideoRequest{ + VideoInputs: []VideoInput{ + { + Character: Character{ + Type: "avatar", + AvatarID: c.avatarID, + }, + Voice: VideoVoice{ + Type: "audio", + AudioURL: audioURL, + }, + }, + }, + Dimension: Dimension{ + Width: 1920, + Height: 1080, + }, + } + + jsonBody, err := json.Marshal(reqBody) + if err != nil { + return nil, fmt.Errorf("failed to marshal request: %w", err) + } + + req, err := http.NewRequest("POST", url, bytes.NewReader(jsonBody)) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("Content-Type", "application/json") + req.Header.Set("X-Api-Key", c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("HeyGen API request failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusCreated { + return nil, fmt.Errorf("HeyGen API error %d: %s", resp.StatusCode, string(body)) + } + + var result CreateVideoResponse + if err := json.Unmarshal(body, &result); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &result, nil +} + +// GetVideoStatus checks the status of a video generation job +func (c *HeyGenClient) GetVideoStatus(videoID string) (*HeyGenVideoStatus, error) { + if !c.IsConfigured() { + return nil, fmt.Errorf("HeyGen API key not configured") + } + + url := fmt.Sprintf("https://api.heygen.com/v1/video_status.get?video_id=%s", videoID) + + req, err := http.NewRequest("GET", url, nil) + if err != nil { + return nil, fmt.Errorf("failed to create request: %w", err) + } + + req.Header.Set("X-Api-Key", c.apiKey) + + resp, err := c.client.Do(req) + if err != nil { + return nil, fmt.Errorf("HeyGen API request failed: %w", err) + } + defer resp.Body.Close() + + body, err := io.ReadAll(resp.Body) + if err != nil { + return nil, fmt.Errorf("failed to read response: %w", err) + } + + var status HeyGenVideoStatus + if err := json.Unmarshal(body, &status); err != nil { + return nil, fmt.Errorf("failed to parse response: %w", err) + } + + return &status, nil +} diff --git a/admin-v2/ai-compliance-sdk/internal/academy/video_generator.go b/admin-v2/ai-compliance-sdk/internal/academy/video_generator.go new file mode 100644 index 0000000..9f6ff59 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/academy/video_generator.go @@ -0,0 +1,91 @@ +package academy + +import ( + "fmt" + "log" +) + +// VideoGenerator orchestrates video generation with 3-tier fallback: +// 1. HeyGen + ElevenLabs -> Avatar video with voice +// 2. ElevenLabs only -> Audio podcast style +// 3. No external services -> Text + Quiz only +type VideoGenerator struct { + elevenLabs *ElevenLabsClient + heyGen *HeyGenClient +} + +// NewVideoGenerator creates a new video generator +func NewVideoGenerator() *VideoGenerator { + return &VideoGenerator{ + elevenLabs: NewElevenLabsClient(), + heyGen: NewHeyGenClient(), + } +} + +// GenerationMode describes the available generation mode +type GenerationMode string + +const ( + ModeAvatarVideo GenerationMode = "avatar_video" // HeyGen + ElevenLabs + ModeAudioOnly GenerationMode = "audio_only" // ElevenLabs only + ModeTextOnly GenerationMode = "text_only" // No external services +) + +// GetAvailableMode returns the best available generation mode +func (vg *VideoGenerator) GetAvailableMode() GenerationMode { + if vg.heyGen.IsConfigured() && vg.elevenLabs.IsConfigured() { + return ModeAvatarVideo + } + if vg.elevenLabs.IsConfigured() { + return ModeAudioOnly + } + return ModeTextOnly +} + +// GenerateAudio generates audio from text using ElevenLabs +func (vg *VideoGenerator) GenerateAudio(text string) ([]byte, error) { + if !vg.elevenLabs.IsConfigured() { + return nil, fmt.Errorf("ElevenLabs not configured") + } + + log.Printf("Generating audio for text (%d chars)...", len(text)) + return vg.elevenLabs.TextToSpeech(text) +} + +// GenerateVideo generates a video from audio using HeyGen +func (vg *VideoGenerator) GenerateVideo(audioURL string) (string, error) { + if !vg.heyGen.IsConfigured() { + return "", fmt.Errorf("HeyGen not configured") + } + + log.Printf("Creating HeyGen video with audio: %s", audioURL) + resp, err := vg.heyGen.CreateVideo(audioURL) + if err != nil { + return "", err + } + + return resp.Data.VideoID, nil +} + +// CheckVideoStatus checks if a HeyGen video is ready +func (vg *VideoGenerator) CheckVideoStatus(videoID string) (string, string, error) { + if !vg.heyGen.IsConfigured() { + return "", "", fmt.Errorf("HeyGen not configured") + } + + status, err := vg.heyGen.GetVideoStatus(videoID) + if err != nil { + return "", "", err + } + + return status.Data.Status, status.Data.VideoURL, nil +} + +// GetStatus returns the configuration status +func (vg *VideoGenerator) GetStatus() map[string]interface{} { + return map[string]interface{}{ + "mode": string(vg.GetAvailableMode()), + "elevenLabsConfigured": vg.elevenLabs.IsConfigured(), + "heyGenConfigured": vg.heyGen.IsConfigured(), + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/api/academy.go b/admin-v2/ai-compliance-sdk/internal/api/academy.go new file mode 100644 index 0000000..f7e8071 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/api/academy.go @@ -0,0 +1,950 @@ +package api + +import ( + "fmt" + "net/http" + "time" + + "github.com/breakpilot/ai-compliance-sdk/internal/academy" + "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" +) + +// AcademyHandler handles all Academy-related HTTP requests +type AcademyHandler struct { + dbPool *db.Pool + llmService *llm.Service + ragService *rag.Service + academyStore *db.AcademyMemStore +} + +// NewAcademyHandler creates a new Academy handler +func NewAcademyHandler(dbPool *db.Pool, llmService *llm.Service, ragService *rag.Service) *AcademyHandler { + return &AcademyHandler{ + dbPool: dbPool, + llmService: llmService, + ragService: ragService, + academyStore: db.NewAcademyMemStore(), + } +} + +func (h *AcademyHandler) getTenantID(c *gin.Context) string { + tid := c.GetHeader("X-Tenant-ID") + if tid == "" { + tid = c.Query("tenantId") + } + if tid == "" { + tid = "default-tenant" + } + return tid +} + +// --------------------------------------------------------------------------- +// Course CRUD +// --------------------------------------------------------------------------- + +// ListCourses returns all courses for the tenant +func (h *AcademyHandler) ListCourses(c *gin.Context) { + tenantID := h.getTenantID(c) + rows := h.academyStore.ListCourses(tenantID) + + courses := make([]AcademyCourse, 0, len(rows)) + for _, row := range rows { + lessons := h.buildLessonsForCourse(row.ID) + courses = append(courses, courseRowToResponse(row, lessons)) + } + + SuccessResponse(c, courses) +} + +// GetCourse returns a single course with its lessons +func (h *AcademyHandler) GetCourse(c *gin.Context) { + id := c.Param("id") + row, err := h.academyStore.GetCourse(id) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") + return + } + + lessons := h.buildLessonsForCourse(row.ID) + SuccessResponse(c, courseRowToResponse(row, lessons)) +} + +// CreateCourse creates a new course with optional lessons +func (h *AcademyHandler) CreateCourse(c *gin.Context) { + var req CreateCourseRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") + return + } + + passingScore := req.PassingScore + if passingScore == 0 { + passingScore = 70 + } + + roles := req.RequiredForRoles + if len(roles) == 0 { + roles = []string{"all"} + } + + courseRow := h.academyStore.CreateCourse(&db.AcademyCourseRow{ + TenantID: req.TenantID, + Title: req.Title, + Description: req.Description, + Category: req.Category, + PassingScore: passingScore, + DurationMinutes: req.DurationMinutes, + RequiredForRoles: roles, + Status: "draft", + }) + + // Create lessons + for i, lessonReq := range req.Lessons { + order := lessonReq.Order + if order == 0 { + order = i + 1 + } + lessonRow := h.academyStore.CreateLesson(&db.AcademyLessonRow{ + CourseID: courseRow.ID, + Title: lessonReq.Title, + Type: lessonReq.Type, + ContentMarkdown: lessonReq.ContentMarkdown, + VideoURL: lessonReq.VideoURL, + SortOrder: order, + DurationMinutes: lessonReq.DurationMinutes, + }) + + // Create quiz questions for this lesson + for j, qReq := range lessonReq.QuizQuestions { + qOrder := qReq.Order + if qOrder == 0 { + qOrder = j + 1 + } + h.academyStore.CreateQuizQuestion(&db.AcademyQuizQuestionRow{ + LessonID: lessonRow.ID, + Question: qReq.Question, + Options: qReq.Options, + CorrectOptionIndex: qReq.CorrectOptionIndex, + Explanation: qReq.Explanation, + SortOrder: qOrder, + }) + } + } + + lessons := h.buildLessonsForCourse(courseRow.ID) + c.JSON(http.StatusCreated, Response{ + Success: true, + Data: courseRowToResponse(courseRow, lessons), + }) +} + +// UpdateCourse updates an existing course +func (h *AcademyHandler) UpdateCourse(c *gin.Context) { + id := c.Param("id") + + var req UpdateCourseRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") + return + } + + updates := make(map[string]interface{}) + if req.Title != nil { + updates["title"] = *req.Title + } + if req.Description != nil { + updates["description"] = *req.Description + } + if req.Category != nil { + updates["category"] = *req.Category + } + if req.DurationMinutes != nil { + updates["durationminutes"] = *req.DurationMinutes + } + if req.PassingScore != nil { + updates["passingscore"] = *req.PassingScore + } + if req.RequiredForRoles != nil { + updates["requiredforroles"] = req.RequiredForRoles + } + + row, err := h.academyStore.UpdateCourse(id, updates) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") + return + } + + lessons := h.buildLessonsForCourse(row.ID) + SuccessResponse(c, courseRowToResponse(row, lessons)) +} + +// DeleteCourse deletes a course and all related data +func (h *AcademyHandler) DeleteCourse(c *gin.Context) { + id := c.Param("id") + + if err := h.academyStore.DeleteCourse(id); err != nil { + ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") + return + } + + SuccessResponse(c, gin.H{ + "courseId": id, + "deletedAt": now(), + }) +} + +// GetStatistics returns academy statistics for the tenant +func (h *AcademyHandler) GetStatistics(c *gin.Context) { + tenantID := h.getTenantID(c) + stats := h.academyStore.GetStatistics(tenantID) + + SuccessResponse(c, AcademyStatistics{ + TotalCourses: stats.TotalCourses, + TotalEnrollments: stats.TotalEnrollments, + CompletionRate: int(stats.CompletionRate), + OverdueCount: stats.OverdueCount, + ByCategory: stats.ByCategory, + ByStatus: stats.ByStatus, + }) +} + +// --------------------------------------------------------------------------- +// Enrollments +// --------------------------------------------------------------------------- + +// ListEnrollments returns enrollments filtered by tenant and optionally course +func (h *AcademyHandler) ListEnrollments(c *gin.Context) { + tenantID := h.getTenantID(c) + courseID := c.Query("courseId") + + rows := h.academyStore.ListEnrollments(tenantID, courseID) + + enrollments := make([]AcademyEnrollment, 0, len(rows)) + for _, row := range rows { + enrollments = append(enrollments, enrollmentRowToResponse(row)) + } + + SuccessResponse(c, enrollments) +} + +// EnrollUser enrolls a user in a course +func (h *AcademyHandler) EnrollUser(c *gin.Context) { + var req EnrollUserRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") + return + } + + deadline, err := time.Parse(time.RFC3339, req.Deadline) + if err != nil { + deadline, err = time.Parse("2006-01-02", req.Deadline) + if err != nil { + ErrorResponse(c, http.StatusBadRequest, "Invalid deadline format. Use RFC3339 or YYYY-MM-DD.", "INVALID_DEADLINE") + return + } + } + + row := h.academyStore.CreateEnrollment(&db.AcademyEnrollmentRow{ + TenantID: req.TenantID, + CourseID: req.CourseID, + UserID: req.UserID, + UserName: req.UserName, + UserEmail: req.UserEmail, + Status: "not_started", + Progress: 0, + Deadline: deadline, + }) + + c.JSON(http.StatusCreated, Response{ + Success: true, + Data: enrollmentRowToResponse(row), + }) +} + +// UpdateProgress updates the progress of an enrollment +func (h *AcademyHandler) UpdateProgress(c *gin.Context) { + id := c.Param("id") + + var req UpdateProgressRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") + return + } + + enrollment, err := h.academyStore.GetEnrollment(id) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND") + return + } + + updates := map[string]interface{}{ + "progress": req.Progress, + } + + // Auto-update status based on progress + if req.Progress >= 100 { + updates["status"] = "completed" + t := time.Now() + updates["completedat"] = &t + } else if req.Progress > 0 && enrollment.Status == "not_started" { + updates["status"] = "in_progress" + } + + row, err := h.academyStore.UpdateEnrollment(id, updates) + if err != nil { + ErrorResponse(c, http.StatusInternalServerError, "Failed to update progress", "UPDATE_FAILED") + return + } + + // Upsert lesson progress if lessonID provided + if req.LessonID != "" { + t := time.Now() + h.academyStore.UpsertLessonProgress(&db.AcademyLessonProgressRow{ + EnrollmentID: id, + LessonID: req.LessonID, + Completed: true, + CompletedAt: &t, + }) + } + + SuccessResponse(c, enrollmentRowToResponse(row)) +} + +// CompleteEnrollment marks an enrollment as completed +func (h *AcademyHandler) CompleteEnrollment(c *gin.Context) { + id := c.Param("id") + + t := time.Now() + updates := map[string]interface{}{ + "status": "completed", + "progress": 100, + "completedat": &t, + } + + row, err := h.academyStore.UpdateEnrollment(id, updates) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND") + return + } + + SuccessResponse(c, enrollmentRowToResponse(row)) +} + +// --------------------------------------------------------------------------- +// Quiz +// --------------------------------------------------------------------------- + +// SubmitQuiz evaluates quiz answers for a lesson +func (h *AcademyHandler) SubmitQuiz(c *gin.Context) { + lessonID := c.Param("id") + + var req SubmitQuizRequest + if err := c.ShouldBindJSON(&req); err != nil { + ErrorResponse(c, http.StatusBadRequest, err.Error(), "INVALID_REQUEST") + return + } + + // Get the lesson + lesson, err := h.academyStore.GetLesson(lessonID) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Lesson not found", "LESSON_NOT_FOUND") + return + } + + // Get quiz questions + questions := h.academyStore.ListQuizQuestions(lessonID) + if len(questions) == 0 { + ErrorResponse(c, http.StatusBadRequest, "No quiz questions found for this lesson", "NO_QUIZ_QUESTIONS") + return + } + + if len(req.Answers) != len(questions) { + ErrorResponse(c, http.StatusBadRequest, + fmt.Sprintf("Expected %d answers, got %d", len(questions), len(req.Answers)), + "ANSWER_COUNT_MISMATCH") + return + } + + // Evaluate answers + correctCount := 0 + results := make([]QuizQuestionResult, len(questions)) + for i, q := range questions { + correct := req.Answers[i] == q.CorrectOptionIndex + if correct { + correctCount++ + } + results[i] = QuizQuestionResult{ + QuestionID: q.ID, + Correct: correct, + Explanation: q.Explanation, + } + } + + score := 0 + if len(questions) > 0 { + score = int(float64(correctCount) / float64(len(questions)) * 100) + } + + // Determine pass/fail based on course's passing score + passingScore := 70 // default + course, err := h.academyStore.GetCourse(lesson.CourseID) + if err == nil && course.PassingScore > 0 { + passingScore = course.PassingScore + } + + SuccessResponse(c, SubmitQuizResponse{ + Score: score, + Passed: score >= passingScore, + CorrectAnswers: correctCount, + TotalQuestions: len(questions), + Results: results, + }) +} + +// --------------------------------------------------------------------------- +// Certificates +// --------------------------------------------------------------------------- + +// GenerateCertificateEndpoint generates a certificate for a completed enrollment +func (h *AcademyHandler) GenerateCertificateEndpoint(c *gin.Context) { + enrollmentID := c.Param("id") + + enrollment, err := h.academyStore.GetEnrollment(enrollmentID) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Enrollment not found", "ENROLLMENT_NOT_FOUND") + return + } + + // Check if already has certificate + if enrollment.CertificateID != "" { + existing, err := h.academyStore.GetCertificate(enrollment.CertificateID) + if err == nil { + SuccessResponse(c, certificateRowToResponse(existing)) + return + } + } + + // Get course name + courseName := "Unbekannter Kurs" + course, err := h.academyStore.GetCourse(enrollment.CourseID) + if err == nil { + courseName = course.Title + } + + issuedAt := time.Now() + validUntil := issuedAt.AddDate(1, 0, 0) // 1 year validity + + cert := h.academyStore.CreateCertificate(&db.AcademyCertificateRow{ + TenantID: enrollment.TenantID, + EnrollmentID: enrollmentID, + CourseID: enrollment.CourseID, + UserID: enrollment.UserID, + UserName: enrollment.UserName, + CourseName: courseName, + Score: enrollment.Progress, + IssuedAt: issuedAt, + ValidUntil: validUntil, + }) + + // Update enrollment with certificate ID + h.academyStore.UpdateEnrollment(enrollmentID, map[string]interface{}{ + "certificateid": cert.ID, + }) + + c.JSON(http.StatusCreated, Response{ + Success: true, + Data: certificateRowToResponse(cert), + }) +} + +// GetCertificate returns a certificate by ID +func (h *AcademyHandler) GetCertificate(c *gin.Context) { + id := c.Param("id") + + cert, err := h.academyStore.GetCertificate(id) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Certificate not found", "CERTIFICATE_NOT_FOUND") + return + } + + SuccessResponse(c, certificateRowToResponse(cert)) +} + +// DownloadCertificatePDF returns the PDF for a certificate +func (h *AcademyHandler) DownloadCertificatePDF(c *gin.Context) { + id := c.Param("id") + + cert, err := h.academyStore.GetCertificate(id) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Certificate not found", "CERTIFICATE_NOT_FOUND") + return + } + + if cert.PdfURL != "" { + c.Redirect(http.StatusFound, cert.PdfURL) + return + } + + // Generate PDF on-the-fly + pdfBytes, err := academy.GenerateCertificatePDF(academy.CertificateData{ + CertificateID: cert.ID, + UserName: cert.UserName, + CourseName: cert.CourseName, + CompanyName: "", + Score: cert.Score, + IssuedAt: cert.IssuedAt, + ValidUntil: cert.ValidUntil, + }) + if err != nil { + ErrorResponse(c, http.StatusInternalServerError, "Failed to generate PDF", "PDF_GENERATION_FAILED") + return + } + + c.Header("Content-Disposition", fmt.Sprintf("attachment; filename=zertifikat-%s.pdf", cert.ID[:min(8, len(cert.ID))])) + c.Data(http.StatusOK, "application/pdf", pdfBytes) +} + +// --------------------------------------------------------------------------- +// AI Course Generation +// --------------------------------------------------------------------------- + +// GenerateCourse generates a course using AI +func (h *AcademyHandler) GenerateCourse(c *gin.Context) { + var req GenerateCourseRequest + 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 = req.Topic + " Compliance Schulung" + } + 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 course content (mock for now) + course := h.generateMockCourse(req) + + // Save to store + courseRow := h.academyStore.CreateCourse(&db.AcademyCourseRow{ + TenantID: req.TenantID, + Title: course.Title, + Description: course.Description, + Category: req.Category, + PassingScore: 70, + DurationMinutes: course.DurationMinutes, + RequiredForRoles: []string{"all"}, + Status: "draft", + }) + + for _, lesson := range course.Lessons { + lessonRow := h.academyStore.CreateLesson(&db.AcademyLessonRow{ + CourseID: courseRow.ID, + Title: lesson.Title, + Type: lesson.Type, + ContentMarkdown: lesson.ContentMarkdown, + SortOrder: lesson.Order, + DurationMinutes: lesson.DurationMinutes, + }) + + for _, q := range lesson.QuizQuestions { + h.academyStore.CreateQuizQuestion(&db.AcademyQuizQuestionRow{ + LessonID: lessonRow.ID, + Question: q.Question, + Options: q.Options, + CorrectOptionIndex: q.CorrectOptionIndex, + Explanation: q.Explanation, + SortOrder: q.Order, + }) + } + } + + lessons := h.buildLessonsForCourse(courseRow.ID) + c.JSON(http.StatusCreated, Response{ + Success: true, + Data: gin.H{ + "course": courseRowToResponse(courseRow, lessons), + "ragSources": ragSources, + "model": h.llmService.GetModel(), + }, + }) +} + +// RegenerateLesson regenerates a single lesson using AI +func (h *AcademyHandler) RegenerateLesson(c *gin.Context) { + lessonID := c.Param("id") + + _, err := h.academyStore.GetLesson(lessonID) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Lesson not found", "LESSON_NOT_FOUND") + return + } + + // For now, return the existing lesson + SuccessResponse(c, gin.H{ + "lessonId": lessonID, + "status": "regeneration_pending", + "message": "AI lesson regeneration will be available in a future version", + }) +} + +// --------------------------------------------------------------------------- +// Video Generation +// --------------------------------------------------------------------------- + +// GenerateVideos initiates video generation for all lessons in a course +func (h *AcademyHandler) GenerateVideos(c *gin.Context) { + courseID := c.Param("id") + + _, err := h.academyStore.GetCourse(courseID) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") + return + } + + lessons := h.academyStore.ListLessons(courseID) + lessonStatuses := make([]LessonVideoStatus, 0, len(lessons)) + for _, l := range lessons { + if l.Type == "text" || l.Type == "video" { + lessonStatuses = append(lessonStatuses, LessonVideoStatus{ + LessonID: l.ID, + Status: "pending", + }) + } + } + + SuccessResponse(c, VideoStatusResponse{ + CourseID: courseID, + Status: "pending", + Lessons: lessonStatuses, + }) +} + +// GetVideoStatus returns the video generation status for a course +func (h *AcademyHandler) GetVideoStatus(c *gin.Context) { + courseID := c.Param("id") + + _, err := h.academyStore.GetCourse(courseID) + if err != nil { + ErrorResponse(c, http.StatusNotFound, "Course not found", "COURSE_NOT_FOUND") + return + } + + lessons := h.academyStore.ListLessons(courseID) + lessonStatuses := make([]LessonVideoStatus, 0, len(lessons)) + for _, l := range lessons { + status := LessonVideoStatus{ + LessonID: l.ID, + Status: "not_started", + VideoURL: l.VideoURL, + AudioURL: l.AudioURL, + } + if l.VideoURL != "" { + status.Status = "completed" + } + lessonStatuses = append(lessonStatuses, status) + } + + overallStatus := "not_started" + hasCompleted := false + hasPending := false + for _, s := range lessonStatuses { + if s.Status == "completed" { + hasCompleted = true + } else { + hasPending = true + } + } + if hasCompleted && !hasPending { + overallStatus = "completed" + } else if hasCompleted && hasPending { + overallStatus = "processing" + } + + SuccessResponse(c, VideoStatusResponse{ + CourseID: courseID, + Status: overallStatus, + Lessons: lessonStatuses, + }) +} + +// --------------------------------------------------------------------------- +// Helpers +// --------------------------------------------------------------------------- + +func (h *AcademyHandler) buildLessonsForCourse(courseID string) []AcademyLesson { + lessonRows := h.academyStore.ListLessons(courseID) + lessons := make([]AcademyLesson, 0, len(lessonRows)) + for _, lr := range lessonRows { + var questions []AcademyQuizQuestion + if lr.Type == "quiz" { + qRows := h.academyStore.ListQuizQuestions(lr.ID) + questions = make([]AcademyQuizQuestion, 0, len(qRows)) + for _, qr := range qRows { + questions = append(questions, quizQuestionRowToResponse(qr)) + } + } + lessons = append(lessons, lessonRowToResponse(lr, questions)) + } + return lessons +} + +func courseRowToResponse(row *db.AcademyCourseRow, lessons []AcademyLesson) AcademyCourse { + return AcademyCourse{ + ID: row.ID, + TenantID: row.TenantID, + Title: row.Title, + Description: row.Description, + Category: row.Category, + PassingScore: row.PassingScore, + DurationMinutes: row.DurationMinutes, + RequiredForRoles: row.RequiredForRoles, + Status: row.Status, + Lessons: lessons, + CreatedAt: row.CreatedAt.Format(time.RFC3339), + UpdatedAt: row.UpdatedAt.Format(time.RFC3339), + } +} + +func lessonRowToResponse(row *db.AcademyLessonRow, questions []AcademyQuizQuestion) AcademyLesson { + return AcademyLesson{ + ID: row.ID, + CourseID: row.CourseID, + Title: row.Title, + Type: row.Type, + ContentMarkdown: row.ContentMarkdown, + VideoURL: row.VideoURL, + AudioURL: row.AudioURL, + Order: row.SortOrder, + DurationMinutes: row.DurationMinutes, + QuizQuestions: questions, + } +} + +func quizQuestionRowToResponse(row *db.AcademyQuizQuestionRow) AcademyQuizQuestion { + return AcademyQuizQuestion{ + ID: row.ID, + LessonID: row.LessonID, + Question: row.Question, + Options: row.Options, + CorrectOptionIndex: row.CorrectOptionIndex, + Explanation: row.Explanation, + Order: row.SortOrder, + } +} + +func enrollmentRowToResponse(row *db.AcademyEnrollmentRow) AcademyEnrollment { + e := AcademyEnrollment{ + ID: row.ID, + TenantID: row.TenantID, + CourseID: row.CourseID, + UserID: row.UserID, + UserName: row.UserName, + UserEmail: row.UserEmail, + Status: row.Status, + Progress: row.Progress, + StartedAt: row.StartedAt.Format(time.RFC3339), + CertificateID: row.CertificateID, + Deadline: row.Deadline.Format(time.RFC3339), + CreatedAt: row.CreatedAt.Format(time.RFC3339), + UpdatedAt: row.UpdatedAt.Format(time.RFC3339), + } + if row.CompletedAt != nil { + e.CompletedAt = row.CompletedAt.Format(time.RFC3339) + } + return e +} + +func certificateRowToResponse(row *db.AcademyCertificateRow) AcademyCertificate { + return AcademyCertificate{ + ID: row.ID, + TenantID: row.TenantID, + EnrollmentID: row.EnrollmentID, + CourseID: row.CourseID, + UserID: row.UserID, + UserName: row.UserName, + CourseName: row.CourseName, + Score: row.Score, + IssuedAt: row.IssuedAt.Format(time.RFC3339), + ValidUntil: row.ValidUntil.Format(time.RFC3339), + PdfURL: row.PdfURL, + } +} + +// --------------------------------------------------------------------------- +// Mock Course Generator (used when LLM is not available) +// --------------------------------------------------------------------------- + +func (h *AcademyHandler) generateMockCourse(req GenerateCourseRequest) AcademyCourse { + switch req.Category { + case "dsgvo_basics": + return h.mockDSGVOCourse(req) + case "it_security": + return h.mockITSecurityCourse(req) + case "ai_literacy": + return h.mockAILiteracyCourse(req) + case "whistleblower_protection": + return h.mockWhistleblowerCourse(req) + default: + return h.mockDSGVOCourse(req) + } +} + +func (h *AcademyHandler) mockDSGVOCourse(req GenerateCourseRequest) AcademyCourse { + return AcademyCourse{ + Title: "DSGVO-Grundlagen fuer Mitarbeiter", + Description: "Umfassende Einfuehrung in die Datenschutz-Grundverordnung. Vermittelt die wichtigsten Grundsaetze des Datenschutzes, Betroffenenrechte und die korrekte Handhabung personenbezogener Daten.", + DurationMinutes: 90, + Lessons: []AcademyLesson{ + { + Title: "Was ist die DSGVO?", + Type: "text", + Order: 1, + DurationMinutes: 15, + ContentMarkdown: "# Was ist die DSGVO?\n\nDie Datenschutz-Grundverordnung (DSGVO) ist eine Verordnung der EU, die seit dem 25. Mai 2018 gilt. Sie schuetzt die Grundrechte natuerlicher Personen bei der Verarbeitung personenbezogener Daten.\n\n## Warum ist die DSGVO wichtig?\n\n- **Einheitlicher Datenschutz** in der gesamten EU\n- **Hohe Bussgelder** bei Verstoessen (bis 20 Mio. EUR oder 4% des Jahresumsatzes)\n- **Staerkung der Betroffenenrechte** (Auskunft, Loeschung, Widerspruch)\n\n## Zentrale Begriffe\n\n- **Personenbezogene Daten**: Alle Informationen, die sich auf eine identifizierte oder identifizierbare Person beziehen\n- **Verantwortlicher**: Die Stelle, die ueber Zweck und Mittel der Verarbeitung entscheidet\n- **Auftragsverarbeiter**: Verarbeitet Daten im Auftrag des Verantwortlichen", + }, + { + Title: "Die 7 Grundsaetze der DSGVO", + Type: "text", + Order: 2, + DurationMinutes: 20, + ContentMarkdown: "# Die 7 Grundsaetze der DSGVO (Art. 5)\n\n## 1. Rechtmaessigkeit, Verarbeitung nach Treu und Glauben, Transparenz\nPersonenbezogene Daten muessen auf rechtmaessige Weise verarbeitet werden.\n\n## 2. Zweckbindung\nDaten duerfen nur fuer festgelegte, eindeutige und legitime Zwecke erhoben werden.\n\n## 3. Datenminimierung\nEs duerfen nur Daten erhoben werden, die fuer den Zweck erforderlich sind.\n\n## 4. Richtigkeit\nDaten muessen sachlich richtig und auf dem neuesten Stand sein.\n\n## 5. Speicherbegrenzung\nDaten duerfen nur so lange gespeichert werden, wie es fuer den Zweck erforderlich ist.\n\n## 6. Integritaet und Vertraulichkeit\nDaten muessen vor unbefugtem Zugriff geschuetzt werden.\n\n## 7. Rechenschaftspflicht\nDer Verantwortliche muss die Einhaltung der Grundsaetze nachweisen koennen.", + }, + { + Title: "Betroffenenrechte (Art. 15-22 DSGVO)", + Type: "text", + Order: 3, + DurationMinutes: 20, + ContentMarkdown: "# Betroffenenrechte\n\n## Recht auf Auskunft (Art. 15)\nJede Person hat das Recht zu erfahren, ob und welche Daten ueber sie verarbeitet werden.\n\n## Recht auf Berichtigung (Art. 16)\nUnrichtige Daten muessen berichtigt werden.\n\n## Recht auf Loeschung (Art. 17)\nDas 'Recht auf Vergessenwerden' ermoeglicht die Loeschung personenbezogener Daten.\n\n## Recht auf Einschraenkung (Art. 18)\nBetroffene koennen die Verarbeitung einschraenken lassen.\n\n## Recht auf Datenuebertragbarkeit (Art. 20)\nDaten muessen in einem maschinenlesbaren Format bereitgestellt werden.\n\n## Widerspruchsrecht (Art. 21)\nBetroffene koennen der Verarbeitung widersprechen.", + }, + { + Title: "Datenschutz im Arbeitsalltag", + Type: "text", + Order: 4, + DurationMinutes: 15, + ContentMarkdown: "# Datenschutz im Arbeitsalltag\n\n## E-Mails\n- Keine personenbezogenen Daten unverschluesselt versenden\n- BCC statt CC bei Massenversand\n- Vorsicht bei Anhangen\n\n## Bildschirmsperre\n- Computer bei Abwesenheit sperren (Win+L / Cmd+Ctrl+Q)\n- Automatische Sperre nach 5 Minuten\n\n## Clean Desk Policy\n- Keine sensiblen Dokumente offen liegen lassen\n- Aktenvernichter fuer Papierdokumente\n\n## Homeoffice\n- VPN nutzen\n- Kein oeffentliches WLAN fuer Firmendaten\n- Bildschirm vor Mitlesern schuetzen\n\n## Datenpannen melden\n- **Sofort** den Datenschutzbeauftragten informieren\n- Innerhalb von 72 Stunden an die Aufsichtsbehoerde\n- Dokumentation der Panne", + }, + { + Title: "Wissenstest: DSGVO-Grundlagen", + Type: "quiz", + Order: 5, + DurationMinutes: 20, + QuizQuestions: []AcademyQuizQuestion{ + { + Question: "Seit wann gilt die DSGVO?", + Options: []string{"1. Januar 2016", "25. Mai 2018", "1. Januar 2020", "25. Mai 2020"}, + CorrectOptionIndex: 1, + Explanation: "Die DSGVO gilt seit dem 25. Mai 2018 in allen EU-Mitgliedstaaten.", + Order: 1, + }, + { + Question: "Was sind personenbezogene Daten?", + Options: []string{"Nur Name und Adresse", "Alle Informationen, die sich auf eine identifizierbare Person beziehen", "Nur digitale Daten", "Nur sensible Gesundheitsdaten"}, + CorrectOptionIndex: 1, + Explanation: "Personenbezogene Daten umfassen alle Informationen, die sich auf eine identifizierte oder identifizierbare natuerliche Person beziehen.", + Order: 2, + }, + { + Question: "Wie hoch kann das Bussgeld bei DSGVO-Verstoessen maximal sein?", + Options: []string{"1 Mio. EUR", "5 Mio. EUR", "10 Mio. EUR oder 2% des Jahresumsatzes", "20 Mio. EUR oder 4% des Jahresumsatzes"}, + CorrectOptionIndex: 3, + Explanation: "Bei schwerwiegenden Verstoessen koennen Bussgelder von bis zu 20 Mio. EUR oder 4% des weltweiten Jahresumsatzes verhaengt werden.", + Order: 3, + }, + { + Question: "Was bedeutet das Prinzip der Datenminimierung?", + Options: []string{"Alle Daten muessen verschluesselt werden", "Es duerfen nur fuer den Zweck erforderliche Daten erhoben werden", "Daten muessen nach 30 Tagen geloescht werden", "Nur Administratoren duerfen auf Daten zugreifen"}, + CorrectOptionIndex: 1, + Explanation: "Datenminimierung bedeutet, dass nur die fuer den jeweiligen Zweck erforderlichen Daten erhoben und verarbeitet werden duerfen.", + Order: 4, + }, + { + Question: "Innerhalb welcher Frist muss eine Datenpanne der Aufsichtsbehoerde gemeldet werden?", + Options: []string{"24 Stunden", "48 Stunden", "72 Stunden", "7 Tage"}, + CorrectOptionIndex: 2, + Explanation: "Gemaess Art. 33 DSGVO muss eine Datenpanne innerhalb von 72 Stunden nach Bekanntwerden der Aufsichtsbehoerde gemeldet werden.", + Order: 5, + }, + }, + }, + }, + } +} + +func (h *AcademyHandler) mockITSecurityCourse(req GenerateCourseRequest) AcademyCourse { + return AcademyCourse{ + Title: "IT-Sicherheit & Cybersecurity Awareness", + Description: "Sensibilisierung fuer IT-Sicherheitsrisiken und Best Practices im Umgang mit Phishing, Passwoertern und Social Engineering.", + DurationMinutes: 60, + Lessons: []AcademyLesson{ + {Title: "Phishing erkennen und vermeiden", Type: "text", Order: 1, DurationMinutes: 15, + ContentMarkdown: "# Phishing erkennen\n\n## Typische Merkmale\n- Dringlichkeit ('Ihr Konto wird gesperrt!')\n- Unbekannter Absender\n- Verdaechtige Links\n- Rechtschreibfehler\n\n## Was tun bei Verdacht?\n1. Link NICHT anklicken\n2. Anhang NICHT oeffnen\n3. IT-Sicherheit informieren"}, + {Title: "Sichere Passwoerter und MFA", Type: "text", Order: 2, DurationMinutes: 15, + ContentMarkdown: "# Sichere Passwoerter\n\n## Regeln\n- Mindestens 12 Zeichen\n- Gross-/Kleinbuchstaben, Zahlen, Sonderzeichen\n- Fuer jeden Dienst ein eigenes Passwort\n- Passwort-Manager verwenden\n\n## Multi-Faktor-Authentifizierung\n- Immer aktivieren wenn moeglich\n- App-basiert (z.B. Microsoft Authenticator) bevorzugen"}, + {Title: "Social Engineering", Type: "text", Order: 3, DurationMinutes: 15, + ContentMarkdown: "# Social Engineering\n\nAngreifer nutzen menschliche Schwaechen aus.\n\n## Methoden\n- **Pretexting**: Falsche Identitaet vortaeuschen\n- **Tailgating**: Unbefugter Zutritt durch Hinterherfolgen\n- **CEO Fraud**: Gefaelschte Anweisungen vom Vorgesetzten\n\n## Schutz\n- Identitaet immer verifizieren\n- Bei Unsicherheit nachfragen"}, + {Title: "Wissenstest: IT-Sicherheit", Type: "quiz", Order: 4, DurationMinutes: 15, + QuizQuestions: []AcademyQuizQuestion{ + {Question: "Was ist ein typisches Merkmal einer Phishing-E-Mail?", Options: []string{"Professionelles Design", "Kuenstliche Dringlichkeit", "Bekannter Absender", "Kurzer Text"}, CorrectOptionIndex: 1, Explanation: "Phishing-Mails erzeugen oft kuenstliche Dringlichkeit.", Order: 1}, + {Question: "Wie lang sollte ein sicheres Passwort mindestens sein?", Options: []string{"6 Zeichen", "8 Zeichen", "10 Zeichen", "12 Zeichen"}, CorrectOptionIndex: 3, Explanation: "Mindestens 12 Zeichen werden empfohlen.", Order: 2}, + {Question: "Was ist CEO Fraud?", Options: []string{"Hacker-Angriff auf Server", "Gefaelschte Anweisung vom Vorgesetzten", "Virus in E-Mail-Anhang", "DDoS-Attacke"}, CorrectOptionIndex: 1, Explanation: "CEO Fraud ist eine Social-Engineering-Methode mit gefaelschten Anweisungen.", Order: 3}, + }}, + }, + } +} + +func (h *AcademyHandler) mockAILiteracyCourse(req GenerateCourseRequest) AcademyCourse { + return AcademyCourse{ + Title: "AI Literacy - Sicherer Umgang mit KI", + Description: "Grundlagen kuenstlicher Intelligenz, EU AI Act und verantwortungsvoller Einsatz von KI-Werkzeugen im Unternehmen.", + DurationMinutes: 75, + Lessons: []AcademyLesson{ + {Title: "Was ist Kuenstliche Intelligenz?", Type: "text", Order: 1, DurationMinutes: 15, + ContentMarkdown: "# Was ist KI?\n\nKuenstliche Intelligenz (KI) bezeichnet Systeme, die menschenaehnliche kognitive Faehigkeiten zeigen.\n\n## Arten von KI\n- **Machine Learning**: Lernt aus Daten\n- **Deep Learning**: Neuronale Netze\n- **Generative AI**: Erstellt neue Inhalte (Text, Bild)\n- **LLMs**: Large Language Models wie ChatGPT"}, + {Title: "Der EU AI Act", Type: "text", Order: 2, DurationMinutes: 20, + ContentMarkdown: "# EU AI Act\n\n## Risikoklassen\n- **Unakzeptabel**: Social Scoring, Manipulation\n- **Hochrisiko**: Bildung, HR, Kritische Infrastruktur\n- **Begrenzt**: Chatbots, Empfehlungssysteme\n- **Minimal**: Spam-Filter\n\n## Art. 4: AI Literacy Pflicht\nAlle Mitarbeiter, die KI-Systeme nutzen, muessen geschult werden."}, + {Title: "KI sicher im Unternehmen nutzen", Type: "text", Order: 3, DurationMinutes: 20, + ContentMarkdown: "# KI sicher nutzen\n\n## Dos\n- Ergebnisse immer pruefen\n- Keine vertraulichen Daten eingeben\n- Firmenpolicies beachten\n\n## Don'ts\n- Blindes Vertrauen in KI-Ergebnisse\n- Personenbezogene Daten in externe KI-Tools\n- KI-generierte Inhalte ohne Pruefung veroeffentlichen"}, + {Title: "Wissenstest: AI Literacy", Type: "quiz", Order: 4, DurationMinutes: 20, + QuizQuestions: []AcademyQuizQuestion{ + {Question: "Was verlangt Art. 4 des EU AI Acts?", Options: []string{"Verbot aller KI-Systeme", "AI Literacy Schulung fuer KI-Nutzer", "Nur Open-Source KI erlaubt", "KI nur in der IT-Abteilung"}, CorrectOptionIndex: 1, Explanation: "Art. 4 EU AI Act fordert AI Literacy fuer alle Mitarbeiter, die KI-Systeme nutzen.", Order: 1}, + {Question: "Duerfen vertrauliche Firmendaten in externe KI-Tools eingegeben werden?", Options: []string{"Ja, immer", "Nur in ChatGPT", "Nein, grundsaetzlich nicht", "Nur mit VPN"}, CorrectOptionIndex: 2, Explanation: "Vertrauliche Daten duerfen nicht in externe KI-Tools eingegeben werden.", Order: 2}, + }}, + }, + } +} + +func (h *AcademyHandler) mockWhistleblowerCourse(req GenerateCourseRequest) AcademyCourse { + return AcademyCourse{ + Title: "Hinweisgeberschutz (HinSchG)", + Description: "Einführung in das Hinweisgeberschutzgesetz, interne Meldewege und Schutz von Whistleblowern.", + DurationMinutes: 45, + Lessons: []AcademyLesson{ + {Title: "Das Hinweisgeberschutzgesetz", Type: "text", Order: 1, DurationMinutes: 15, + ContentMarkdown: "# Hinweisgeberschutzgesetz (HinSchG)\n\nSeit Juli 2023 muessen Unternehmen ab 50 Mitarbeitern interne Meldestellen einrichten.\n\n## Was ist geschuetzt?\n- Meldungen ueber Rechtsverstoesse\n- Verstoesse gegen EU-Recht\n- Straftaten und Ordnungswidrigkeiten"}, + {Title: "Interne Meldewege", Type: "text", Order: 2, DurationMinutes: 15, + ContentMarkdown: "# Interne Meldewege\n\n## Wie melde ich einen Verstoss?\n1. **Interne Meldestelle** (bevorzugt)\n2. **Externe Meldestelle** (BfJ)\n3. **Offenlegung** (nur als letztes Mittel)\n\n## Schutz fuer Hinweisgeber\n- Kuendigungsschutz\n- Keine Benachteiligung\n- Vertraulichkeit"}, + {Title: "Wissenstest: Hinweisgeberschutz", Type: "quiz", Order: 3, DurationMinutes: 15, + QuizQuestions: []AcademyQuizQuestion{ + {Question: "Ab wie vielen Mitarbeitern muessen Unternehmen eine Meldestelle einrichten?", Options: []string{"10", "25", "50", "250"}, CorrectOptionIndex: 2, Explanation: "Unternehmen ab 50 Beschaeftigten muessen eine interne Meldestelle einrichten.", Order: 1}, + {Question: "Welche Meldung ist NICHT durch das HinSchG geschuetzt?", Options: []string{"Straftaten", "Verstoesse gegen EU-Recht", "Persoenliche Beschwerden ueber Kollegen", "Umweltverstoesse"}, CorrectOptionIndex: 2, Explanation: "Persoenliche Konflikte fallen nicht unter das HinSchG.", Order: 2}, + }}, + }, + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/api/academy_models.go b/admin-v2/ai-compliance-sdk/internal/api/academy_models.go new file mode 100644 index 0000000..908f01d --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/api/academy_models.go @@ -0,0 +1,209 @@ +package api + +// Academy Course models + +// AcademyCourse represents a training course in the Academy module +type AcademyCourse struct { + ID string `json:"id"` + TenantID string `json:"tenantId,omitempty"` + Title string `json:"title"` + Description string `json:"description"` + Category string `json:"category"` + PassingScore int `json:"passingScore"` + DurationMinutes int `json:"durationMinutes"` + RequiredForRoles []string `json:"requiredForRoles"` + Status string `json:"status"` + Lessons []AcademyLesson `json:"lessons"` + CreatedAt string `json:"createdAt"` + UpdatedAt string `json:"updatedAt"` +} + +// AcademyLesson represents a single lesson within a course +type AcademyLesson struct { + ID string `json:"id"` + CourseID string `json:"courseId"` + Title string `json:"title"` + Type string `json:"type"` // video, text, quiz + ContentMarkdown string `json:"contentMarkdown"` + VideoURL string `json:"videoUrl,omitempty"` + AudioURL string `json:"audioUrl,omitempty"` + Order int `json:"order"` + DurationMinutes int `json:"durationMinutes"` + QuizQuestions []AcademyQuizQuestion `json:"quizQuestions,omitempty"` +} + +// AcademyQuizQuestion represents a single quiz question within a lesson +type AcademyQuizQuestion struct { + ID string `json:"id"` + LessonID string `json:"lessonId"` + Question string `json:"question"` + Options []string `json:"options"` + CorrectOptionIndex int `json:"correctOptionIndex"` + Explanation string `json:"explanation"` + Order int `json:"order"` +} + +// AcademyEnrollment represents a user's enrollment in a course +type AcademyEnrollment struct { + ID string `json:"id"` + TenantID string `json:"tenantId,omitempty"` + CourseID string `json:"courseId"` + UserID string `json:"userId"` + UserName string `json:"userName"` + UserEmail string `json:"userEmail"` + Status string `json:"status"` // not_started, in_progress, completed, expired + Progress int `json:"progress"` // 0-100 + StartedAt string `json:"startedAt"` + CompletedAt string `json:"completedAt,omitempty"` + CertificateID string `json:"certificateId,omitempty"` + Deadline string `json:"deadline"` + CreatedAt string `json:"createdAt,omitempty"` + UpdatedAt string `json:"updatedAt,omitempty"` +} + +// AcademyCertificate represents a certificate issued upon course completion +type AcademyCertificate struct { + ID string `json:"id"` + TenantID string `json:"tenantId,omitempty"` + EnrollmentID string `json:"enrollmentId"` + CourseID string `json:"courseId"` + UserID string `json:"userId"` + UserName string `json:"userName"` + CourseName string `json:"courseName"` + Score int `json:"score"` + IssuedAt string `json:"issuedAt"` + ValidUntil string `json:"validUntil"` + PdfURL string `json:"pdfUrl,omitempty"` +} + +// AcademyLessonProgress tracks a user's progress through a single lesson +type AcademyLessonProgress struct { + ID string `json:"id"` + EnrollmentID string `json:"enrollmentId"` + LessonID string `json:"lessonId"` + Completed bool `json:"completed"` + QuizScore *int `json:"quizScore,omitempty"` + CompletedAt string `json:"completedAt,omitempty"` +} + +// AcademyStatistics provides aggregate statistics for the Academy module +type AcademyStatistics struct { + TotalCourses int `json:"totalCourses"` + TotalEnrollments int `json:"totalEnrollments"` + CompletionRate int `json:"completionRate"` + OverdueCount int `json:"overdueCount"` + ByCategory map[string]int `json:"byCategory"` + ByStatus map[string]int `json:"byStatus"` +} + +// Request types + +// CreateCourseRequest is the request body for creating a new course +type CreateCourseRequest struct { + TenantID string `json:"tenantId" binding:"required"` + Title string `json:"title" binding:"required"` + Description string `json:"description"` + Category string `json:"category" binding:"required"` + DurationMinutes int `json:"durationMinutes"` + RequiredForRoles []string `json:"requiredForRoles"` + PassingScore int `json:"passingScore"` + Lessons []CreateLessonRequest `json:"lessons"` +} + +// CreateLessonRequest is the request body for creating a lesson within a course +type CreateLessonRequest struct { + Title string `json:"title" binding:"required"` + Type string `json:"type" binding:"required"` + ContentMarkdown string `json:"contentMarkdown"` + VideoURL string `json:"videoUrl"` + Order int `json:"order"` + DurationMinutes int `json:"durationMinutes"` + QuizQuestions []CreateQuizQuestionRequest `json:"quizQuestions"` +} + +// CreateQuizQuestionRequest is the request body for creating a quiz question +type CreateQuizQuestionRequest struct { + Question string `json:"question" binding:"required"` + Options []string `json:"options" binding:"required"` + CorrectOptionIndex int `json:"correctOptionIndex"` + Explanation string `json:"explanation"` + Order int `json:"order"` +} + +// UpdateCourseRequest is the request body for updating an existing course +type UpdateCourseRequest struct { + Title *string `json:"title"` + Description *string `json:"description"` + Category *string `json:"category"` + DurationMinutes *int `json:"durationMinutes"` + RequiredForRoles []string `json:"requiredForRoles"` + PassingScore *int `json:"passingScore"` +} + +// EnrollUserRequest is the request body for enrolling a user in a course +type EnrollUserRequest struct { + TenantID string `json:"tenantId" binding:"required"` + CourseID string `json:"courseId" binding:"required"` + UserID string `json:"userId" binding:"required"` + UserName string `json:"userName" binding:"required"` + UserEmail string `json:"userEmail" binding:"required"` + Deadline string `json:"deadline" binding:"required"` +} + +// UpdateProgressRequest is the request body for updating enrollment progress +type UpdateProgressRequest struct { + Progress int `json:"progress"` + LessonID string `json:"lessonId"` +} + +// SubmitQuizRequest is the request body for submitting quiz answers +type SubmitQuizRequest struct { + Answers []int `json:"answers" binding:"required"` +} + +// SubmitQuizResponse is the response for a quiz submission +type SubmitQuizResponse struct { + Score int `json:"score"` + Passed bool `json:"passed"` + CorrectAnswers int `json:"correctAnswers"` + TotalQuestions int `json:"totalQuestions"` + Results []QuizQuestionResult `json:"results"` +} + +// QuizQuestionResult represents the result of a single quiz question +type QuizQuestionResult struct { + QuestionID string `json:"questionId"` + Correct bool `json:"correct"` + Explanation string `json:"explanation"` +} + +// GenerateCourseRequest is the request body for AI-generating a course +type GenerateCourseRequest struct { + TenantID string `json:"tenantId" binding:"required"` + Topic string `json:"topic" binding:"required"` + Category string `json:"category" binding:"required"` + TargetGroup string `json:"targetGroup"` + Language string `json:"language"` + UseRAG bool `json:"useRag"` + RAGQuery string `json:"ragQuery"` +} + +// GenerateVideosRequest is the request body for generating lesson videos +type GenerateVideosRequest struct { + TenantID string `json:"tenantId" binding:"required"` +} + +// VideoStatusResponse represents the video generation status for a course +type VideoStatusResponse struct { + CourseID string `json:"courseId"` + Status string `json:"status"` // pending, processing, completed, failed + Lessons []LessonVideoStatus `json:"lessons"` +} + +// LessonVideoStatus represents the video generation status for a single lesson +type LessonVideoStatus struct { + LessonID string `json:"lessonId"` + Status string `json:"status"` + VideoURL string `json:"videoUrl,omitempty"` + AudioURL string `json:"audioUrl,omitempty"` +} diff --git a/admin-v2/ai-compliance-sdk/internal/api/generate.go b/admin-v2/ai-compliance-sdk/internal/api/generate.go index 7f7d8c9..bfc7794 100644 --- a/admin-v2/ai-compliance-sdk/internal/api/generate.go +++ b/admin-v2/ai-compliance-sdk/internal/api/generate.go @@ -31,15 +31,15 @@ func (h *GenerateHandler) GenerateDSFA(c *gin.Context) { } // Get RAG context if requested - var ragSources []SearchResult + var ragSources []llm.SearchResult if req.UseRAG && h.ragService != nil { query := req.RAGQuery if query == "" { - query = "DSFA Datenschutz-Folgenabschätzung Anforderungen" + query = "DSFA Datenschutz-Folgenabschaetzung Anforderungen" } results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "regulation:DSGVO") for _, r := range results { - ragSources = append(ragSources, SearchResult{ + ragSources = append(ragSources, llm.SearchResult{ ID: r.ID, Content: r.Content, Source: r.Source, @@ -62,7 +62,7 @@ func (h *GenerateHandler) GenerateDSFA(c *gin.Context) { GeneratedAt: now(), Model: h.llmService.GetModel(), TokensUsed: tokensUsed, - RAGSources: ragSources, + RAGSources: convertLLMSources(ragSources), Confidence: 0.85, }) } @@ -76,15 +76,15 @@ func (h *GenerateHandler) GenerateTOM(c *gin.Context) { } // Get RAG context if requested - var ragSources []SearchResult + var llmRagSources []llm.SearchResult if req.UseRAG && h.ragService != nil { query := req.RAGQuery if query == "" { - query = "technische organisatorische Maßnahmen TOM Datenschutz" + query = "technische organisatorische Massnahmen TOM Datenschutz" } results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "") for _, r := range results { - ragSources = append(ragSources, SearchResult{ + llmRagSources = append(llmRagSources, llm.SearchResult{ ID: r.ID, Content: r.Content, Source: r.Source, @@ -95,7 +95,7 @@ func (h *GenerateHandler) GenerateTOM(c *gin.Context) { } // Generate TOM content - content, tokensUsed, err := h.llmService.GenerateTOM(c.Request.Context(), req.Context, ragSources) + content, tokensUsed, err := h.llmService.GenerateTOM(c.Request.Context(), req.Context, llmRagSources) if err != nil { content = h.getMockTOM(req.Context) tokensUsed = 0 @@ -106,7 +106,7 @@ func (h *GenerateHandler) GenerateTOM(c *gin.Context) { GeneratedAt: now(), Model: h.llmService.GetModel(), TokensUsed: tokensUsed, - RAGSources: ragSources, + RAGSources: convertLLMSources(llmRagSources), Confidence: 0.82, }) } @@ -120,7 +120,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) { } // Get RAG context if requested - var ragSources []SearchResult + var llmRagSources []llm.SearchResult if req.UseRAG && h.ragService != nil { query := req.RAGQuery if query == "" { @@ -128,7 +128,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) { } results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "regulation:DSGVO") for _, r := range results { - ragSources = append(ragSources, SearchResult{ + llmRagSources = append(llmRagSources, llm.SearchResult{ ID: r.ID, Content: r.Content, Source: r.Source, @@ -139,7 +139,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) { } // Generate VVT content - content, tokensUsed, err := h.llmService.GenerateVVT(c.Request.Context(), req.Context, ragSources) + content, tokensUsed, err := h.llmService.GenerateVVT(c.Request.Context(), req.Context, llmRagSources) if err != nil { content = h.getMockVVT(req.Context) tokensUsed = 0 @@ -150,7 +150,7 @@ func (h *GenerateHandler) GenerateVVT(c *gin.Context) { GeneratedAt: now(), Model: h.llmService.GetModel(), TokensUsed: tokensUsed, - RAGSources: ragSources, + RAGSources: convertLLMSources(llmRagSources), Confidence: 0.88, }) } @@ -164,7 +164,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) { } // Get RAG context if requested - var ragSources []SearchResult + var llmRagSources []llm.SearchResult if req.UseRAG && h.ragService != nil { query := req.RAGQuery if query == "" { @@ -172,7 +172,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) { } results, _ := h.ragService.Search(c.Request.Context(), query, 5, "legal_corpus", "") for _, r := range results { - ragSources = append(ragSources, SearchResult{ + llmRagSources = append(llmRagSources, llm.SearchResult{ ID: r.ID, Content: r.Content, Source: r.Source, @@ -183,7 +183,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) { } // Generate Gutachten content - content, tokensUsed, err := h.llmService.GenerateGutachten(c.Request.Context(), req.Context, ragSources) + content, tokensUsed, err := h.llmService.GenerateGutachten(c.Request.Context(), req.Context, llmRagSources) if err != nil { content = h.getMockGutachten(req.Context) tokensUsed = 0 @@ -194,7 +194,7 @@ func (h *GenerateHandler) GenerateGutachten(c *gin.Context) { GeneratedAt: now(), Model: h.llmService.GetModel(), TokensUsed: tokensUsed, - RAGSources: ragSources, + RAGSources: convertLLMSources(llmRagSources), Confidence: 0.80, }) } @@ -363,3 +363,21 @@ Das geprüfte KI-System erfüllt die wesentlichen Anforderungen der DSGVO und de Erstellt am: ${new Date().toISOString()} ` } + +// convertLLMSources converts llm.SearchResult to api.SearchResult for the response +func convertLLMSources(sources []llm.SearchResult) []SearchResult { + if sources == nil { + return nil + } + result := make([]SearchResult, len(sources)) + for i, s := range sources { + result[i] = SearchResult{ + ID: s.ID, + Content: s.Content, + Source: s.Source, + Score: s.Score, + Metadata: s.Metadata, + } + } + return result +} diff --git a/admin-v2/ai-compliance-sdk/internal/db/academy_store.go b/admin-v2/ai-compliance-sdk/internal/db/academy_store.go new file mode 100644 index 0000000..ee09fda --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/db/academy_store.go @@ -0,0 +1,681 @@ +package db + +import ( + "fmt" + "sort" + "strings" + "sync" + "time" +) + +// AcademyMemStore provides in-memory storage for academy data +type AcademyMemStore struct { + mu sync.RWMutex + courses map[string]*AcademyCourseRow + lessons map[string]*AcademyLessonRow + quizQuestions map[string]*AcademyQuizQuestionRow + enrollments map[string]*AcademyEnrollmentRow + certificates map[string]*AcademyCertificateRow + lessonProgress map[string]*AcademyLessonProgressRow +} + +// Row types matching the DB schema +type AcademyCourseRow struct { + ID string + TenantID string + Title string + Description string + Category string + PassingScore int + DurationMinutes int + RequiredForRoles []string + Status string + CreatedAt time.Time + UpdatedAt time.Time +} + +type AcademyLessonRow struct { + ID string + CourseID string + Title string + Type string + ContentMarkdown string + VideoURL string + AudioURL string + SortOrder int + DurationMinutes int + CreatedAt time.Time + UpdatedAt time.Time +} + +type AcademyQuizQuestionRow struct { + ID string + LessonID string + Question string + Options []string + CorrectOptionIndex int + Explanation string + SortOrder int + CreatedAt time.Time +} + +type AcademyEnrollmentRow struct { + ID string + TenantID string + CourseID string + UserID string + UserName string + UserEmail string + Status string + Progress int + StartedAt time.Time + CompletedAt *time.Time + CertificateID string + Deadline time.Time + CreatedAt time.Time + UpdatedAt time.Time +} + +type AcademyCertificateRow struct { + ID string + TenantID string + EnrollmentID string + CourseID string + UserID string + UserName string + CourseName string + Score int + IssuedAt time.Time + ValidUntil time.Time + PdfURL string +} + +type AcademyLessonProgressRow struct { + ID string + EnrollmentID string + LessonID string + Completed bool + QuizScore *int + CompletedAt *time.Time +} + +type AcademyStatisticsRow struct { + TotalCourses int + TotalEnrollments int + CompletionRate float64 + OverdueCount int + ByCategory map[string]int + ByStatus map[string]int +} + +func NewAcademyMemStore() *AcademyMemStore { + return &AcademyMemStore{ + courses: make(map[string]*AcademyCourseRow), + lessons: make(map[string]*AcademyLessonRow), + quizQuestions: make(map[string]*AcademyQuizQuestionRow), + enrollments: make(map[string]*AcademyEnrollmentRow), + certificates: make(map[string]*AcademyCertificateRow), + lessonProgress: make(map[string]*AcademyLessonProgressRow), + } +} + +// generateID creates a simple unique ID +func generateID() string { + return fmt.Sprintf("%d", time.Now().UnixNano()) +} + +// --------------------------------------------------------------------------- +// Course CRUD +// --------------------------------------------------------------------------- + +// ListCourses returns all courses for a tenant, sorted by UpdatedAt DESC. +func (s *AcademyMemStore) ListCourses(tenantID string) []*AcademyCourseRow { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*AcademyCourseRow + for _, c := range s.courses { + if c.TenantID == tenantID { + result = append(result, c) + } + } + + sort.Slice(result, func(i, j int) bool { + return result[i].UpdatedAt.After(result[j].UpdatedAt) + }) + + return result +} + +// GetCourse retrieves a single course by ID. +func (s *AcademyMemStore) GetCourse(id string) (*AcademyCourseRow, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + c, ok := s.courses[id] + if !ok { + return nil, fmt.Errorf("course not found: %s", id) + } + return c, nil +} + +// CreateCourse inserts a new course with auto-generated ID and timestamps. +func (s *AcademyMemStore) CreateCourse(row *AcademyCourseRow) *AcademyCourseRow { + s.mu.Lock() + defer s.mu.Unlock() + + now := time.Now() + row.ID = generateID() + row.CreatedAt = now + row.UpdatedAt = now + s.courses[row.ID] = row + return row +} + +// UpdateCourse partially updates a course. Supported keys: Title, Description, +// Category, PassingScore, DurationMinutes, RequiredForRoles, Status. +func (s *AcademyMemStore) UpdateCourse(id string, updates map[string]interface{}) (*AcademyCourseRow, error) { + s.mu.Lock() + defer s.mu.Unlock() + + c, ok := s.courses[id] + if !ok { + return nil, fmt.Errorf("course not found: %s", id) + } + + for k, v := range updates { + switch strings.ToLower(k) { + case "title": + if val, ok := v.(string); ok { + c.Title = val + } + case "description": + if val, ok := v.(string); ok { + c.Description = val + } + case "category": + if val, ok := v.(string); ok { + c.Category = val + } + case "passingscore", "passing_score": + switch val := v.(type) { + case int: + c.PassingScore = val + case float64: + c.PassingScore = int(val) + } + case "durationminutes", "duration_minutes": + switch val := v.(type) { + case int: + c.DurationMinutes = val + case float64: + c.DurationMinutes = int(val) + } + case "requiredforroles", "required_for_roles": + if val, ok := v.([]string); ok { + c.RequiredForRoles = val + } + case "status": + if val, ok := v.(string); ok { + c.Status = val + } + } + } + + c.UpdatedAt = time.Now() + return c, nil +} + +// DeleteCourse removes a course and all related lessons, quiz questions, +// enrollments, certificates, and lesson progress. +func (s *AcademyMemStore) DeleteCourse(id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.courses[id]; !ok { + return fmt.Errorf("course not found: %s", id) + } + + // Collect lesson IDs for this course + lessonIDs := make(map[string]bool) + for lid, l := range s.lessons { + if l.CourseID == id { + lessonIDs[lid] = true + } + } + + // Delete quiz questions belonging to those lessons + for qid, q := range s.quizQuestions { + if lessonIDs[q.LessonID] { + delete(s.quizQuestions, qid) + } + } + + // Delete lessons + for lid := range lessonIDs { + delete(s.lessons, lid) + } + + // Collect enrollment IDs for this course + enrollmentIDs := make(map[string]bool) + for eid, e := range s.enrollments { + if e.CourseID == id { + enrollmentIDs[eid] = true + } + } + + // Delete lesson progress belonging to those enrollments + for pid, p := range s.lessonProgress { + if enrollmentIDs[p.EnrollmentID] { + delete(s.lessonProgress, pid) + } + } + + // Delete certificates belonging to those enrollments + for cid, cert := range s.certificates { + if cert.CourseID == id { + delete(s.certificates, cid) + } + } + + // Delete enrollments + for eid := range enrollmentIDs { + delete(s.enrollments, eid) + } + + // Delete the course itself + delete(s.courses, id) + + return nil +} + +// --------------------------------------------------------------------------- +// Lesson CRUD +// --------------------------------------------------------------------------- + +// ListLessons returns all lessons for a course, sorted by SortOrder ASC. +func (s *AcademyMemStore) ListLessons(courseID string) []*AcademyLessonRow { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*AcademyLessonRow + for _, l := range s.lessons { + if l.CourseID == courseID { + result = append(result, l) + } + } + + sort.Slice(result, func(i, j int) bool { + return result[i].SortOrder < result[j].SortOrder + }) + + return result +} + +// GetLesson retrieves a single lesson by ID. +func (s *AcademyMemStore) GetLesson(id string) (*AcademyLessonRow, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + l, ok := s.lessons[id] + if !ok { + return nil, fmt.Errorf("lesson not found: %s", id) + } + return l, nil +} + +// CreateLesson inserts a new lesson with auto-generated ID and timestamps. +func (s *AcademyMemStore) CreateLesson(row *AcademyLessonRow) *AcademyLessonRow { + s.mu.Lock() + defer s.mu.Unlock() + + now := time.Now() + row.ID = generateID() + row.CreatedAt = now + row.UpdatedAt = now + s.lessons[row.ID] = row + return row +} + +// UpdateLesson partially updates a lesson. Supported keys: Title, Type, +// ContentMarkdown, VideoURL, AudioURL, SortOrder, DurationMinutes. +func (s *AcademyMemStore) UpdateLesson(id string, updates map[string]interface{}) (*AcademyLessonRow, error) { + s.mu.Lock() + defer s.mu.Unlock() + + l, ok := s.lessons[id] + if !ok { + return nil, fmt.Errorf("lesson not found: %s", id) + } + + for k, v := range updates { + switch strings.ToLower(k) { + case "title": + if val, ok := v.(string); ok { + l.Title = val + } + case "type": + if val, ok := v.(string); ok { + l.Type = val + } + case "contentmarkdown", "content_markdown": + if val, ok := v.(string); ok { + l.ContentMarkdown = val + } + case "videourl", "video_url": + if val, ok := v.(string); ok { + l.VideoURL = val + } + case "audiourl", "audio_url": + if val, ok := v.(string); ok { + l.AudioURL = val + } + case "sortorder", "sort_order": + switch val := v.(type) { + case int: + l.SortOrder = val + case float64: + l.SortOrder = int(val) + } + case "durationminutes", "duration_minutes": + switch val := v.(type) { + case int: + l.DurationMinutes = val + case float64: + l.DurationMinutes = int(val) + } + } + } + + l.UpdatedAt = time.Now() + return l, nil +} + +// DeleteLesson removes a lesson and its quiz questions. +func (s *AcademyMemStore) DeleteLesson(id string) error { + s.mu.Lock() + defer s.mu.Unlock() + + if _, ok := s.lessons[id]; !ok { + return fmt.Errorf("lesson not found: %s", id) + } + + // Delete quiz questions belonging to this lesson + for qid, q := range s.quizQuestions { + if q.LessonID == id { + delete(s.quizQuestions, qid) + } + } + + delete(s.lessons, id) + return nil +} + +// --------------------------------------------------------------------------- +// Quiz Questions +// --------------------------------------------------------------------------- + +// ListQuizQuestions returns all quiz questions for a lesson, sorted by SortOrder ASC. +func (s *AcademyMemStore) ListQuizQuestions(lessonID string) []*AcademyQuizQuestionRow { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*AcademyQuizQuestionRow + for _, q := range s.quizQuestions { + if q.LessonID == lessonID { + result = append(result, q) + } + } + + sort.Slice(result, func(i, j int) bool { + return result[i].SortOrder < result[j].SortOrder + }) + + return result +} + +// CreateQuizQuestion inserts a new quiz question with auto-generated ID and timestamp. +func (s *AcademyMemStore) CreateQuizQuestion(row *AcademyQuizQuestionRow) *AcademyQuizQuestionRow { + s.mu.Lock() + defer s.mu.Unlock() + + row.ID = generateID() + row.CreatedAt = time.Now() + s.quizQuestions[row.ID] = row + return row +} + +// --------------------------------------------------------------------------- +// Enrollments +// --------------------------------------------------------------------------- + +// ListEnrollments returns enrollments filtered by tenantID and optionally by courseID. +// If courseID is empty, all enrollments for the tenant are returned. +func (s *AcademyMemStore) ListEnrollments(tenantID string, courseID string) []*AcademyEnrollmentRow { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*AcademyEnrollmentRow + for _, e := range s.enrollments { + if e.TenantID != tenantID { + continue + } + if courseID != "" && e.CourseID != courseID { + continue + } + result = append(result, e) + } + + sort.Slice(result, func(i, j int) bool { + return result[i].UpdatedAt.After(result[j].UpdatedAt) + }) + + return result +} + +// GetEnrollment retrieves a single enrollment by ID. +func (s *AcademyMemStore) GetEnrollment(id string) (*AcademyEnrollmentRow, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + e, ok := s.enrollments[id] + if !ok { + return nil, fmt.Errorf("enrollment not found: %s", id) + } + return e, nil +} + +// CreateEnrollment inserts a new enrollment with auto-generated ID and timestamps. +func (s *AcademyMemStore) CreateEnrollment(row *AcademyEnrollmentRow) *AcademyEnrollmentRow { + s.mu.Lock() + defer s.mu.Unlock() + + now := time.Now() + row.ID = generateID() + row.CreatedAt = now + row.UpdatedAt = now + if row.StartedAt.IsZero() { + row.StartedAt = now + } + s.enrollments[row.ID] = row + return row +} + +// UpdateEnrollment partially updates an enrollment. Supported keys: Status, +// Progress, CompletedAt, CertificateID, Deadline. +func (s *AcademyMemStore) UpdateEnrollment(id string, updates map[string]interface{}) (*AcademyEnrollmentRow, error) { + s.mu.Lock() + defer s.mu.Unlock() + + e, ok := s.enrollments[id] + if !ok { + return nil, fmt.Errorf("enrollment not found: %s", id) + } + + for k, v := range updates { + switch strings.ToLower(k) { + case "status": + if val, ok := v.(string); ok { + e.Status = val + } + case "progress": + switch val := v.(type) { + case int: + e.Progress = val + case float64: + e.Progress = int(val) + } + case "completedat", "completed_at": + if val, ok := v.(*time.Time); ok { + e.CompletedAt = val + } else if val, ok := v.(time.Time); ok { + e.CompletedAt = &val + } + case "certificateid", "certificate_id": + if val, ok := v.(string); ok { + e.CertificateID = val + } + case "deadline": + if val, ok := v.(time.Time); ok { + e.Deadline = val + } + } + } + + e.UpdatedAt = time.Now() + return e, nil +} + +// --------------------------------------------------------------------------- +// Certificates +// --------------------------------------------------------------------------- + +// GetCertificate retrieves a certificate by ID. +func (s *AcademyMemStore) GetCertificate(id string) (*AcademyCertificateRow, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + cert, ok := s.certificates[id] + if !ok { + return nil, fmt.Errorf("certificate not found: %s", id) + } + return cert, nil +} + +// GetCertificateByEnrollment retrieves a certificate by enrollment ID. +func (s *AcademyMemStore) GetCertificateByEnrollment(enrollmentID string) (*AcademyCertificateRow, error) { + s.mu.RLock() + defer s.mu.RUnlock() + + for _, cert := range s.certificates { + if cert.EnrollmentID == enrollmentID { + return cert, nil + } + } + return nil, fmt.Errorf("certificate not found for enrollment: %s", enrollmentID) +} + +// CreateCertificate inserts a new certificate with auto-generated ID. +func (s *AcademyMemStore) CreateCertificate(row *AcademyCertificateRow) *AcademyCertificateRow { + s.mu.Lock() + defer s.mu.Unlock() + + row.ID = generateID() + if row.IssuedAt.IsZero() { + row.IssuedAt = time.Now() + } + s.certificates[row.ID] = row + return row +} + +// --------------------------------------------------------------------------- +// Lesson Progress +// --------------------------------------------------------------------------- + +// ListLessonProgress returns all progress entries for an enrollment. +func (s *AcademyMemStore) ListLessonProgress(enrollmentID string) []*AcademyLessonProgressRow { + s.mu.RLock() + defer s.mu.RUnlock() + + var result []*AcademyLessonProgressRow + for _, p := range s.lessonProgress { + if p.EnrollmentID == enrollmentID { + result = append(result, p) + } + } + return result +} + +// UpsertLessonProgress inserts or updates a lesson progress entry. +// Matching is done by EnrollmentID + LessonID composite key. +func (s *AcademyMemStore) UpsertLessonProgress(row *AcademyLessonProgressRow) *AcademyLessonProgressRow { + s.mu.Lock() + defer s.mu.Unlock() + + // Look for existing entry with same enrollment_id + lesson_id + for _, p := range s.lessonProgress { + if p.EnrollmentID == row.EnrollmentID && p.LessonID == row.LessonID { + p.Completed = row.Completed + p.QuizScore = row.QuizScore + p.CompletedAt = row.CompletedAt + return p + } + } + + // Insert new entry + row.ID = generateID() + s.lessonProgress[row.ID] = row + return row +} + +// --------------------------------------------------------------------------- +// Statistics +// --------------------------------------------------------------------------- + +// GetStatistics computes aggregate statistics for a tenant. +func (s *AcademyMemStore) GetStatistics(tenantID string) *AcademyStatisticsRow { + s.mu.RLock() + defer s.mu.RUnlock() + + stats := &AcademyStatisticsRow{ + ByCategory: make(map[string]int), + ByStatus: make(map[string]int), + } + + // Count courses by category + for _, c := range s.courses { + if c.TenantID != tenantID { + continue + } + stats.TotalCourses++ + if c.Category != "" { + stats.ByCategory[c.Category]++ + } + } + + // Count enrollments and compute completion rate + var completedCount int + now := time.Now() + for _, e := range s.enrollments { + if e.TenantID != tenantID { + continue + } + stats.TotalEnrollments++ + stats.ByStatus[e.Status]++ + + if e.Status == "completed" { + completedCount++ + } + + // Overdue: not completed and past deadline + if e.Status != "completed" && !e.Deadline.IsZero() && now.After(e.Deadline) { + stats.OverdueCount++ + } + } + + if stats.TotalEnrollments > 0 { + stats.CompletionRate = float64(completedCount) / float64(stats.TotalEnrollments) * 100.0 + } + + return stats +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/engine.go b/admin-v2/ai-compliance-sdk/internal/gci/engine.go new file mode 100644 index 0000000..1599f8b --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/engine.go @@ -0,0 +1,371 @@ +package gci + +import ( + "fmt" + "math" + "time" +) + +// Engine calculates the GCI score +type Engine struct{} + +// NewEngine creates a new GCI calculation engine +func NewEngine() *Engine { + return &Engine{} +} + +// Calculate computes the full GCI result for a tenant +func (e *Engine) Calculate(tenantID string, profileID string) *GCIResult { + now := time.Now() + profile := GetProfile(profileID) + auditTrail := []AuditEntry{} + + // Step 1: Get module data (mock for now) + modules := MockModuleData(tenantID) + certDates := MockCertificateData() + + // Step 2: Calculate Level 1 - Module Scores with validity + for i := range modules { + m := &modules[i] + if m.Assigned > 0 { + m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0 + } + // Apply validity factor + if validUntil, ok := certDates[m.ModuleID]; ok { + m.ValidityFactor = CalculateValidityFactor(validUntil, now) + } else { + m.ValidityFactor = 1.0 // No certificate tracking = assume valid + } + m.FinalScore = m.RawScore * m.ValidityFactor + + if m.ValidityFactor < 1.0 { + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "validity_decay", + Description: fmt.Sprintf("Modul '%s': Gueltigkeitsfaktor %.2f (Zertifikat laeuft ab/abgelaufen)", m.ModuleName, m.ValidityFactor), + Value: m.ValidityFactor, + Impact: "negative", + }) + } + } + + // Step 3: Calculate Level 2 - Risk-Weighted Scores per area + areaModules := map[string][]ModuleScore{ + "dsgvo": {}, + "nis2": {}, + "iso27001": {}, + "ai_act": {}, + } + for _, m := range modules { + if _, ok := areaModules[m.Category]; ok { + areaModules[m.Category] = append(areaModules[m.Category], m) + } + } + + level2Areas := []RiskWeightedScore{} + areaNames := map[string]string{ + "dsgvo": "DSGVO", + "nis2": "NIS2", + "iso27001": "ISO 27001", + "ai_act": "EU AI Act", + } + + for areaID, mods := range areaModules { + rws := RiskWeightedScore{ + AreaID: areaID, + AreaName: areaNames[areaID], + Modules: mods, + } + for _, m := range mods { + rws.WeightedSum += m.FinalScore * m.RiskWeight + rws.TotalWeight += m.RiskWeight + } + if rws.TotalWeight > 0 { + rws.AreaScore = rws.WeightedSum / rws.TotalWeight + } + level2Areas = append(level2Areas, rws) + } + + // Step 4: Calculate Level 3 - Regulation Area Scores + areaScores := []RegulationAreaScore{} + for _, rws := range level2Areas { + weight := profile.Weights[rws.AreaID] + completedCount := 0 + for _, m := range rws.Modules { + if m.Completed >= m.Assigned && m.Assigned > 0 { + completedCount++ + } + } + ras := RegulationAreaScore{ + RegulationID: rws.AreaID, + RegulationName: rws.AreaName, + Score: math.Round(rws.AreaScore*100) / 100, + Weight: weight, + WeightedScore: rws.AreaScore * weight, + ModuleCount: len(rws.Modules), + CompletedCount: completedCount, + } + areaScores = append(areaScores, ras) + + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "area_score", + Description: fmt.Sprintf("Bereich '%s': Score %.1f, Gewicht %.0f%%", rws.AreaName, rws.AreaScore, weight*100), + Value: rws.AreaScore, + Impact: "neutral", + }) + } + + // Step 5: Calculate raw GCI + rawGCI := 0.0 + totalWeight := 0.0 + for _, ras := range areaScores { + rawGCI += ras.WeightedScore + totalWeight += ras.Weight + } + if totalWeight > 0 { + rawGCI = rawGCI / totalWeight + } + + // Step 6: Apply Criticality Multiplier + criticalityMult := calculateCriticalityMultiplier(modules) + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "criticality_multiplier", + Description: fmt.Sprintf("Kritikalitaetsmultiplikator: %.3f", criticalityMult), + Value: criticalityMult, + Impact: func() string { + if criticalityMult < 1.0 { + return "negative" + } + return "neutral" + }(), + }) + + // Step 7: Apply Incident Adjustment + openInc, critInc := MockIncidentData() + incidentAdj := calculateIncidentAdjustment(openInc, critInc) + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "incident_adjustment", + Description: fmt.Sprintf("Vorfallsanpassung: %.3f (%d offen, %d kritisch)", incidentAdj, openInc, critInc), + Value: incidentAdj, + Impact: "negative", + }) + + // Step 8: Final GCI + finalGCI := rawGCI * criticalityMult * incidentAdj + finalGCI = math.Max(0, math.Min(100, math.Round(finalGCI*10)/10)) + + // Step 9: Determine Maturity Level + maturity := determineMaturityLevel(finalGCI) + + auditTrail = append(auditTrail, AuditEntry{ + Timestamp: now, + Factor: "final_gci", + Description: fmt.Sprintf("GCI-Endergebnis: %.1f → Reifegrad: %s", finalGCI, MaturityLabels[maturity]), + Value: finalGCI, + Impact: "neutral", + }) + + return &GCIResult{ + TenantID: tenantID, + GCIScore: finalGCI, + MaturityLevel: maturity, + MaturityLabel: MaturityLabels[maturity], + CalculatedAt: now, + Profile: profileID, + AreaScores: areaScores, + CriticalityMult: criticalityMult, + IncidentAdj: incidentAdj, + AuditTrail: auditTrail, + } +} + +// CalculateBreakdown returns the full 4-level breakdown +func (e *Engine) CalculateBreakdown(tenantID string, profileID string) *GCIBreakdown { + result := e.Calculate(tenantID, profileID) + modules := MockModuleData(tenantID) + certDates := MockCertificateData() + now := time.Now() + + // Recalculate module scores for the breakdown + for i := range modules { + m := &modules[i] + if m.Assigned > 0 { + m.RawScore = float64(m.Completed) / float64(m.Assigned) * 100.0 + } + if validUntil, ok := certDates[m.ModuleID]; ok { + m.ValidityFactor = CalculateValidityFactor(validUntil, now) + } else { + m.ValidityFactor = 1.0 + } + m.FinalScore = m.RawScore * m.ValidityFactor + } + + // Build Level 2 areas + areaModules := map[string][]ModuleScore{} + for _, m := range modules { + areaModules[m.Category] = append(areaModules[m.Category], m) + } + + areaNames := map[string]string{"dsgvo": "DSGVO", "nis2": "NIS2", "iso27001": "ISO 27001", "ai_act": "EU AI Act"} + level2 := []RiskWeightedScore{} + for areaID, mods := range areaModules { + rws := RiskWeightedScore{AreaID: areaID, AreaName: areaNames[areaID], Modules: mods} + for _, m := range mods { + rws.WeightedSum += m.FinalScore * m.RiskWeight + rws.TotalWeight += m.RiskWeight + } + if rws.TotalWeight > 0 { + rws.AreaScore = rws.WeightedSum / rws.TotalWeight + } + level2 = append(level2, rws) + } + + return &GCIBreakdown{ + GCIResult: *result, + Level1Modules: modules, + Level2Areas: level2, + } +} + +// GetHistory returns historical GCI snapshots +func (e *Engine) GetHistory(tenantID string) []GCISnapshot { + // Add current score to history + result := e.Calculate(tenantID, "default") + history := MockGCIHistory(tenantID) + current := GCISnapshot{ + TenantID: tenantID, + Score: result.GCIScore, + MaturityLevel: result.MaturityLevel, + AreaScores: make(map[string]float64), + CalculatedAt: result.CalculatedAt, + } + for _, as := range result.AreaScores { + current.AreaScores[as.RegulationID] = as.Score + } + history = append(history, current) + return history +} + +// GetMatrix returns the compliance matrix (roles x regulations) +func (e *Engine) GetMatrix(tenantID string) []ComplianceMatrixEntry { + modules := MockModuleData(tenantID) + + roles := []struct { + ID string + Name string + }{ + {"management", "Geschaeftsfuehrung"}, + {"it_security", "IT-Sicherheit / CISO"}, + {"data_protection", "Datenschutz / DSB"}, + {"hr", "Personalwesen"}, + {"general", "Allgemeine Mitarbeiter"}, + } + + // Define which modules are relevant per role + roleModules := map[string][]string{ + "management": {"dsgvo-grundlagen", "nis2-management", "ai-governance", "iso-isms"}, + "it_security": {"nis2-risikomanagement", "nis2-incident-response", "iso-zugangssteuerung", "iso-kryptografie", "ai-hochrisiko"}, + "data_protection": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "dsgvo-tom", "dsgvo-dsfa", "dsgvo-auftragsverarbeitung"}, + "hr": {"dsgvo-grundlagen", "dsgvo-betroffenenrechte", "nis2-management"}, + "general": {"dsgvo-grundlagen", "nis2-risikomanagement", "ai-risikokategorien", "ai-transparenz"}, + } + + moduleMap := map[string]ModuleScore{} + for _, m := range modules { + moduleMap[m.ModuleID] = m + } + + entries := []ComplianceMatrixEntry{} + for _, role := range roles { + entry := ComplianceMatrixEntry{ + Role: role.ID, + RoleName: role.Name, + Regulations: map[string]float64{}, + } + + regScores := map[string][]float64{} + requiredModuleIDs := roleModules[role.ID] + entry.RequiredModules = len(requiredModuleIDs) + + for _, modID := range requiredModuleIDs { + if m, ok := moduleMap[modID]; ok { + score := 0.0 + if m.Assigned > 0 { + score = float64(m.Completed) / float64(m.Assigned) * 100 + } + regScores[m.Category] = append(regScores[m.Category], score) + if m.Completed >= m.Assigned && m.Assigned > 0 { + entry.CompletedModules++ + } + } + } + + totalScore := 0.0 + count := 0 + for reg, scores := range regScores { + sum := 0.0 + for _, s := range scores { + sum += s + } + avg := sum / float64(len(scores)) + entry.Regulations[reg] = math.Round(avg*10) / 10 + totalScore += avg + count++ + } + if count > 0 { + entry.OverallScore = math.Round(totalScore/float64(count)*10) / 10 + } + + entries = append(entries, entry) + } + + return entries +} + +// Helper functions + +func calculateCriticalityMultiplier(modules []ModuleScore) float64 { + criticalModules := 0 + criticalLow := 0 + for _, m := range modules { + if m.RiskWeight >= 2.5 { + criticalModules++ + if m.FinalScore < 50 { + criticalLow++ + } + } + } + if criticalModules == 0 { + return 1.0 + } + // Reduce score if critical modules have low completion + ratio := float64(criticalLow) / float64(criticalModules) + return 1.0 - (ratio * 0.15) // max 15% reduction +} + +func calculateIncidentAdjustment(openIncidents, criticalIncidents int) float64 { + adj := 1.0 + // Each open incident reduces by 1% + adj -= float64(openIncidents) * 0.01 + // Each critical incident reduces by additional 3% + adj -= float64(criticalIncidents) * 0.03 + return math.Max(0.8, adj) // minimum 80% (max 20% reduction) +} + +func determineMaturityLevel(score float64) string { + switch { + case score >= 90: + return MaturityOptimized + case score >= 75: + return MaturityManaged + case score >= 60: + return MaturityDefined + case score >= 40: + return MaturityReactive + default: + return MaturityHighRisk + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go b/admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go new file mode 100644 index 0000000..9032f45 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/iso_gap_analysis.go @@ -0,0 +1,188 @@ +package gci + +import "math" + +// ISOGapAnalysis represents the complete ISO 27001 gap analysis +type ISOGapAnalysis struct { + TenantID string `json:"tenant_id"` + TotalControls int `json:"total_controls"` + CoveredFull int `json:"covered_full"` + CoveredPartial int `json:"covered_partial"` + NotCovered int `json:"not_covered"` + CoveragePercent float64 `json:"coverage_percent"` + CategorySummaries []ISOCategorySummary `json:"category_summaries"` + ControlDetails []ISOControlDetail `json:"control_details"` + Gaps []ISOGap `json:"gaps"` +} + +// ISOControlDetail shows coverage status for a single control +type ISOControlDetail struct { + Control ISOControl `json:"control"` + CoverageLevel string `json:"coverage_level"` // full, partial, none + CoveredBy []string `json:"covered_by"` // module IDs + Score float64 `json:"score"` // 0-100 +} + +// ISOGap represents an identified gap in ISO coverage +type ISOGap struct { + ControlID string `json:"control_id"` + ControlName string `json:"control_name"` + Category string `json:"category"` + Priority string `json:"priority"` // high, medium, low + Recommendation string `json:"recommendation"` +} + +// CalculateISOGapAnalysis performs the ISO 27001 gap analysis +func CalculateISOGapAnalysis(tenantID string) *ISOGapAnalysis { + modules := MockModuleData(tenantID) + moduleMap := map[string]ModuleScore{} + for _, m := range modules { + moduleMap[m.ModuleID] = m + } + + // Build reverse mapping: control -> modules covering it + controlCoverage := map[string][]string{} + controlCoverageLevel := map[string]string{} + for _, mapping := range DefaultISOModuleMappings { + for _, controlID := range mapping.ISOControls { + controlCoverage[controlID] = append(controlCoverage[controlID], mapping.ModuleID) + // Use the highest coverage level + existingLevel := controlCoverageLevel[controlID] + if mapping.CoverageLevel == "full" || existingLevel == "" { + controlCoverageLevel[controlID] = mapping.CoverageLevel + } + } + } + + // Analyze each control + details := []ISOControlDetail{} + gaps := []ISOGap{} + coveredFull := 0 + coveredPartial := 0 + notCovered := 0 + + categoryCounts := map[string]*ISOCategorySummary{ + "A.5": {CategoryID: "A.5", CategoryName: "Organisatorische Massnahmen"}, + "A.6": {CategoryID: "A.6", CategoryName: "Personelle Massnahmen"}, + "A.7": {CategoryID: "A.7", CategoryName: "Physische Massnahmen"}, + "A.8": {CategoryID: "A.8", CategoryName: "Technologische Massnahmen"}, + } + + for _, control := range ISOControls { + coveredBy := controlCoverage[control.ID] + level := controlCoverageLevel[control.ID] + + if len(coveredBy) == 0 { + level = "none" + } + + // Calculate score based on module completion + score := 0.0 + if len(coveredBy) > 0 { + scoreSum := 0.0 + count := 0 + for _, modID := range coveredBy { + if m, ok := moduleMap[modID]; ok && m.Assigned > 0 { + scoreSum += float64(m.Completed) / float64(m.Assigned) * 100 + count++ + } + } + if count > 0 { + score = scoreSum / float64(count) + } + // Adjust for coverage level + if level == "partial" { + score *= 0.7 // partial coverage reduces effective score + } + } + + detail := ISOControlDetail{ + Control: control, + CoverageLevel: level, + CoveredBy: coveredBy, + Score: math.Round(score*10) / 10, + } + details = append(details, detail) + + // Count by category + cat := categoryCounts[control.CategoryID] + if cat != nil { + cat.TotalControls++ + switch level { + case "full": + coveredFull++ + cat.CoveredFull++ + case "partial": + coveredPartial++ + cat.CoveredPartial++ + default: + notCovered++ + cat.NotCovered++ + // Generate gap recommendation + gap := ISOGap{ + ControlID: control.ID, + ControlName: control.Name, + Category: control.Category, + Priority: determineGapPriority(control), + Recommendation: generateGapRecommendation(control), + } + gaps = append(gaps, gap) + } + } + } + + totalControls := len(ISOControls) + coveragePercent := 0.0 + if totalControls > 0 { + coveragePercent = math.Round(float64(coveredFull+coveredPartial)/float64(totalControls)*100*10) / 10 + } + + summaries := []ISOCategorySummary{} + for _, catID := range []string{"A.5", "A.6", "A.7", "A.8"} { + if cat, ok := categoryCounts[catID]; ok { + summaries = append(summaries, *cat) + } + } + + return &ISOGapAnalysis{ + TenantID: tenantID, + TotalControls: totalControls, + CoveredFull: coveredFull, + CoveredPartial: coveredPartial, + NotCovered: notCovered, + CoveragePercent: coveragePercent, + CategorySummaries: summaries, + ControlDetails: details, + Gaps: gaps, + } +} + +func determineGapPriority(control ISOControl) string { + // High priority for access, incident, and data protection controls + highPriority := map[string]bool{ + "A.5.15": true, "A.5.17": true, "A.5.24": true, "A.5.26": true, + "A.5.34": true, "A.8.2": true, "A.8.5": true, "A.8.7": true, + "A.8.10": true, "A.8.20": true, + } + if highPriority[control.ID] { + return "high" + } + // Medium for organizational and people controls + if control.CategoryID == "A.5" || control.CategoryID == "A.6" { + return "medium" + } + return "low" +} + +func generateGapRecommendation(control ISOControl) string { + recommendations := map[string]string{ + "organizational": "Erstellen Sie eine Richtlinie und weisen Sie Verantwortlichkeiten zu fuer: " + control.Name, + "people": "Implementieren Sie Schulungen und Prozesse fuer: " + control.Name, + "physical": "Definieren Sie physische Sicherheitsmassnahmen fuer: " + control.Name, + "technological": "Implementieren Sie technische Kontrollen fuer: " + control.Name, + } + if rec, ok := recommendations[control.Category]; ok { + return rec + } + return "Massnahmen implementieren fuer: " + control.Name +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go b/admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go new file mode 100644 index 0000000..8f1a8fa --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/iso_mapping.go @@ -0,0 +1,207 @@ +package gci + +// ISOControl represents an ISO 27001:2022 Annex A control +type ISOControl struct { + ID string `json:"id"` // e.g. "A.5.1" + Name string `json:"name"` + Category string `json:"category"` // organizational, people, physical, technological + CategoryID string `json:"category_id"` // A.5, A.6, A.7, A.8 + Description string `json:"description"` +} + +// ISOModuleMapping maps a course/module to ISO controls +type ISOModuleMapping struct { + ModuleID string `json:"module_id"` + ModuleName string `json:"module_name"` + ISOControls []string `json:"iso_controls"` // control IDs + CoverageLevel string `json:"coverage_level"` // full, partial, none +} + +// ISO 27001:2022 Annex A controls (representative selection) +var ISOControls = []ISOControl{ + // A.5 Organizational Controls (37 controls, showing key ones) + {ID: "A.5.1", Name: "Informationssicherheitsrichtlinien", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheitsleitlinie und themenspezifische Richtlinien"}, + {ID: "A.5.2", Name: "Rollen und Verantwortlichkeiten", Category: "organizational", CategoryID: "A.5", Description: "Definition und Zuweisung von Informationssicherheitsrollen"}, + {ID: "A.5.3", Name: "Aufgabentrennung", Category: "organizational", CategoryID: "A.5", Description: "Trennung von konfligierenden Aufgaben und Verantwortlichkeiten"}, + {ID: "A.5.4", Name: "Managementverantwortung", Category: "organizational", CategoryID: "A.5", Description: "Fuehrungskraefte muessen Sicherheitsrichtlinien einhalten und durchsetzen"}, + {ID: "A.5.5", Name: "Kontakt mit Behoerden", Category: "organizational", CategoryID: "A.5", Description: "Pflege von Kontakten zu relevanten Aufsichtsbehoerden"}, + {ID: "A.5.6", Name: "Kontakt mit Interessengruppen", Category: "organizational", CategoryID: "A.5", Description: "Kontakt zu Fachgruppen und Sicherheitsforen"}, + {ID: "A.5.7", Name: "Bedrohungsintelligenz", Category: "organizational", CategoryID: "A.5", Description: "Sammlung und Analyse von Bedrohungsinformationen"}, + {ID: "A.5.8", Name: "Informationssicherheit im Projektmanagement", Category: "organizational", CategoryID: "A.5", Description: "Integration von Sicherheit in Projektmanagement"}, + {ID: "A.5.9", Name: "Inventar der Informationswerte", Category: "organizational", CategoryID: "A.5", Description: "Inventarisierung und Verwaltung von Informationswerten"}, + {ID: "A.5.10", Name: "Zuleassige Nutzung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer die zuleassige Nutzung von Informationswerten"}, + {ID: "A.5.11", Name: "Rueckgabe von Werten", Category: "organizational", CategoryID: "A.5", Description: "Rueckgabe von Werten bei Beendigung"}, + {ID: "A.5.12", Name: "Klassifizierung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Klassifizierungsschema fuer Informationen"}, + {ID: "A.5.13", Name: "Kennzeichnung von Informationen", Category: "organizational", CategoryID: "A.5", Description: "Kennzeichnung gemaess Klassifizierung"}, + {ID: "A.5.14", Name: "Informationsuebertragung", Category: "organizational", CategoryID: "A.5", Description: "Regeln fuer sichere Informationsuebertragung"}, + {ID: "A.5.15", Name: "Zugangssteuerung", Category: "organizational", CategoryID: "A.5", Description: "Zugangssteuerungsrichtlinie"}, + {ID: "A.5.16", Name: "Identitaetsmanagement", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung des Lebenszyklus von Identitaeten"}, + {ID: "A.5.17", Name: "Authentifizierungsinformationen", Category: "organizational", CategoryID: "A.5", Description: "Verwaltung von Authentifizierungsinformationen"}, + {ID: "A.5.18", Name: "Zugriffsrechte", Category: "organizational", CategoryID: "A.5", Description: "Vergabe, Pruefung und Entzug von Zugriffsrechten"}, + {ID: "A.5.19", Name: "Informationssicherheit in Lieferantenbeziehungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsanforderungen an Lieferanten"}, + {ID: "A.5.20", Name: "Informationssicherheit in Lieferantenvereinbarungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheitsklauseln in Vertraegen"}, + {ID: "A.5.21", Name: "IKT-Lieferkette", Category: "organizational", CategoryID: "A.5", Description: "Management der IKT-Lieferkette"}, + {ID: "A.5.22", Name: "Ueberwachung von Lieferantenservices", Category: "organizational", CategoryID: "A.5", Description: "Ueberwachung und Pruefung von Lieferantenservices"}, + {ID: "A.5.23", Name: "Cloud-Sicherheit", Category: "organizational", CategoryID: "A.5", Description: "Informationssicherheit fuer Cloud-Dienste"}, + {ID: "A.5.24", Name: "Vorfallsmanagement - Planung", Category: "organizational", CategoryID: "A.5", Description: "Planung und Vorbereitung des Vorfallsmanagements"}, + {ID: "A.5.25", Name: "Vorfallsbeurteilung", Category: "organizational", CategoryID: "A.5", Description: "Beurteilung und Entscheidung ueber Sicherheitsereignisse"}, + {ID: "A.5.26", Name: "Vorfallsreaktion", Category: "organizational", CategoryID: "A.5", Description: "Reaktion auf Sicherheitsvorfaelle"}, + {ID: "A.5.27", Name: "Aus Vorfaellen lernen", Category: "organizational", CategoryID: "A.5", Description: "Lessons Learned aus Sicherheitsvorfaellen"}, + {ID: "A.5.28", Name: "Beweissicherung", Category: "organizational", CategoryID: "A.5", Description: "Identifikation und Sicherung von Beweisen"}, + {ID: "A.5.29", Name: "Informationssicherheit bei Stoerungen", Category: "organizational", CategoryID: "A.5", Description: "Sicherheit waehrend Stoerungen und Krisen"}, + {ID: "A.5.30", Name: "IKT-Bereitschaft fuer Business Continuity", Category: "organizational", CategoryID: "A.5", Description: "IKT-Bereitschaft zur Unterstuetzung der Geschaeftskontinuitaet"}, + {ID: "A.5.31", Name: "Rechtliche Anforderungen", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung rechtlicher und vertraglicher Anforderungen"}, + {ID: "A.5.32", Name: "Geistige Eigentumsrechte", Category: "organizational", CategoryID: "A.5", Description: "Schutz geistigen Eigentums"}, + {ID: "A.5.33", Name: "Schutz von Aufzeichnungen", Category: "organizational", CategoryID: "A.5", Description: "Schutz von Aufzeichnungen vor Verlust und Manipulation"}, + {ID: "A.5.34", Name: "Datenschutz und PII", Category: "organizational", CategoryID: "A.5", Description: "Datenschutz und Schutz personenbezogener Daten"}, + {ID: "A.5.35", Name: "Unabhaengige Ueberpruefung", Category: "organizational", CategoryID: "A.5", Description: "Unabhaengige Ueberpruefung der Informationssicherheit"}, + {ID: "A.5.36", Name: "Richtlinienkonformitaet", Category: "organizational", CategoryID: "A.5", Description: "Einhaltung von Richtlinien und Standards"}, + {ID: "A.5.37", Name: "Dokumentierte Betriebsverfahren", Category: "organizational", CategoryID: "A.5", Description: "Dokumentation von Betriebsverfahren"}, + + // A.6 People Controls (8 controls) + {ID: "A.6.1", Name: "Ueberpruefen", Category: "people", CategoryID: "A.6", Description: "Hintergrundpruefungen vor der Einstellung"}, + {ID: "A.6.2", Name: "Beschaeftigungsbedingungen", Category: "people", CategoryID: "A.6", Description: "Sicherheitsanforderungen in Arbeitsvertraegen"}, + {ID: "A.6.3", Name: "Sensibilisierung und Schulung", Category: "people", CategoryID: "A.6", Description: "Awareness-Programme und Schulungen"}, + {ID: "A.6.4", Name: "Disziplinarverfahren", Category: "people", CategoryID: "A.6", Description: "Formales Disziplinarverfahren"}, + {ID: "A.6.5", Name: "Verantwortlichkeiten nach Beendigung", Category: "people", CategoryID: "A.6", Description: "Sicherheitspflichten nach Beendigung des Beschaeftigungsverhaeltnisses"}, + {ID: "A.6.6", Name: "Vertraulichkeitsvereinbarungen", Category: "people", CategoryID: "A.6", Description: "Vertraulichkeits- und Geheimhaltungsvereinbarungen"}, + {ID: "A.6.7", Name: "Remote-Arbeit", Category: "people", CategoryID: "A.6", Description: "Sicherheitsmassnahmen fuer Remote-Arbeit"}, + {ID: "A.6.8", Name: "Meldung von Sicherheitsereignissen", Category: "people", CategoryID: "A.6", Description: "Mechanismen zur Meldung von Sicherheitsereignissen"}, + + // A.7 Physical Controls (14 controls, showing key ones) + {ID: "A.7.1", Name: "Physische Sicherheitsperimeter", Category: "physical", CategoryID: "A.7", Description: "Definition physischer Sicherheitszonen"}, + {ID: "A.7.2", Name: "Physischer Zutritt", Category: "physical", CategoryID: "A.7", Description: "Zutrittskontrolle zu Sicherheitszonen"}, + {ID: "A.7.3", Name: "Sicherung von Bueros und Raeumen", Category: "physical", CategoryID: "A.7", Description: "Physische Sicherheit fuer Bueros und Raeume"}, + {ID: "A.7.4", Name: "Physische Sicherheitsueberwachung", Category: "physical", CategoryID: "A.7", Description: "Ueberwachung physischer Sicherheit"}, + {ID: "A.7.5", Name: "Schutz vor Umweltgefahren", Category: "physical", CategoryID: "A.7", Description: "Schutz gegen natuerliche und menschgemachte Gefahren"}, + {ID: "A.7.6", Name: "Arbeit in Sicherheitszonen", Category: "physical", CategoryID: "A.7", Description: "Regeln fuer das Arbeiten in Sicherheitszonen"}, + {ID: "A.7.7", Name: "Aufgeraemter Schreibtisch", Category: "physical", CategoryID: "A.7", Description: "Clean-Desk und Clear-Screen Richtlinie"}, + {ID: "A.7.8", Name: "Geraeteplatzierung", Category: "physical", CategoryID: "A.7", Description: "Platzierung und Schutz von Geraeten"}, + {ID: "A.7.9", Name: "Sicherheit von Geraeten ausserhalb", Category: "physical", CategoryID: "A.7", Description: "Sicherheit von Geraeten ausserhalb der Raeumlichkeiten"}, + {ID: "A.7.10", Name: "Speichermedien", Category: "physical", CategoryID: "A.7", Description: "Verwaltung von Speichermedien"}, + {ID: "A.7.11", Name: "Versorgungseinrichtungen", Category: "physical", CategoryID: "A.7", Description: "Schutz vor Ausfaellen der Versorgungseinrichtungen"}, + {ID: "A.7.12", Name: "Verkabelungssicherheit", Category: "physical", CategoryID: "A.7", Description: "Schutz der Verkabelung"}, + {ID: "A.7.13", Name: "Instandhaltung von Geraeten", Category: "physical", CategoryID: "A.7", Description: "Korrekte Instandhaltung von Geraeten"}, + {ID: "A.7.14", Name: "Sichere Entsorgung", Category: "physical", CategoryID: "A.7", Description: "Sichere Entsorgung oder Wiederverwendung"}, + + // A.8 Technological Controls (34 controls, showing key ones) + {ID: "A.8.1", Name: "Endbenutzergeraete", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Endbenutzergeraeten"}, + {ID: "A.8.2", Name: "Privilegierte Zugriffsrechte", Category: "technological", CategoryID: "A.8", Description: "Verwaltung privilegierter Zugriffsrechte"}, + {ID: "A.8.3", Name: "Informationszugangsbeschraenkung", Category: "technological", CategoryID: "A.8", Description: "Beschraenkung des Zugangs zu Informationen"}, + {ID: "A.8.4", Name: "Zugang zu Quellcode", Category: "technological", CategoryID: "A.8", Description: "Sicherer Zugang zu Quellcode"}, + {ID: "A.8.5", Name: "Sichere Authentifizierung", Category: "technological", CategoryID: "A.8", Description: "Sichere Authentifizierungstechnologien"}, + {ID: "A.8.6", Name: "Kapazitaetsmanagement", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung und Anpassung der Kapazitaet"}, + {ID: "A.8.7", Name: "Schutz gegen Malware", Category: "technological", CategoryID: "A.8", Description: "Schutz vor Schadprogrammen"}, + {ID: "A.8.8", Name: "Management technischer Schwachstellen", Category: "technological", CategoryID: "A.8", Description: "Identifikation und Behebung von Schwachstellen"}, + {ID: "A.8.9", Name: "Konfigurationsmanagement", Category: "technological", CategoryID: "A.8", Description: "Sichere Konfiguration von Systemen"}, + {ID: "A.8.10", Name: "Datensicherung", Category: "technological", CategoryID: "A.8", Description: "Erstellen und Testen von Datensicherungen"}, + {ID: "A.8.11", Name: "Datenredundanz", Category: "technological", CategoryID: "A.8", Description: "Redundanz von Informationsverarbeitungseinrichtungen"}, + {ID: "A.8.12", Name: "Protokollierung", Category: "technological", CategoryID: "A.8", Description: "Aufzeichnung und Ueberwachung von Aktivitaeten"}, + {ID: "A.8.13", Name: "Ueberwachung von Aktivitaeten", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung von Netzwerken und Systemen"}, + {ID: "A.8.14", Name: "Zeitsynchronisation", Category: "technological", CategoryID: "A.8", Description: "Synchronisation von Uhren"}, + {ID: "A.8.15", Name: "Nutzung privilegierter Hilfsprogramme", Category: "technological", CategoryID: "A.8", Description: "Einschraenkung privilegierter Hilfsprogramme"}, + {ID: "A.8.16", Name: "Softwareinstallation", Category: "technological", CategoryID: "A.8", Description: "Kontrolle der Softwareinstallation"}, + {ID: "A.8.17", Name: "Netzwerksicherheit", Category: "technological", CategoryID: "A.8", Description: "Sicherheit von Netzwerken"}, + {ID: "A.8.18", Name: "Netzwerksegmentierung", Category: "technological", CategoryID: "A.8", Description: "Segmentierung von Netzwerken"}, + {ID: "A.8.19", Name: "Webfilterung", Category: "technological", CategoryID: "A.8", Description: "Filterung des Webzugangs"}, + {ID: "A.8.20", Name: "Kryptografie", Category: "technological", CategoryID: "A.8", Description: "Einsatz kryptografischer Massnahmen"}, + {ID: "A.8.21", Name: "Sichere Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Sichere Entwicklungslebenszyklus"}, + {ID: "A.8.22", Name: "Sicherheitsanforderungen bei Applikationen", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsanforderungen bei Anwendungen"}, + {ID: "A.8.23", Name: "Sichere Systemarchitektur", Category: "technological", CategoryID: "A.8", Description: "Sicherheitsprinzipien in der Systemarchitektur"}, + {ID: "A.8.24", Name: "Sicheres Programmieren", Category: "technological", CategoryID: "A.8", Description: "Sichere Programmierpraktiken"}, + {ID: "A.8.25", Name: "Sicherheitstests", Category: "technological", CategoryID: "A.8", Description: "Sicherheitstests in der Entwicklung und Abnahme"}, + {ID: "A.8.26", Name: "Auslagerung der Entwicklung", Category: "technological", CategoryID: "A.8", Description: "Ueberwachung ausgelagerter Entwicklung"}, + {ID: "A.8.27", Name: "Trennung von Umgebungen", Category: "technological", CategoryID: "A.8", Description: "Trennung von Entwicklungs-, Test- und Produktionsumgebungen"}, + {ID: "A.8.28", Name: "Aenderungsmanagement", Category: "technological", CategoryID: "A.8", Description: "Formales Aenderungsmanagement"}, + {ID: "A.8.29", Name: "Sicherheitstests in der Abnahme", Category: "technological", CategoryID: "A.8", Description: "Durchfuehrung von Sicherheitstests vor Abnahme"}, + {ID: "A.8.30", Name: "Datenloeschung", Category: "technological", CategoryID: "A.8", Description: "Sichere Datenloeschung"}, + {ID: "A.8.31", Name: "Datenmaskierung", Category: "technological", CategoryID: "A.8", Description: "Techniken zur Datenmaskierung"}, + {ID: "A.8.32", Name: "Verhinderung von Datenverlust", Category: "technological", CategoryID: "A.8", Description: "DLP-Massnahmen"}, + {ID: "A.8.33", Name: "Testinformationen", Category: "technological", CategoryID: "A.8", Description: "Schutz von Testinformationen"}, + {ID: "A.8.34", Name: "Audit-Informationssysteme", Category: "technological", CategoryID: "A.8", Description: "Schutz von Audit-Tools und -systemen"}, +} + +// Default mappings: which modules cover which ISO controls +var DefaultISOModuleMappings = []ISOModuleMapping{ + { + ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen", + ISOControls: []string{"A.5.1", "A.5.2", "A.5.3", "A.5.4", "A.5.35", "A.5.36"}, + CoverageLevel: "full", + }, + { + ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung", + ISOControls: []string{"A.5.7", "A.5.8", "A.5.9", "A.5.10", "A.5.12", "A.5.13"}, + CoverageLevel: "full", + }, + { + ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung", + ISOControls: []string{"A.5.15", "A.5.16", "A.5.17", "A.5.18", "A.8.2", "A.8.3", "A.8.5"}, + CoverageLevel: "full", + }, + { + ModuleID: "iso-kryptografie", ModuleName: "Kryptografie", + ISOControls: []string{"A.8.20", "A.8.21", "A.8.24"}, + CoverageLevel: "partial", + }, + { + ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit", + ISOControls: []string{"A.7.1", "A.7.2", "A.7.3", "A.7.4", "A.7.5", "A.7.7", "A.7.8"}, + CoverageLevel: "full", + }, + { + ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen", + ISOControls: []string{"A.5.34", "A.8.10", "A.8.12", "A.8.30", "A.8.31"}, + CoverageLevel: "partial", + }, + { + ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response", + ISOControls: []string{"A.5.24", "A.5.25", "A.5.26", "A.5.27", "A.5.28", "A.6.8"}, + CoverageLevel: "full", + }, + { + ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit", + ISOControls: []string{"A.5.19", "A.5.20", "A.5.21", "A.5.22", "A.5.23"}, + CoverageLevel: "full", + }, + { + ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement", + ISOControls: []string{"A.5.29", "A.5.30", "A.8.6", "A.8.7", "A.8.8", "A.8.9"}, + CoverageLevel: "partial", + }, + { + ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen", + ISOControls: []string{"A.5.31", "A.5.34", "A.6.2", "A.6.3"}, + CoverageLevel: "partial", + }, +} + +// GetISOControlByID returns a control by its ID +func GetISOControlByID(id string) (ISOControl, bool) { + for _, c := range ISOControls { + if c.ID == id { + return c, true + } + } + return ISOControl{}, false +} + +// GetISOControlsByCategory returns all controls in a category +func GetISOControlsByCategory(categoryID string) []ISOControl { + var result []ISOControl + for _, c := range ISOControls { + if c.CategoryID == categoryID { + result = append(result, c) + } + } + return result +} + +// ISOCategorySummary provides a summary per ISO category +type ISOCategorySummary struct { + CategoryID string `json:"category_id"` + CategoryName string `json:"category_name"` + TotalControls int `json:"total_controls"` + CoveredFull int `json:"covered_full"` + CoveredPartial int `json:"covered_partial"` + NotCovered int `json:"not_covered"` +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/mock_data.go b/admin-v2/ai-compliance-sdk/internal/gci/mock_data.go new file mode 100644 index 0000000..bb8c074 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/mock_data.go @@ -0,0 +1,74 @@ +package gci + +import "time" + +// MockModuleData provides fallback data when academy store is empty +func MockModuleData(tenantID string) []ModuleScore { + return []ModuleScore{ + // DSGVO modules + {ModuleID: "dsgvo-grundlagen", ModuleName: "DSGVO Grundlagen", Assigned: 25, Completed: 22, Category: "dsgvo", RiskWeight: 2.0}, + {ModuleID: "dsgvo-betroffenenrechte", ModuleName: "Betroffenenrechte", Assigned: 25, Completed: 18, Category: "dsgvo", RiskWeight: 2.5}, + {ModuleID: "dsgvo-tom", ModuleName: "Technisch-Organisatorische Massnahmen", Assigned: 20, Completed: 17, Category: "dsgvo", RiskWeight: 2.5}, + {ModuleID: "dsgvo-dsfa", ModuleName: "Datenschutz-Folgenabschaetzung", Assigned: 15, Completed: 10, Category: "dsgvo", RiskWeight: 2.0}, + {ModuleID: "dsgvo-auftragsverarbeitung", ModuleName: "Auftragsverarbeitung", Assigned: 20, Completed: 16, Category: "dsgvo", RiskWeight: 2.0}, + + // NIS2 modules + {ModuleID: "nis2-risikomanagement", ModuleName: "NIS2 Risikomanagement", Assigned: 15, Completed: 11, Category: "nis2", RiskWeight: 3.0}, + {ModuleID: "nis2-incident-response", ModuleName: "NIS2 Incident Response", Assigned: 15, Completed: 9, Category: "nis2", RiskWeight: 3.0}, + {ModuleID: "nis2-supply-chain", ModuleName: "NIS2 Lieferkettensicherheit", Assigned: 10, Completed: 6, Category: "nis2", RiskWeight: 2.0}, + {ModuleID: "nis2-management", ModuleName: "NIS2 Geschaeftsleitungspflicht", Assigned: 10, Completed: 8, Category: "nis2", RiskWeight: 3.0}, + + // ISO 27001 modules + {ModuleID: "iso-isms", ModuleName: "ISMS Grundlagen", Assigned: 20, Completed: 16, Category: "iso27001", RiskWeight: 2.0}, + {ModuleID: "iso-risikobewertung", ModuleName: "Risikobewertung", Assigned: 15, Completed: 12, Category: "iso27001", RiskWeight: 2.0}, + {ModuleID: "iso-zugangssteuerung", ModuleName: "Zugangssteuerung", Assigned: 20, Completed: 18, Category: "iso27001", RiskWeight: 2.0}, + {ModuleID: "iso-kryptografie", ModuleName: "Kryptografie", Assigned: 10, Completed: 7, Category: "iso27001", RiskWeight: 1.5}, + {ModuleID: "iso-physisch", ModuleName: "Physische Sicherheit", Assigned: 10, Completed: 9, Category: "iso27001", RiskWeight: 1.0}, + + // AI Act modules + {ModuleID: "ai-risikokategorien", ModuleName: "KI-Risikokategorien", Assigned: 15, Completed: 12, Category: "ai_act", RiskWeight: 2.5}, + {ModuleID: "ai-transparenz", ModuleName: "KI-Transparenzpflichten", Assigned: 15, Completed: 10, Category: "ai_act", RiskWeight: 2.0}, + {ModuleID: "ai-hochrisiko", ModuleName: "Hochrisiko-KI-Systeme", Assigned: 10, Completed: 6, Category: "ai_act", RiskWeight: 2.5}, + {ModuleID: "ai-governance", ModuleName: "KI-Governance", Assigned: 10, Completed: 7, Category: "ai_act", RiskWeight: 2.0}, + } +} + +// MockCertificateData provides mock certificate validity dates +func MockCertificateData() map[string]time.Time { + now := time.Now() + return map[string]time.Time{ + "dsgvo-grundlagen": now.AddDate(0, 8, 0), // valid 8 months + "dsgvo-betroffenenrechte": now.AddDate(0, 3, 0), // expiring in 3 months + "dsgvo-tom": now.AddDate(0, 10, 0), // valid + "dsgvo-dsfa": now.AddDate(0, -1, 0), // expired 1 month ago + "dsgvo-auftragsverarbeitung": now.AddDate(0, 6, 0), + "nis2-risikomanagement": now.AddDate(0, 5, 0), + "nis2-incident-response": now.AddDate(0, 2, 0), // expiring soon + "nis2-supply-chain": now.AddDate(0, -2, 0), // expired 2 months + "nis2-management": now.AddDate(0, 9, 0), + "iso-isms": now.AddDate(1, 0, 0), + "iso-risikobewertung": now.AddDate(0, 4, 0), + "iso-zugangssteuerung": now.AddDate(0, 11, 0), + "iso-kryptografie": now.AddDate(0, 1, 0), // expiring in 1 month + "iso-physisch": now.AddDate(0, 7, 0), + "ai-risikokategorien": now.AddDate(0, 6, 0), + "ai-transparenz": now.AddDate(0, 3, 0), + "ai-hochrisiko": now.AddDate(0, -3, 0), // expired 3 months + "ai-governance": now.AddDate(0, 5, 0), + } +} + +// MockIncidentData returns mock incident counts for adjustment +func MockIncidentData() (openIncidents int, criticalIncidents int) { + return 3, 1 +} + +// MockGCIHistory returns mock historical GCI snapshots +func MockGCIHistory(tenantID string) []GCISnapshot { + now := time.Now() + return []GCISnapshot{ + {TenantID: tenantID, Score: 58.2, MaturityLevel: MaturityReactive, AreaScores: map[string]float64{"dsgvo": 62, "nis2": 48, "iso27001": 60, "ai_act": 55}, CalculatedAt: now.AddDate(0, -3, 0)}, + {TenantID: tenantID, Score: 62.5, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 65, "nis2": 55, "iso27001": 63, "ai_act": 58}, CalculatedAt: now.AddDate(0, -2, 0)}, + {TenantID: tenantID, Score: 67.8, MaturityLevel: MaturityDefined, AreaScores: map[string]float64{"dsgvo": 70, "nis2": 60, "iso27001": 68, "ai_act": 62}, CalculatedAt: now.AddDate(0, -1, 0)}, + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/models.go b/admin-v2/ai-compliance-sdk/internal/gci/models.go new file mode 100644 index 0000000..0f75779 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/models.go @@ -0,0 +1,104 @@ +package gci + +import "time" + +// Level 1: Module Score +type ModuleScore struct { + ModuleID string `json:"module_id"` + ModuleName string `json:"module_name"` + Assigned int `json:"assigned"` + Completed int `json:"completed"` + RawScore float64 `json:"raw_score"` // completions/assigned + ValidityFactor float64 `json:"validity_factor"` // 0.0-1.0 + FinalScore float64 `json:"final_score"` // RawScore * ValidityFactor + RiskWeight float64 `json:"risk_weight"` // module criticality weight + Category string `json:"category"` // dsgvo, nis2, iso27001, ai_act +} + +// Level 2: Risk-weighted Module Score per regulation area +type RiskWeightedScore struct { + AreaID string `json:"area_id"` + AreaName string `json:"area_name"` + Modules []ModuleScore `json:"modules"` + WeightedSum float64 `json:"weighted_sum"` + TotalWeight float64 `json:"total_weight"` + AreaScore float64 `json:"area_score"` // WeightedSum / TotalWeight +} + +// Level 3: Regulation Area Score +type RegulationAreaScore struct { + RegulationID string `json:"regulation_id"` // dsgvo, nis2, iso27001, ai_act + RegulationName string `json:"regulation_name"` // Display name + Score float64 `json:"score"` // 0-100 + Weight float64 `json:"weight"` // regulation weight in GCI + WeightedScore float64 `json:"weighted_score"` // Score * Weight + ModuleCount int `json:"module_count"` + CompletedCount int `json:"completed_count"` +} + +// Level 4: GCI Result +type GCIResult struct { + TenantID string `json:"tenant_id"` + GCIScore float64 `json:"gci_score"` // 0-100 + MaturityLevel string `json:"maturity_level"` // Optimized, Managed, Defined, Reactive, HighRisk + MaturityLabel string `json:"maturity_label"` // German label + CalculatedAt time.Time `json:"calculated_at"` + Profile string `json:"profile"` // default, nis2_relevant, ki_nutzer + AreaScores []RegulationAreaScore `json:"area_scores"` + CriticalityMult float64 `json:"criticality_multiplier"` + IncidentAdj float64 `json:"incident_adjustment"` + AuditTrail []AuditEntry `json:"audit_trail"` +} + +// GCI Breakdown with all 4 levels +type GCIBreakdown struct { + GCIResult + Level1Modules []ModuleScore `json:"level1_modules"` + Level2Areas []RiskWeightedScore `json:"level2_areas"` +} + +// MaturityLevel constants +const ( + MaturityOptimized = "OPTIMIZED" + MaturityManaged = "MANAGED" + MaturityDefined = "DEFINED" + MaturityReactive = "REACTIVE" + MaturityHighRisk = "HIGH_RISK" +) + +// Maturity level labels (German) +var MaturityLabels = map[string]string{ + MaturityOptimized: "Optimiert", + MaturityManaged: "Gesteuert", + MaturityDefined: "Definiert", + MaturityReactive: "Reaktiv", + MaturityHighRisk: "Hohes Risiko", +} + +// AuditEntry for score transparency +type AuditEntry struct { + Timestamp time.Time `json:"timestamp"` + Factor string `json:"factor"` + Description string `json:"description"` + Value float64 `json:"value"` + Impact string `json:"impact"` // positive, negative, neutral +} + +// ComplianceMatrixEntry maps roles to regulations +type ComplianceMatrixEntry struct { + Role string `json:"role"` + RoleName string `json:"role_name"` + Regulations map[string]float64 `json:"regulations"` // regulation_id -> score + OverallScore float64 `json:"overall_score"` + RequiredModules int `json:"required_modules"` + CompletedModules int `json:"completed_modules"` +} + +// GCI History snapshot +type GCISnapshot struct { + TenantID string `json:"tenant_id"` + Score float64 `json:"score"` + MaturityLevel string `json:"maturity_level"` + AreaScores map[string]float64 `json:"area_scores"` + CalculatedAt time.Time `json:"calculated_at"` +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go b/admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go new file mode 100644 index 0000000..c75d134 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/nis2_roles.go @@ -0,0 +1,118 @@ +package gci + +// NIS2Role defines a NIS2 role classification +type NIS2Role struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + MandatoryModules []string `json:"mandatory_modules"` + Priority int `json:"priority"` // 1=highest +} + +// NIS2RoleAssignment represents a user's NIS2 role +type NIS2RoleAssignment struct { + TenantID string `json:"tenant_id"` + UserID string `json:"user_id"` + UserName string `json:"user_name"` + RoleID string `json:"role_id"` + RoleName string `json:"role_name"` + AssignedAt string `json:"assigned_at"` +} + +// NIS2 role definitions +var NIS2Roles = map[string]NIS2Role{ + "N1": { + ID: "N1", + Name: "Geschaeftsleitung", + Description: "Leitungsorgane mit persoenlicher Haftung gemaess NIS2 Art. 20", + Priority: 1, + MandatoryModules: []string{ + "nis2-management", + "nis2-risikomanagement", + "dsgvo-grundlagen", + "iso-isms", + }, + }, + "N2": { + ID: "N2", + Name: "IT-Sicherheit / CISO", + Description: "Verantwortliche fuer IT-Sicherheit und Cybersecurity", + Priority: 2, + MandatoryModules: []string{ + "nis2-risikomanagement", + "nis2-incident-response", + "nis2-supply-chain", + "iso-zugangssteuerung", + "iso-kryptografie", + }, + }, + "N3": { + ID: "N3", + Name: "Kritische Funktionen", + Description: "Mitarbeiter in kritischen Geschaeftsprozessen", + Priority: 3, + MandatoryModules: []string{ + "nis2-risikomanagement", + "nis2-incident-response", + "dsgvo-tom", + "iso-zugangssteuerung", + }, + }, + "N4": { + ID: "N4", + Name: "Allgemeine Mitarbeiter", + Description: "Alle Mitarbeiter mit IT-Zugang", + Priority: 4, + MandatoryModules: []string{ + "nis2-risikomanagement", + "dsgvo-grundlagen", + "iso-isms", + }, + }, + "N5": { + ID: "N5", + Name: "Incident Response Team", + Description: "Mitglieder des IRT/CSIRT gemaess NIS2 Art. 21", + Priority: 2, + MandatoryModules: []string{ + "nis2-incident-response", + "nis2-risikomanagement", + "nis2-supply-chain", + "iso-zugangssteuerung", + "iso-kryptografie", + "iso-isms", + }, + }, +} + +// GetNIS2Role returns a NIS2 role by ID +func GetNIS2Role(roleID string) (NIS2Role, bool) { + r, ok := NIS2Roles[roleID] + return r, ok +} + +// ListNIS2Roles returns all NIS2 roles sorted by priority +func ListNIS2Roles() []NIS2Role { + roles := []NIS2Role{} + // Return in priority order + order := []string{"N1", "N2", "N5", "N3", "N4"} + for _, id := range order { + if r, ok := NIS2Roles[id]; ok { + roles = append(roles, r) + } + } + return roles +} + +// MockNIS2RoleAssignments returns mock role assignments +func MockNIS2RoleAssignments(tenantID string) []NIS2RoleAssignment { + return []NIS2RoleAssignment{ + {TenantID: tenantID, UserID: "user-001", UserName: "Dr. Schmidt", RoleID: "N1", RoleName: "Geschaeftsleitung", AssignedAt: "2025-06-01"}, + {TenantID: tenantID, UserID: "user-002", UserName: "M. Weber", RoleID: "N2", RoleName: "IT-Sicherheit / CISO", AssignedAt: "2025-06-01"}, + {TenantID: tenantID, UserID: "user-003", UserName: "S. Mueller", RoleID: "N5", RoleName: "Incident Response Team", AssignedAt: "2025-07-15"}, + {TenantID: tenantID, UserID: "user-004", UserName: "K. Fischer", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"}, + {TenantID: tenantID, UserID: "user-005", UserName: "L. Braun", RoleID: "N3", RoleName: "Kritische Funktionen", AssignedAt: "2025-08-01"}, + {TenantID: tenantID, UserID: "user-006", UserName: "A. Schwarz", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"}, + {TenantID: tenantID, UserID: "user-007", UserName: "T. Wagner", RoleID: "N4", RoleName: "Allgemeine Mitarbeiter", AssignedAt: "2025-09-01"}, + } +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go b/admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go new file mode 100644 index 0000000..57b7468 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/nis2_scoring.go @@ -0,0 +1,147 @@ +package gci + +import "math" + +// NIS2Score represents the NIS2-specific compliance score +type NIS2Score struct { + TenantID string `json:"tenant_id"` + OverallScore float64 `json:"overall_score"` + MaturityLevel string `json:"maturity_level"` + MaturityLabel string `json:"maturity_label"` + AreaScores []NIS2AreaScore `json:"area_scores"` + RoleCompliance []NIS2RoleScore `json:"role_compliance"` +} + +// NIS2AreaScore represents a NIS2 compliance area +type NIS2AreaScore struct { + AreaID string `json:"area_id"` + AreaName string `json:"area_name"` + Score float64 `json:"score"` + Weight float64 `json:"weight"` + ModuleIDs []string `json:"module_ids"` +} + +// NIS2RoleScore represents completion per NIS2 role +type NIS2RoleScore struct { + RoleID string `json:"role_id"` + RoleName string `json:"role_name"` + AssignedUsers int `json:"assigned_users"` + CompletionRate float64 `json:"completion_rate"` + MandatoryTotal int `json:"mandatory_total"` + MandatoryDone int `json:"mandatory_done"` +} + +// NIS2 scoring areas with weights +// NIS2Score = 25% Management + 25% Incident + 30% IT Security + 20% Supply Chain +var nis2Areas = []struct { + ID string + Name string + Weight float64 + ModuleIDs []string +}{ + { + ID: "management", Name: "Management & Governance", Weight: 0.25, + ModuleIDs: []string{"nis2-management", "dsgvo-grundlagen", "iso-isms"}, + }, + { + ID: "incident", Name: "Vorfallsbehandlung", Weight: 0.25, + ModuleIDs: []string{"nis2-incident-response"}, + }, + { + ID: "it_security", Name: "IT-Sicherheit", Weight: 0.30, + ModuleIDs: []string{"nis2-risikomanagement", "iso-zugangssteuerung", "iso-kryptografie"}, + }, + { + ID: "supply_chain", Name: "Lieferkettensicherheit", Weight: 0.20, + ModuleIDs: []string{"nis2-supply-chain", "dsgvo-auftragsverarbeitung"}, + }, +} + +// CalculateNIS2Score computes the NIS2-specific compliance score +func CalculateNIS2Score(tenantID string) *NIS2Score { + modules := MockModuleData(tenantID) + moduleMap := map[string]ModuleScore{} + for _, m := range modules { + moduleMap[m.ModuleID] = m + } + + areaScores := []NIS2AreaScore{} + totalWeighted := 0.0 + + for _, area := range nis2Areas { + areaScore := NIS2AreaScore{ + AreaID: area.ID, + AreaName: area.Name, + Weight: area.Weight, + ModuleIDs: area.ModuleIDs, + } + + scoreSum := 0.0 + count := 0 + for _, modID := range area.ModuleIDs { + if m, ok := moduleMap[modID]; ok { + if m.Assigned > 0 { + scoreSum += float64(m.Completed) / float64(m.Assigned) * 100 + } + count++ + } + } + if count > 0 { + areaScore.Score = math.Round(scoreSum/float64(count)*10) / 10 + } + totalWeighted += areaScore.Score * areaScore.Weight + areaScores = append(areaScores, areaScore) + } + + overallScore := math.Round(totalWeighted*10) / 10 + + // Calculate role compliance + roleAssignments := MockNIS2RoleAssignments(tenantID) + roleScores := calculateNIS2RoleScores(roleAssignments, moduleMap) + + return &NIS2Score{ + TenantID: tenantID, + OverallScore: overallScore, + MaturityLevel: determineMaturityLevel(overallScore), + MaturityLabel: MaturityLabels[determineMaturityLevel(overallScore)], + AreaScores: areaScores, + RoleCompliance: roleScores, + } +} + +func calculateNIS2RoleScores(assignments []NIS2RoleAssignment, moduleMap map[string]ModuleScore) []NIS2RoleScore { + // Count users per role + roleCounts := map[string]int{} + for _, a := range assignments { + roleCounts[a.RoleID]++ + } + + scores := []NIS2RoleScore{} + for roleID, role := range NIS2Roles { + rs := NIS2RoleScore{ + RoleID: roleID, + RoleName: role.Name, + AssignedUsers: roleCounts[roleID], + MandatoryTotal: len(role.MandatoryModules), + } + + completionSum := 0.0 + for _, modID := range role.MandatoryModules { + if m, ok := moduleMap[modID]; ok { + if m.Assigned > 0 { + rate := float64(m.Completed) / float64(m.Assigned) + completionSum += rate + if rate >= 0.8 { // 80%+ = considered done + rs.MandatoryDone++ + } + } + } + } + if rs.MandatoryTotal > 0 { + rs.CompletionRate = math.Round(completionSum/float64(rs.MandatoryTotal)*100*10) / 10 + } + scores = append(scores, rs) + } + + return scores +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/validity.go b/admin-v2/ai-compliance-sdk/internal/gci/validity.go new file mode 100644 index 0000000..5578f3d --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/validity.go @@ -0,0 +1,59 @@ +package gci + +import ( + "math" + "time" +) + +const ( + // GracePeriodDays is the number of days after expiry during which + // the certificate still contributes (with declining factor) + GracePeriodDays = 180 + + // DecayStartDays is how many days before expiry the linear decay begins + DecayStartDays = 180 +) + +// CalculateValidityFactor computes the validity factor for a certificate +// based on its expiry date. +// +// Rules: +// - Certificate not yet expiring (>6 months): factor = 1.0 +// - Certificate expiring within 6 months: linear decay from 1.0 to 0.5 +// - Certificate expired: linear decay from 0.5 to 0.0 over grace period +// - Certificate expired beyond grace period: factor = 0.0 +func CalculateValidityFactor(validUntil time.Time, now time.Time) float64 { + daysUntilExpiry := validUntil.Sub(now).Hours() / 24.0 + + if daysUntilExpiry > float64(DecayStartDays) { + // Not yet in decay window + return 1.0 + } + + if daysUntilExpiry > 0 { + // In pre-expiry decay window: linear from 1.0 to 0.5 + fraction := daysUntilExpiry / float64(DecayStartDays) + return 0.5 + 0.5*fraction + } + + // Certificate is expired + daysExpired := -daysUntilExpiry + if daysExpired > float64(GracePeriodDays) { + return 0.0 + } + + // In grace period: linear from 0.5 to 0.0 + fraction := 1.0 - (daysExpired / float64(GracePeriodDays)) + return math.Max(0, 0.5*fraction) +} + +// IsExpired returns true if the certificate is past its validity date +func IsExpired(validUntil time.Time, now time.Time) bool { + return now.After(validUntil) +} + +// IsExpiringSoon returns true if the certificate expires within the decay window +func IsExpiringSoon(validUntil time.Time, now time.Time) bool { + daysUntil := validUntil.Sub(now).Hours() / 24.0 + return daysUntil > 0 && daysUntil <= float64(DecayStartDays) +} diff --git a/admin-v2/ai-compliance-sdk/internal/gci/weights.go b/admin-v2/ai-compliance-sdk/internal/gci/weights.go new file mode 100644 index 0000000..7c50742 --- /dev/null +++ b/admin-v2/ai-compliance-sdk/internal/gci/weights.go @@ -0,0 +1,78 @@ +package gci + +// WeightProfile defines regulation weights for different compliance profiles +type WeightProfile struct { + ID string `json:"id"` + Name string `json:"name"` + Description string `json:"description"` + Weights map[string]float64 `json:"weights"` // regulation_id -> weight (0.0-1.0) +} + +// Default weight profiles +var DefaultProfiles = map[string]WeightProfile{ + "default": { + ID: "default", + Name: "Standard", + Description: "Ausgewogenes Profil fuer allgemeine Compliance", + Weights: map[string]float64{ + "dsgvo": 0.30, + "nis2": 0.25, + "iso27001": 0.25, + "ai_act": 0.20, + }, + }, + "nis2_relevant": { + ID: "nis2_relevant", + Name: "NIS2-relevant", + Description: "Fuer Betreiber kritischer Infrastrukturen", + Weights: map[string]float64{ + "dsgvo": 0.25, + "nis2": 0.35, + "iso27001": 0.25, + "ai_act": 0.15, + }, + }, + "ki_nutzer": { + ID: "ki_nutzer", + Name: "KI-Nutzer", + Description: "Fuer Organisationen mit KI-Einsatz", + Weights: map[string]float64{ + "dsgvo": 0.25, + "nis2": 0.25, + "iso27001": 0.20, + "ai_act": 0.30, + }, + }, +} + +// ModuleRiskWeights defines risk criticality per module type +var ModuleRiskWeights = map[string]float64{ + "incident_response": 3.0, + "management_awareness": 3.0, + "data_protection": 2.5, + "it_security": 2.5, + "supply_chain": 2.0, + "risk_assessment": 2.0, + "access_control": 2.0, + "business_continuity": 2.0, + "employee_training": 1.5, + "documentation": 1.5, + "physical_security": 1.0, + "general": 1.0, +} + +// GetProfile returns a weight profile by ID, defaulting to "default" +func GetProfile(profileID string) WeightProfile { + if p, ok := DefaultProfiles[profileID]; ok { + return p + } + return DefaultProfiles["default"] +} + +// GetModuleRiskWeight returns the risk weight for a module category +func GetModuleRiskWeight(category string) float64 { + if w, ok := ModuleRiskWeights[category]; ok { + return w + } + return 1.0 +} diff --git a/admin-v2/ai-compliance-sdk/internal/llm/service.go b/admin-v2/ai-compliance-sdk/internal/llm/service.go index 61a78e0..d640ac3 100644 --- a/admin-v2/ai-compliance-sdk/internal/llm/service.go +++ b/admin-v2/ai-compliance-sdk/internal/llm/service.go @@ -45,7 +45,7 @@ func (s *Service) GenerateDSFA(ctx context.Context, context map[string]interface } // Build prompt with context and RAG sources - prompt := s.buildDSFAPrompt(context, ragSources) + _ = s.buildDSFAPrompt(context, ragSources) // In production, this would call the Anthropic API // response, err := s.callAnthropicAPI(ctx, prompt) diff --git a/admin-v2/ai-compliance-sdk/internal/rag/service.go b/admin-v2/ai-compliance-sdk/internal/rag/service.go index 1366094..5d7e5d1 100644 --- a/admin-v2/ai-compliance-sdk/internal/rag/service.go +++ b/admin-v2/ai-compliance-sdk/internal/rag/service.go @@ -88,7 +88,7 @@ func (s *Service) getMockSearchResults(query string, topK int) []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");", + Content: "Art. 5 DSGVO - Grundsaetze fuer die Verarbeitung personenbezogener Daten\n\n(1) Personenbezogene Daten muessen:\na) auf rechtmaessige Weise, nach Treu und Glauben und in einer fuer die betroffene Person nachvollziehbaren Weise verarbeitet werden (Rechtmaessigkeit, Verarbeitung nach Treu und Glauben, Transparenz);\nb) fuer festgelegte, eindeutige und legitime Zwecke erhoben werden und duerfen nicht in einer mit diesen Zwecken nicht zu vereinbarenden Weise weiterverarbeitet werden (Zweckbindung);\nc) dem Zweck angemessen und erheblich sowie auf das fuer die Zwecke der Verarbeitung notwendige Mass beschraenkt sein (Datenminimierung);", Source: "DSGVO", Score: 0.95, Metadata: map[string]string{ diff --git a/admin-v2/app/(admin)/ai/rag/page.tsx b/admin-v2/app/(admin)/ai/rag/page.tsx index 93b63eb..23c726e 100644 --- a/admin-v2/app/(admin)/ai/rag/page.tsx +++ b/admin-v2/app/(admin)/ai/rag/page.tsx @@ -14,6 +14,7 @@ import { AIModuleSidebarResponsive } from '@/components/ai/AIModuleSidebar' // API uses local proxy route to klausur-service const API_PROXY = '/api/legal-corpus' +const DSFA_API_PROXY = '/api/dsfa-corpus' // Types interface RegulationStatus { @@ -45,6 +46,32 @@ interface SearchResult { score: number } +// DSFA source type (from /api/dsfa-corpus) +interface DsfaSource { + source_code: string + name: string + full_name?: string + organization?: string + source_url?: string + license_code: string + attribution_text: string + document_type: string + language: string + chunk_count?: number +} + +interface DsfaCorpusStatus { + qdrant_collection: string + total_sources: number + total_documents: number + total_chunks: number + qdrant_points_count: number + qdrant_status: string +} + +// RAG category filter for Regulations tab +type RegulationCategory = 'regulations' | 'dsfa' | 'nibis' | 'templates' + // Tab definitions type TabId = 'overview' | 'regulations' | 'map' | 'search' | 'data' | 'ingestion' | 'pipeline' @@ -366,20 +393,744 @@ const REGULATIONS = [ keyTopics: ['Patientenakte (MyHealth@EU)', 'Sekundaernutzung', 'Datenzugangsorgane', 'Gesundheitsdatenstandards', 'Forschungszugang'], effectiveDate: '2025 (gestaffelt bis 2029)' }, + // National Data Protection Laws (migrated from bp_dsfa_corpus) + { + code: 'AT_DSG', + name: 'DSG Oesterreich', + fullName: 'Datenschutzgesetz Oesterreich (DSG)', + type: 'national_law', + expected: 50, + description: 'Oesterreichisches Datenschutzgesetz zur Ergaenzung der DSGVO. Regelt nationale Besonderheiten wie Bildverarbeitung, Datenschutzbehoerde und Strafbestimmungen.', + relevantFor: ['Unternehmen in Oesterreich', 'DACH-Unternehmen', 'Auftragsverarbeiter'], + keyTopics: ['Nationale DSGVO-Ergaenzung', 'Bildverarbeitung', 'Datenschutzbehoerde', 'Strafbestimmungen'], + effectiveDate: '25. Mai 2018' + }, + { + code: 'BDSG_FULL', + name: 'BDSG', + fullName: 'Bundesdatenschutzgesetz (BDSG) - Volltext', + type: 'de_law', + expected: 9, + description: 'Deutsches Bundesdatenschutzgesetz als nationale Ergaenzung zur DSGVO. Regelt Beschaeftigtendatenschutz, Videoueberachung, Scoring und Datenschutzbeauftragte.', + relevantFor: ['Deutsche Unternehmen', 'Arbeitgeber', 'Auskunfteien', 'Oeffentliche Stellen'], + keyTopics: ['Beschaeftigtendatenschutz', 'Videoueberachung', 'Scoring', 'Datenschutzbeauftragter'], + effectiveDate: '25. Mai 2018' + }, + { + code: 'CH_DSG', + name: 'DSG Schweiz', + fullName: 'Datenschutzgesetz Schweiz (revDSG 2023)', + type: 'national_law', + expected: 2, + description: 'Revidiertes Schweizer Datenschutzgesetz mit DSGVO-nahen Anforderungen. Gilt fuer Schweizer Unternehmen und solche, die Schweizer Daten verarbeiten.', + relevantFor: ['Schweizer Unternehmen', 'DACH-Unternehmen', 'Internationale Dienstleister'], + keyTopics: ['Datenschutz-Folgenabschaetzung', 'Meldepflichten', 'Profiling', 'Strafbestimmungen'], + effectiveDate: '1. September 2023' + }, + { + code: 'LI_DSG', + name: 'DSG Liechtenstein', + fullName: 'Datenschutzgesetz Liechtenstein', + type: 'national_law', + expected: 1, + description: 'Liechtensteinisches Datenschutzgesetz als EWR-Umsetzung der DSGVO.', + relevantFor: ['Unternehmen in Liechtenstein', 'EWR-Dienstleister'], + keyTopics: ['EWR-Datenschutz', 'Nationale Ergaenzung', 'Datenschutzstelle'], + effectiveDate: '2018' + }, + { + code: 'BE_DPA_LAW', + name: 'Datenschutzgesetz Belgien', + fullName: 'Loi relative a la protection des donnees (Belgien)', + type: 'national_law', + expected: 153, + description: 'Belgisches Datenschutzgesetz zur nationalen Umsetzung der DSGVO. Regelt die Autorite de protection des donnees (APD).', + relevantFor: ['Unternehmen in Belgien', 'EU-Hauptsitz Bruessel'], + keyTopics: ['APD', 'Nationale DSGVO-Umsetzung', 'Strafbestimmungen', 'Sektorale Regeln'], + effectiveDate: '2018' + }, + { + code: 'NL_UAVG', + name: 'UAVG Niederlande', + fullName: 'Uitvoeringswet AVG (UAVG) Niederlande', + type: 'national_law', + expected: 138, + description: 'Niederlaendisches Ausfuehrungsgesetz zur DSGVO. Regelt nationale Besonderheiten wie BSN-Verarbeitung und Gesundheitsdaten.', + relevantFor: ['Unternehmen in den Niederlanden', 'Gesundheitssektor NL'], + keyTopics: ['BSN-Nummer', 'Gesundheitsdaten', 'Autoriteit Persoonsgegevens', 'Nationale Ergaenzung'], + effectiveDate: '25. Mai 2018' + }, + { + code: 'FR_CNIL_GUIDE', + name: 'CNIL Guide RGPD', + fullName: 'Guide pratique RGPD (CNIL Frankreich)', + type: 'national_law', + expected: 14, + description: 'Praktischer DSGVO-Leitfaden der franzoesischen Datenschutzbehoerde CNIL. Wichtig fuer alle Unternehmen mit franzoesischen Kunden.', + relevantFor: ['Unternehmen in Frankreich', 'Franzoesischsprachige Maerkte'], + keyTopics: ['CNIL-Guidance', 'Cookies', 'Einwilligung', 'Sanktionen'], + effectiveDate: '2018' + }, + { + code: 'ES_LOPDGDD', + name: 'LOPDGDD Spanien', + fullName: 'Ley Organica de Proteccion de Datos (LOPDGDD) Spanien', + type: 'national_law', + expected: 154, + description: 'Spanisches organisches Datenschutzgesetz mit Garantien digitaler Rechte. Umfassende DSGVO-Umsetzung mit digitalen Grundrechten.', + relevantFor: ['Unternehmen in Spanien', 'Spanischsprachige Maerkte'], + keyTopics: ['Digitale Rechte', 'AEPD', 'Recht auf Vergessenwerden', 'Beschaeftigtendatenschutz'], + effectiveDate: '7. Dezember 2018' + }, + { + code: 'IT_CODICE_PRIVACY', + name: 'Codice Privacy Italien', + fullName: 'Codice in materia di protezione dei dati personali (Italien)', + type: 'national_law', + expected: 3, + description: 'Italienisches Datenschutzgesetzbuch, aktualisiert gemaess DSGVO. Umfassende nationale Regelung durch den Garante.', + relevantFor: ['Unternehmen in Italien', 'Garante-regulierte Sektoren'], + keyTopics: ['Garante Privacy', 'Codice Privacy', 'Gesundheitsdaten', 'Strafrecht'], + effectiveDate: '2018 (aktualisiert)' + }, + { + code: 'IE_DPA_2018', + name: 'DPA 2018 Ireland', + fullName: 'Data Protection Act 2018 (Ireland)', + type: 'national_law', + expected: 28, + description: 'Irisches Datenschutzgesetz. Besonders relevant da viele Tech-Konzerne (Google, Meta, Apple) ihren EU-Hauptsitz in Irland haben.', + relevantFor: ['Tech-Konzerne mit EU-Sitz Irland', 'Irische Unternehmen', 'DPC-reguliert'], + keyTopics: ['DPC Ireland', 'Big Tech Aufsicht', 'Nationale Ergaenzung', 'Strafbestimmungen'], + effectiveDate: '24. Mai 2018' + }, + { + code: 'UK_DPA_2018', + name: 'DPA 2018 UK', + fullName: 'Data Protection Act 2018 (United Kingdom)', + type: 'national_law', + expected: 94, + description: 'Britisches Datenschutzgesetz nach dem Brexit. Ergaenzt die UK GDPR mit nationalen Bestimmungen, reguliert durch das ICO.', + relevantFor: ['Unternehmen mit UK-Kunden', 'UK-Datentransfers', 'ICO-regulierte Unternehmen'], + keyTopics: ['ICO', 'UK Adequacy', 'Post-Brexit Datenschutz', 'Law Enforcement'], + effectiveDate: '23. Mai 2018' + }, + { + code: 'UK_GDPR', + name: 'UK GDPR', + fullName: 'UK General Data Protection Regulation (retained EU law)', + type: 'national_law', + expected: 24, + description: 'In UK-Recht ueberfuehrte DSGVO nach dem Brexit. Weitgehend identisch mit EU-DSGVO, aber unter britischer Aufsicht (ICO).', + relevantFor: ['UK-Unternehmen', 'EU-UK Datentransfers', 'Internationale Konzerne'], + keyTopics: ['Retained EU Law', 'UK-EU Adequacy', 'ICO Enforcement', 'UK-spezifische Anpassungen'], + effectiveDate: '1. Januar 2021' + }, + { + code: 'NO_PERSONOPPLYSNINGSLOVEN', + name: 'Personopplysningsloven', + fullName: 'Personopplysningsloven (Norwegen)', + type: 'national_law', + expected: 18, + description: 'Norwegisches Datenschutzgesetz als EWR-Umsetzung der DSGVO. Reguliert durch Datatilsynet.', + relevantFor: ['Unternehmen in Norwegen', 'EWR-Dienstleister', 'Skandinavische Maerkte'], + keyTopics: ['Datatilsynet', 'EWR-Datenschutz', 'Nationale Ergaenzung', 'Kameras'], + effectiveDate: '20. Juli 2018' + }, + { + code: 'SE_DATASKYDDSLAG', + name: 'Dataskyddslag Schweden', + fullName: 'Dataskyddslag (2018:218) Schweden', + type: 'national_law', + expected: 30, + description: 'Schwedisches Datenschutzgesetz als ergaenzende Bestimmungen zur DSGVO. Reguliert durch IMY.', + relevantFor: ['Unternehmen in Schweden', 'Skandinavische Maerkte'], + keyTopics: ['IMY', 'Personnummer', 'Forschungsdaten', 'Pressefreiheit'], + effectiveDate: '25. Mai 2018' + }, + { + code: 'FI_TIETOSUOJALAKI', + name: 'Tietosuojalaki Finnland', + fullName: 'Tietosuojalaki (1050/2018) Finnland', + type: 'national_law', + expected: 1, + description: 'Finnisches Datenschutzgesetz als nationale Ergaenzung zur DSGVO.', + relevantFor: ['Unternehmen in Finnland', 'Nordische Maerkte'], + keyTopics: ['Nationale Ergaenzung', 'Tietosuojavaltuutettu', 'Forschungsdaten'], + effectiveDate: '1. Januar 2019' + }, + { + code: 'PL_UODO', + name: 'UODO Polen', + fullName: 'Ustawa o ochronie danych osobowych (Polen)', + type: 'national_law', + expected: 1, + description: 'Polnisches Datenschutzgesetz als DSGVO-Umsetzung. Reguliert durch den UODO (Praesident des Amtes fuer den Schutz personenbezogener Daten).', + relevantFor: ['Unternehmen in Polen', 'Osteuropaeische Maerkte'], + keyTopics: ['UODO', 'Nationale Ergaenzung', 'Strafbestimmungen', 'Oeffentlicher Sektor'], + effectiveDate: '25. Mai 2018' + }, + { + code: 'CZ_ZOU', + name: 'Zakon Tschechien', + fullName: 'Zakon o zpracovani osobnich udaju (Tschechien)', + type: 'national_law', + expected: 135, + description: 'Tschechisches Datenschutzgesetz zur DSGVO-Umsetzung. Reguliert durch das UOOU.', + relevantFor: ['Unternehmen in Tschechien', 'Mitteleuropaeische Maerkte'], + keyTopics: ['UOOU', 'Nationale Ergaenzung', 'Kamerasysteme', 'Strafbestimmungen'], + effectiveDate: '24. April 2019' + }, + { + code: 'HU_INFOTV', + name: 'Infotv. Ungarn', + fullName: 'Informacios torvenye (Infotv.) Ungarn', + type: 'national_law', + expected: 156, + description: 'Ungarisches Informationsgesetz ueber Selbstbestimmung und Informationsfreiheit als DSGVO-Ergaenzung. Reguliert durch NAIH.', + relevantFor: ['Unternehmen in Ungarn', 'Mitteleuropaeische Maerkte'], + keyTopics: ['NAIH', 'Informationsfreiheit', 'Nationale Ergaenzung', 'Datensicherheit'], + effectiveDate: '2018 (aktualisiert)' + }, + { + code: 'SCC_FULL_TEXT', + name: 'SCC Volltext', + fullName: 'Standardvertragsklauseln Volltext (2021/914/EU)', + type: 'eu_regulation', + expected: 154, + description: 'Vollstaendiger Text der EU-Standardvertragsklauseln fuer internationale Datentransfers. Alle Module (C2C, C2P, P2C, P2P) mit Annexen.', + relevantFor: ['Alle mit Drittlandtransfers', 'Cloud-Nutzer', 'Auftragsverarbeiter'], + keyTopics: ['Module 1-4', 'TIA', 'Annexe', 'Technische Massnahmen'], + effectiveDate: '27. Juni 2021' + }, + { + code: 'EDPB_GUIDELINES_2_2019', + name: 'EDPB GL Art. 6(1)(b)', + fullName: 'EDPB Leitlinien 2/2019 zu Art. 6(1)(b) DSGVO', + type: 'eu_guideline', + expected: 3, + description: 'EDPB-Leitlinien zur Verarbeitung personenbezogener Daten auf Grundlage der Vertragserfullung gemaess Art. 6 Abs. 1 lit. b DSGVO.', + relevantFor: ['Alle Verantwortlichen', 'Vertragsdatenverarbeitung', 'Online-Dienste'], + keyTopics: ['Vertragserfullung', 'Art. 6(1)(b)', 'Erforderlichkeit', 'Online-Dienste'], + effectiveDate: '2019' + }, + { + code: 'EDPB_GUIDELINES_3_2019', + name: 'EDPB GL Videoueberwachung', + fullName: 'EDPB Leitlinien 3/2019 Videoueberwachung', + type: 'eu_guideline', + expected: 3, + description: 'EDPB-Leitlinien zur Verarbeitung personenbezogener Daten durch Videoueberwachungsgeraete.', + relevantFor: ['Videoueberwachung', 'Sicherheitsdienste', 'Einzelhandel', 'Oeffentliche Stellen'], + keyTopics: ['Videoueberwachung', 'Kameras', 'Speicherfristen', 'Hinweisschilder'], + effectiveDate: '2020' + }, + { + code: 'EDPB_GUIDELINES_5_2020', + name: 'EDPB GL Einwilligung', + fullName: 'EDPB Leitlinien 5/2020 zur Einwilligung', + type: 'eu_guideline', + expected: 2, + description: 'EDPB-Leitlinien zur Einwilligung gemaess DSGVO. Klaert Anforderungen an gueltige Einwilligungen, Widerruf und Cookie-Consent.', + relevantFor: ['Website-Betreiber', 'Marketing', 'App-Entwickler', 'Consent-Management'], + keyTopics: ['Einwilligung', 'Cookie-Consent', 'Widerruf', 'Freiwilligkeit'], + effectiveDate: '2020' + }, + { + code: 'EDPB_GUIDELINES_7_2020', + name: 'EDPB GL Controller/Processor', + fullName: 'EDPB Leitlinien 7/2020 Controller und Processor', + type: 'eu_guideline', + expected: 2, + description: 'EDPB-Leitlinien zu den Begriffen Verantwortlicher und Auftragsverarbeiter. Klaert Rollen, Pflichten und Joint Controllership.', + relevantFor: ['Alle Verantwortlichen', 'Auftragsverarbeiter', 'Joint Controller'], + keyTopics: ['Verantwortlicher', 'Auftragsverarbeiter', 'Joint Controller', 'AVV'], + effectiveDate: '2021' + }, + // ===================================================================== + // DACH National Laws — Deutschland + // ===================================================================== + { + code: 'DE_DDG', + name: 'Digitale-Dienste-Gesetz', + fullName: 'Digitale-Dienste-Gesetz (DDG)', + type: 'de_law', + expected: 30, + description: 'Deutsches Umsetzungsgesetz zum DSA. Regelt Impressumspflicht (§5 DDG), Informationspflichten fuer digitale Dienste und Cookie-Consent.', + relevantFor: ['Website-Betreiber', 'Online-Dienste', 'Plattformen'], + keyTopics: ['Impressumspflicht §5', 'Informationspflichten', 'Digitale Dienste'], + effectiveDate: '14. Mai 2024' + }, + { + code: 'DE_BGB_AGB', + name: 'BGB AGB-Recht', + fullName: 'BGB §§305-310, 312-312k — AGB und Fernabsatz', + type: 'de_law', + expected: 40, + description: 'Deutsches AGB-Recht: Einbeziehungskontrolle (§305), Inhaltskontrolle (§307), Klauselverbote (§§308-309). Fernabsatz: Widerrufsrecht, Button-Loesung.', + relevantFor: ['Alle Unternehmen mit AGB', 'Online-Shops', 'SaaS-Anbieter', 'Dienstleister'], + keyTopics: ['AGB-Kontrolle', 'Klauselverbote', 'Widerrufsrecht', 'Button-Loesung', 'Fernabsatz'], + effectiveDate: 'Dauerhaft gueltig' + }, + { + code: 'DE_EGBGB', + name: 'EGBGB Art. 246-248', + fullName: 'EGBGB — Informationspflichten bei Verbrauchervertraegen', + type: 'de_law', + expected: 20, + description: 'Detaillierte Informationspflichten bei Verbrauchervertraegen (Art. 246), Fernabsatz (Art. 246a) und E-Commerce (Art. 246c).', + relevantFor: ['Online-Shops', 'E-Commerce', 'Dienstleister', 'App-Anbieter'], + keyTopics: ['Vorvertragliche Information', 'Widerrufsbelehrung', 'E-Commerce-Pflichten'], + effectiveDate: 'Dauerhaft gueltig' + }, + { + code: 'DE_UWG', + name: 'UWG Deutschland', + fullName: 'Gesetz gegen den unlauteren Wettbewerb (UWG)', + type: 'de_law', + expected: 25, + description: 'Schutz vor unlauterem Wettbewerb: irrefuehrende Werbung, Spam-Verbot, Preisangaben, Online-Marketing-Regeln.', + relevantFor: ['Marketing', 'Vertrieb', 'Online-Shops', 'Werbetreibende'], + keyTopics: ['Irrefuehrende Werbung', 'Spam-Verbot', 'Dark Patterns', 'Preisangaben'], + effectiveDate: '2004 (laufend aktualisiert)' + }, + { + code: 'DE_HGB_RET', + name: 'HGB Aufbewahrung', + fullName: 'HGB §§238-261, 257 — Handelsbuecher und Aufbewahrungsfristen', + type: 'de_law', + expected: 15, + description: 'Buchfuehrungspflicht und handelsrechtliche Aufbewahrungsfristen: 6 Jahre (Handelsbriefe) und 10 Jahre (Buchungsbelege, Jahresabschluesse).', + relevantFor: ['Alle Kaufleute', 'Kapitalgesellschaften', 'Buchhaltung'], + keyTopics: ['Aufbewahrung 6/10 Jahre', 'Buchfuehrungspflicht', 'Elektronische Aufbewahrung'], + effectiveDate: 'Dauerhaft gueltig' + }, + { + code: 'DE_AO_RET', + name: 'AO Aufbewahrung', + fullName: 'Abgabenordnung §§140-148 — Steuerliche Aufbewahrungspflichten', + type: 'de_law', + expected: 12, + description: 'Steuerliche Buchfuehrungs- und Aufbewahrungspflichten. 6/10 Jahre Fristen, Datenzugriff durch Finanzbehoerden (§147 Abs. 6).', + relevantFor: ['Alle Steuerpflichtigen', 'Gewerbetreibende', 'Buchhaltung'], + keyTopics: ['Steuerliche Aufbewahrung', 'Datenzugriff Finanzamt', 'GoBD'], + effectiveDate: 'Dauerhaft gueltig' + }, + { + code: 'DE_TKG', + name: 'TKG 2021', + fullName: 'Telekommunikationsgesetz 2021', + type: 'de_law', + expected: 45, + description: 'Telekommunikationsregulierung: Kundenschutz, Datenschutz, Vertragslaufzeiten max. 24 Monate, Netzinfrastruktur.', + relevantFor: ['Telekommunikationsanbieter', 'VoIP-Dienste', 'ISPs'], + keyTopics: ['Kundenschutz', 'Vertragslaufzeiten', 'Fernmeldegeheimnis', 'Netzneutralitaet'], + effectiveDate: '1. Dezember 2021' + }, + { + code: 'DE_PANGV', + name: 'PAngV', + fullName: 'Preisangabenverordnung (PAngV 2022)', + type: 'de_law', + expected: 15, + description: 'Preisangaben: Gesamtpreis, Grundpreis, Streichpreise (§11 — 30-Tage-Regel), Online-Preisauszeichnung.', + relevantFor: ['Online-Shops', 'Einzelhandel', 'Marktplaetze'], + keyTopics: ['Gesamtpreis', 'Grundpreis', 'Streichpreis-Regel', 'Online-Preise'], + effectiveDate: '28. Mai 2022' + }, + { + code: 'DE_DLINFOV', + name: 'DL-InfoV', + fullName: 'Dienstleistungs-Informationspflichten-Verordnung', + type: 'de_law', + expected: 10, + description: 'Informationspflichten fuer Dienstleister: Identitaet, Kontakt, Berufshaftpflicht, AGB-Zugang.', + relevantFor: ['Dienstleister', 'Freiberufler', 'Handwerker'], + keyTopics: ['Dienstleister-Impressum', 'Kontaktdaten', 'Berufshaftpflicht'], + effectiveDate: '17. Mai 2010' + }, + { + code: 'DE_BETRVG', + name: 'BetrVG §87', + fullName: 'Betriebsverfassungsgesetz §87 Abs.1 Nr.6', + type: 'de_law', + expected: 5, + description: 'Mitbestimmung des Betriebsrats bei technischer Ueberwachung: IT-Systeme die Arbeitnehmerverhalten ueberwachen koennen.', + relevantFor: ['Arbeitgeber mit Betriebsrat', 'IT-Abteilungen', 'HR'], + keyTopics: ['Mitbestimmung', 'Technische Ueberwachung', 'IT-Systeme', 'DSFA-Pflicht'], + effectiveDate: '1972 (laufend aktualisiert)' + }, + { + code: 'DE_GESCHGEHG', + name: 'GeschGehG', + fullName: 'Gesetz zum Schutz von Geschaeftsgeheimnissen', + type: 'de_law', + expected: 10, + description: 'Schutz von Geschaeftsgeheimnissen: Definition, angemessene Geheimhaltungsmassnahmen erforderlich, Reverse Engineering erlaubt.', + relevantFor: ['Alle Unternehmen', 'IT-Sicherheit', 'F&E-Abteilungen'], + keyTopics: ['Geschaeftsgeheimnis-Definition', 'Geheimhaltungsmassnahmen', 'Reverse Engineering'], + effectiveDate: '26. April 2019' + }, + { + code: 'DE_BSIG', + name: 'BSI-Gesetz', + fullName: 'Gesetz ueber das Bundesamt fuer Sicherheit in der Informationstechnik', + type: 'de_law', + expected: 20, + description: 'BSI-Aufgaben, KRITIS-Meldepflichten, IT-Sicherheitsstandards, Zertifizierung, Warn- und Empfehlungsbefugnis.', + relevantFor: ['KRITIS-Betreiber', 'IT-Sicherheit', 'Cloud-Anbieter'], + keyTopics: ['KRITIS-Meldepflicht', 'IT-Sicherheitsstandards', 'BSI-Zertifizierung'], + effectiveDate: '2009 (laufend aktualisiert)' + }, + { + code: 'DE_USTG_RET', + name: 'UStG §14b', + fullName: 'Umsatzsteuergesetz §14b — Aufbewahrung von Rechnungen', + type: 'de_law', + expected: 5, + description: 'Aufbewahrungspflicht fuer Rechnungen: 10 Jahre, Grundstuecke 20 Jahre, elektronische Aufbewahrung.', + relevantFor: ['Alle Unternehmer', 'Buchhaltung', 'Steuerberater'], + keyTopics: ['Rechnungsaufbewahrung', '10/20 Jahre Frist', 'Elektronische Rechnungen'], + effectiveDate: 'Dauerhaft gueltig' + }, + // ===================================================================== + // DACH National Laws — Oesterreich + // ===================================================================== + { + code: 'AT_ECG', + name: 'E-Commerce-Gesetz AT', + fullName: 'E-Commerce-Gesetz (ECG) Oesterreich', + type: 'at_law', + expected: 30, + description: 'Oesterreichisches E-Commerce-Gesetz: Impressum/Offenlegungspflicht (§5), Informationspflichten, Haftung von Diensteanbietern.', + relevantFor: ['Oesterreichische Online-Dienste', 'E-Commerce AT', 'Website-Betreiber'], + keyTopics: ['Impressum §5 ECG', 'Offenlegungspflicht', 'Diensteanbieter-Haftung'], + effectiveDate: '1. Januar 2002' + }, + { + code: 'AT_TKG', + name: 'TKG 2021 AT', + fullName: 'Telekommunikationsgesetz 2021 Oesterreich', + type: 'at_law', + expected: 40, + description: 'Oesterreichisches TKG: Cookie-Bestimmungen (§165), Kommunikationsgeheimnis, Endgeraetezugriff, Spam-Verbot.', + relevantFor: ['Oesterreichische Websites', 'Telekommunikation AT', 'App-Anbieter'], + keyTopics: ['Cookies §165', 'Kommunikationsgeheimnis', 'Endgeraetezugriff', 'Spam-Verbot'], + effectiveDate: '1. November 2021' + }, + { + code: 'AT_KSCHG', + name: 'KSchG Oesterreich', + fullName: 'Konsumentenschutzgesetz (KSchG) Oesterreich', + type: 'at_law', + expected: 35, + description: 'Konsumentenschutz: AGB-Kontrolle (§6 Klauselverbote, §9 Verbandsklage), Ruecktrittsrecht bei Haustuergeschaeften.', + relevantFor: ['Unternehmen mit oesterreichischen Verbrauchern', 'E-Commerce AT'], + keyTopics: ['AGB-Klauselverbote §6', 'Verbandsklage §9', 'Ruecktrittsrecht', 'Transparenzkontrolle'], + effectiveDate: '1. Oktober 1979 (laufend aktualisiert)' + }, + { + code: 'AT_FAGG', + name: 'FAGG Oesterreich', + fullName: 'Fern- und Auswaertsgeschaefte-Gesetz (FAGG) Oesterreich', + type: 'at_law', + expected: 20, + description: 'Fernabsatzrecht: Informationspflichten, Widerrufsrecht 14 Tage, Button-Loesung, Ausnahmen.', + relevantFor: ['Oesterreichische Online-Shops', 'Fernabsatz AT', 'Versandhandel'], + keyTopics: ['Widerrufsrecht 14 Tage', 'Informationspflichten', 'Button-Loesung', 'Kostenfolgen'], + effectiveDate: '13. Juni 2014' + }, + { + code: 'AT_UGB_RET', + name: 'UGB Aufbewahrung AT', + fullName: 'UGB §§189-216, 212 — Rechnungslegung und Aufbewahrung Oesterreich', + type: 'at_law', + expected: 15, + description: 'Oesterreichische Rechnungslegungspflicht und Aufbewahrungsfristen (7 Jahre). Buchfuehrung, Jahresabschluss.', + relevantFor: ['Oesterreichische Kapitalgesellschaften', 'Unternehmen >700k EUR Umsatz'], + keyTopics: ['Aufbewahrung 7 Jahre', 'Rechnungslegung', 'Buchfuehrung'], + effectiveDate: 'Dauerhaft gueltig' + }, + { + code: 'AT_BAO_RET', + name: 'BAO §132 AT', + fullName: 'Bundesabgabenordnung §132 — Aufbewahrung Oesterreich', + type: 'at_law', + expected: 5, + description: 'Steuerliche Aufbewahrungspflicht 7 Jahre fuer Buecher, Aufzeichnungen und Belege. Grundstuecke 22 Jahre.', + relevantFor: ['Oesterreichische Steuerpflichtige', 'Buchhaltung AT'], + keyTopics: ['Aufbewahrung 7 Jahre', 'Grundstuecke 22 Jahre', 'Steuerliche Belege'], + effectiveDate: 'Dauerhaft gueltig' + }, + { + code: 'AT_MEDIENG', + name: 'MedienG §§24-25 AT', + fullName: 'Mediengesetz §§24-25 Oesterreich — Impressum und Offenlegung', + type: 'at_law', + expected: 10, + description: 'Impressum/Offenlegungspflicht fuer periodische Medien und Websites in Oesterreich.', + relevantFor: ['Medienunternehmen AT', 'Website-Betreiber AT', 'Blogger AT'], + keyTopics: ['Impressum', 'Offenlegung', 'Medieninhaber', 'Periodische Medien'], + effectiveDate: '1. Januar 1982 (laufend aktualisiert)' + }, + { + code: 'AT_ABGB_AGB', + name: 'ABGB AGB-Recht AT', + fullName: 'ABGB §§861-879, 864a — AGB-Kontrolle Oesterreich', + type: 'at_law', + expected: 10, + description: 'Geltungskontrolle (§864a — ueberraschende Klauseln), Sittenwidrigkeitskontrolle (§879 Abs.3 — groebliche Benachteiligung).', + relevantFor: ['Unternehmen mit oesterreichischen Kunden', 'AGB-Ersteller'], + keyTopics: ['Geltungskontrolle §864a', 'Inhaltskontrolle §879', 'Groebliche Benachteiligung'], + effectiveDate: '1. Juni 1811 (laufend aktualisiert)' + }, + { + code: 'AT_UWG', + name: 'UWG Oesterreich', + fullName: 'Bundesgesetz gegen den unlauteren Wettbewerb Oesterreich', + type: 'at_law', + expected: 15, + description: 'Lauterkeitsrecht AT: irrefuehrende Geschaeftspraktiken, aggressive Praktiken, Preisauszeichnung.', + relevantFor: ['Marketing AT', 'Vertrieb AT', 'Werbetreibende AT'], + keyTopics: ['Irrefuehrung', 'Aggressive Praktiken', 'Preisauszeichnung', 'Unterlassungsklagen'], + effectiveDate: '1984 (laufend aktualisiert)' + }, + // ===================================================================== + // DACH National Laws — Schweiz + // ===================================================================== + { + code: 'CH_DSV', + name: 'DSV Schweiz', + fullName: 'Datenschutzverordnung (DSV) Schweiz — SR 235.11', + type: 'ch_law', + expected: 30, + description: 'Ausfuehrungsverordnung zum revDSG: Meldepflichten, DSFA-Verfahren, Auslandtransfers, technische Massnahmen.', + relevantFor: ['Schweizer Unternehmen', 'DACH-Unternehmen', 'Datenexporteure CH'], + keyTopics: ['Meldepflicht', 'DSFA-Verfahren', 'Datentransfer', 'Technische Massnahmen'], + effectiveDate: '1. September 2023' + }, + { + code: 'CH_OR_AGB', + name: 'OR AGB/Aufbewahrung CH', + fullName: 'Obligationenrecht — AGB-Kontrolle und Aufbewahrung Schweiz (SR 220)', + type: 'ch_law', + expected: 20, + description: 'Art. 8 OR (AGB-Inhaltskontrolle), Art. 19/20 (Vertragsfreiheit), Art. 957-958f (Buchfuehrung, 10 Jahre Aufbewahrung).', + relevantFor: ['Schweizer Unternehmen', 'AGB-Ersteller CH', 'Buchhaltung CH'], + keyTopics: ['AGB-Kontrolle Art. 8', 'Aufbewahrung 10 Jahre', 'Buchfuehrungspflicht'], + effectiveDate: '1. Januar 2023 (AGB-Revision)' + }, + { + code: 'CH_UWG', + name: 'UWG Schweiz', + fullName: 'Bundesgesetz gegen den unlauteren Wettbewerb Schweiz (SR 241)', + type: 'ch_law', + expected: 20, + description: 'Lauterkeitsrecht: Impressumspflicht, irrefuehrende Werbung, aggressive Verkaufsmethoden, AGB-Transparenz.', + relevantFor: ['Schweizer Unternehmen', 'Marketing CH', 'Online-Shops CH'], + keyTopics: ['Impressumspflicht', 'Irrefuehrende Werbung', 'AGB-Transparenz'], + effectiveDate: '1. Maerz 1988 (laufend aktualisiert)' + }, + { + code: 'CH_FMG', + name: 'FMG Schweiz', + fullName: 'Fernmeldegesetz Schweiz (SR 784.10)', + type: 'ch_law', + expected: 25, + description: 'Telekommunikationsregulierung: Fernmeldegeheimnis, Cookies/Tracking (Art. 45c), Spam-Verbot, Datenschutz.', + relevantFor: ['Schweizer Websites', 'Telekommunikation CH', 'App-Anbieter CH'], + keyTopics: ['Cookies Art. 45c', 'Fernmeldegeheimnis', 'Spam-Verbot', 'Tracking'], + effectiveDate: '1. April 2007 (laufend aktualisiert)' + }, + { + code: 'CH_GEBUV', + name: 'GeBuV Schweiz', + fullName: 'Geschaeftsbuecher-Verordnung Schweiz (SR 221.431)', + type: 'ch_law', + expected: 10, + description: 'Ausfuehrungsvorschriften zur Buchfuehrung: elektronische Aufbewahrung, Integritaet, Datentraeger.', + relevantFor: ['Schweizer Unternehmen', 'Buchhaltung CH', 'IT-Archivierung'], + keyTopics: ['Elektronische Aufbewahrung', 'Integritaet', 'Unveraenderbarkeit'], + effectiveDate: '1. Juni 2002' + }, + { + code: 'CH_ZERTES', + name: 'ZertES Schweiz', + fullName: 'Bundesgesetz ueber die elektronische Signatur (SR 943.03)', + type: 'ch_law', + expected: 10, + description: 'Elektronische Signatur und Zertifizierung: Qualifizierte Signaturen, Zertifizierungsdiensteanbieter.', + relevantFor: ['Vertragsmanagement CH', 'AVV-Erstellung', 'E-Government CH'], + keyTopics: ['Qualifizierte Signatur', 'Zertifizierungsdienste', 'Rechtswirkung'], + effectiveDate: '1. Januar 2017' + }, + { + code: 'CH_ZGB_PERS', + name: 'ZGB Persoenlichkeitsschutz CH', + fullName: 'Zivilgesetzbuch Art. 28-28l — Persoenlichkeitsschutz Schweiz (SR 210)', + type: 'ch_law', + expected: 8, + description: 'Persoenlichkeitsschutz: Recht am eigenen Bild, Schutz der Privatsphaere, Gegendarstellungsrecht.', + relevantFor: ['Medien CH', 'Social Media', 'Datenschutz CH'], + keyTopics: ['Persoenlichkeitsschutz', 'Recht am Bild', 'Gegendarstellung'], + effectiveDate: '1. Juli 1985 (laufend aktualisiert)' + }, + // ===================================================================== + // 3 fehlgeschlagene Quellen mit korrigierten URLs + // ===================================================================== + { + code: 'LU_DPA_LAW', + name: 'Datenschutzgesetz Luxemburg', + fullName: 'Loi du 1er aout 2018 — Datenschutzgesetz Luxemburg', + type: 'national_law', + expected: 40, + description: 'Luxemburgisches Datenschutzgesetz: Organisation der CNPD, nationale DSGVO-Ergaenzung.', + relevantFor: ['Unternehmen in Luxemburg', 'EU-Finanzplatz', 'CNPD-reguliert'], + keyTopics: ['CNPD', 'Nationale DSGVO-Umsetzung', 'Strafbestimmungen'], + effectiveDate: '1. August 2018' + }, + { + code: 'DK_DATABESKYTTELSESLOVEN', + name: 'Databeskyttelsesloven DK', + fullName: 'Databeskyttelsesloven — Datenschutzgesetz Daenemark', + type: 'national_law', + expected: 30, + description: 'Daenisches Datenschutzgesetz als ergaenzende Bestimmungen zur DSGVO. Reguliert durch Datatilsynet.', + relevantFor: ['Unternehmen in Daenemark', 'Skandinavische Maerkte'], + keyTopics: ['Datatilsynet', 'Nationale DSGVO-Ergaenzung', 'Strafbestimmungen'], + effectiveDate: '25. Mai 2018' + }, + { + code: 'EDPB_GUIDELINES_1_2022', + name: 'EDPB GL Bussgelder', + fullName: 'EDPB Leitlinien 04/2022 zur Berechnung von Bussgeldern nach der DSGVO', + type: 'eu_guideline', + expected: 15, + description: 'EDPB-Leitlinien zur Berechnung von Verwaltungsbussgeldern unter der DSGVO. Systematik, Schwere, Milderungsgruende.', + relevantFor: ['Datenschutzbeauftragte', 'Compliance-Abteilungen', 'Rechtsabteilungen'], + keyTopics: ['Bussgeldberechnung', 'Schweregrad', 'Milderungsgruende', 'Bussgeldrahmen'], + effectiveDate: '2022' + }, ] +// License info for each regulation +const REGULATION_LICENSES: Record = { + GDPR: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk der EU — frei verwendbar' }, + EPRIVACY: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' }, + TDDDG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + SCC: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Durchfuehrungsbeschluss — amtliches Werk' }, + DPF: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Angemessenheitsbeschluss — amtliches Werk' }, + AIACT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + CRA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + NIS2: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' }, + EUCSA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + DATAACT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + DGA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + DSA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + EAA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' }, + DSM: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' }, + PLD: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' }, + GPSR: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + 'BSI-TR-03161-1': { license: 'DL-DE-BY-2.0', licenseNote: 'Datenlizenz Deutschland — Namensnennung 2.0' }, + 'BSI-TR-03161-2': { license: 'DL-DE-BY-2.0', licenseNote: 'Datenlizenz Deutschland — Namensnennung 2.0' }, + 'BSI-TR-03161-3': { license: 'DL-DE-BY-2.0', licenseNote: 'Datenlizenz Deutschland — Namensnennung 2.0' }, + DORA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + PSD2: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Richtlinie — amtliches Werk' }, + AMLR: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + MiCA: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + EHDS: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Verordnung — amtliches Werk' }, + // National Data Protection Laws + AT_DSG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + BDSG_FULL: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + CH_DSG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + LI_DSG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Liechtenstein — frei verwendbar' }, + BE_DPA_LAW: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Belgien — frei verwendbar' }, + NL_UAVG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Niederlande — frei verwendbar' }, + FR_CNIL_GUIDE: { license: 'PUBLIC_DOMAIN', licenseNote: 'CNIL — oeffentliches Dokument' }, + ES_LOPDGDD: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Spanien (BOE) — frei verwendbar' }, + IT_CODICE_PRIVACY: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Italien — frei verwendbar' }, + IE_DPA_2018: { license: 'OGL-3.0', licenseNote: 'Open Government Licence v3.0 — Ireland' }, + UK_DPA_2018: { license: 'OGL-3.0', licenseNote: 'Open Government Licence v3.0 — UK' }, + UK_GDPR: { license: 'OGL-3.0', licenseNote: 'Open Government Licence v3.0 — UK' }, + NO_PERSONOPPLYSNINGSLOVEN: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Norwegen — frei verwendbar' }, + SE_DATASKYDDSLAG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweden — frei verwendbar' }, + FI_TIETOSUOJALAKI: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Finnland — frei verwendbar' }, + PL_UODO: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Polen — frei verwendbar' }, + CZ_ZOU: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Tschechien — frei verwendbar' }, + HU_INFOTV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Ungarn — frei verwendbar' }, + SCC_FULL_TEXT: { license: 'PUBLIC_DOMAIN', licenseNote: 'EU-Durchfuehrungsbeschluss — amtliches Werk' }, + EDPB_GUIDELINES_2_2019: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' }, + EDPB_GUIDELINES_3_2019: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' }, + EDPB_GUIDELINES_5_2020: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' }, + EDPB_GUIDELINES_7_2020: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' }, + // DACH National Laws — Deutschland + DE_DDG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_BGB_AGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_EGBGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_UWG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_HGB_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_AO_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_TKG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_PANGV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsche Verordnung — amtliches Werk (§5 UrhG)' }, + DE_DLINFOV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsche Verordnung — amtliches Werk (§5 UrhG)' }, + DE_BETRVG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_GESCHGEHG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_BSIG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + DE_USTG_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Deutsches Bundesgesetz — amtliches Werk (§5 UrhG)' }, + // DACH National Laws — Oesterreich + AT_ECG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_TKG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_KSCHG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_FAGG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_UGB_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_BAO_RET: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_MEDIENG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_ABGB_AGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + AT_UWG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Oesterreich — frei verwendbar' }, + // DACH National Laws — Schweiz + CH_DSV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + CH_OR_AGB: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + CH_UWG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + CH_FMG: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + CH_GEBUV: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + CH_ZERTES: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + CH_ZGB_PERS: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Schweiz — frei verwendbar' }, + // 3 fehlgeschlagene Quellen (korrigiert) + LU_DPA_LAW: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Luxemburg — frei verwendbar' }, + DK_DATABESKYTTELSESLOVEN: { license: 'PUBLIC_DOMAIN', licenseNote: 'Amtliches Werk Daenemark — frei verwendbar' }, + EDPB_GUIDELINES_1_2022: { license: 'EDPB-LICENSE', licenseNote: 'EDPB Document License' }, +} + +// License display labels +const LICENSE_LABELS: Record = { + PUBLIC_DOMAIN: 'Public Domain', + 'DL-DE-BY-2.0': 'DL-DE-BY 2.0', + 'CC-BY-4.0': 'CC BY 4.0', + 'EDPB-LICENSE': 'EDPB License', + 'OGL-3.0': 'OGL v3.0', + PROPRIETARY: 'Proprietaer', +} + const TYPE_COLORS: Record = { eu_regulation: 'bg-blue-100 text-blue-700', eu_directive: 'bg-purple-100 text-purple-700', de_law: 'bg-yellow-100 text-yellow-700', + at_law: 'bg-red-100 text-red-700', + ch_law: 'bg-rose-100 text-rose-700', bsi_standard: 'bg-green-100 text-green-700', + national_law: 'bg-orange-100 text-orange-700', + eu_guideline: 'bg-teal-100 text-teal-700', } const TYPE_LABELS: Record = { eu_regulation: 'EU-VO', eu_directive: 'EU-RL', de_law: 'DE-Gesetz', + at_law: 'AT-Gesetz', + ch_law: 'CH-Gesetz', bsi_standard: 'BSI', + national_law: 'Nat. Gesetz', + eu_guideline: 'EDPB-GL', } // Industry/Sector definitions for the regulation map @@ -693,6 +1444,13 @@ export default function RAGPage() { const [autoRefresh, setAutoRefresh] = useState(true) const [elapsedTime, setElapsedTime] = useState('') + // DSFA corpus state + const [dsfaSources, setDsfaSources] = useState([]) + const [dsfaStatus, setDsfaStatus] = useState(null) + const [dsfaLoading, setDsfaLoading] = useState(false) + const [regulationCategory, setRegulationCategory] = useState('regulations') + const [expandedDsfaSource, setExpandedDsfaSource] = useState(null) + // Data tab state const [customDocuments, setCustomDocuments] = useState([]) const [uploadFile, setUploadFile] = useState(null) @@ -734,6 +1492,28 @@ export default function RAGPage() { } }, []) + const fetchDsfaStatus = useCallback(async () => { + setDsfaLoading(true) + try { + const [statusRes, sourcesRes] = await Promise.all([ + fetch(`${DSFA_API_PROXY}?action=status`), + fetch(`${DSFA_API_PROXY}?action=sources`), + ]) + if (statusRes.ok) { + const data = await statusRes.json() + setDsfaStatus(data) + } + if (sourcesRes.ok) { + const data = await sourcesRes.json() + setDsfaSources(data.sources || data || []) + } + } catch (error) { + console.error('Failed to fetch DSFA status:', error) + } finally { + setDsfaLoading(false) + } + }, []) + const fetchCustomDocuments = useCallback(async () => { try { const res = await fetch(`${API_PROXY}?action=custom-documents`) @@ -848,7 +1628,8 @@ export default function RAGPage() { useEffect(() => { fetchStatus() - }, [fetchStatus]) + fetchDsfaStatus() + }, [fetchStatus, fetchDsfaStatus]) useEffect(() => { if (activeTab === 'pipeline') { @@ -1023,47 +1804,45 @@ export default function RAGPage() { {/* Page Purpose */} {/* AI Module Sidebar - Desktop: Fixed, Mobile: FAB + Drawer */} - {/* Stats Cards */} + {/* RAG Collections Stats */}
-

Regulierungen

-

{REGULATIONS.length}

+

Legal Corpus

+

{loading ? '-' : getTotalChunks().toLocaleString()}

+

Chunks · {REGULATIONS.length} Regulierungen

-

Chunks Total

-

{loading ? '-' : getTotalChunks().toLocaleString()}

+

DSFA Corpus

+

{dsfaLoading ? '-' : (dsfaStatus?.total_chunks || 0).toLocaleString()}

+

Chunks · {dsfaSources.length || '~70'} Quellen

-

Vector Size

-

{collectionStatus?.vectorSize || 1024}

+

NiBiS EH

+

28.662

+

Chunks · Bildungs-Erwartungshorizonte

-
-

Status

-

- {collectionStatus?.status === 'green' ? '✓ Ready' : loading ? '-' : collectionStatus?.status || 'N/A'} -

+
+

Legal Templates

+

824

+

Chunks · Dokumentvorlagen

@@ -1088,6 +1867,39 @@ export default function RAGPage() { {/* Tab Content */} {activeTab === 'overview' && (
+ {/* RAG Categories Overview */} +
+

RAG-Kategorien

+
+ + +
+

NiBiS EH

+

28.662

+

Chunks · Bildungs-Erwartungshorizonte

+
+
+

Legal Templates

+

824

+

Chunks · Dokumentvorlagen (VVT, TOM, DSFA)

+
+
+
+ {/* Quick Stats per Type */}
{Object.entries(TYPE_LABELS).map(([type, label]) => { @@ -1134,6 +1946,53 @@ export default function RAGPage() { )} {activeTab === 'regulations' && ( +
+ {/* Category Filter */} +
+ + + + +
+ + {/* Regulations Table (existing) */} + {regulationCategory === 'regulations' && (

Alle {REGULATIONS.length} Regulierungen

@@ -1224,7 +2083,17 @@ export default function RAGPage() {
- In Kraft seit: {reg.effectiveDate} +
+ In Kraft seit: {reg.effectiveDate} + {REGULATION_LICENSES[reg.code] && ( + + + {LICENSE_LABELS[REGULATION_LICENSES[reg.code].license] || REGULATION_LICENSES[reg.code].license} + + {REGULATION_LICENSES[reg.code].licenseNote} + + )} +
+ )} + + {/* DSFA Sources */} + {regulationCategory === 'dsfa' && ( +
+
+
+

DSFA Quellen ({dsfaSources.length || '~70'})

+

WP248, DSK Kurzpapiere, Muss-Listen, nationale Datenschutzgesetze

+
+ +
+ {dsfaLoading ? ( +
Lade DSFA-Quellen...
+ ) : dsfaSources.length === 0 ? ( +
+

Keine DSFA-Quellen vom Backend geladen.

+

Endpunkt: /api/dsfa-corpus?action=sources

+
+ ) : ( +
+ {dsfaSources.map((source) => { + const isExpanded = expandedDsfaSource === source.source_code + const typeColors: Record = { + regulation: 'bg-blue-100 text-blue-700', + legislation: 'bg-indigo-100 text-indigo-700', + guideline: 'bg-teal-100 text-teal-700', + checklist: 'bg-yellow-100 text-yellow-700', + standard: 'bg-green-100 text-green-700', + methodology: 'bg-purple-100 text-purple-700', + specification: 'bg-orange-100 text-orange-700', + catalog: 'bg-pink-100 text-pink-700', + guidance: 'bg-cyan-100 text-cyan-700', + } + return ( + +
setExpandedDsfaSource(isExpanded ? null : source.source_code)} + className="px-4 py-3 hover:bg-slate-50 cursor-pointer transition-colors flex items-center justify-between" + > +
+ + {source.source_code} + + {source.document_type} + + {source.name} +
+
+ + {source.language} + + {source.chunk_count != null && ( + {source.chunk_count} Chunks + )} +
+
+ {isExpanded && ( +
+
+
+

{source.full_name || source.name}

+ {source.organization && ( +

Organisation: {source.organization}

+ )} +
+
+ + + {LICENSE_LABELS[source.license_code] || source.license_code} + + {source.attribution_text} + +
+ {source.source_url && ( + + )} +
+
+ )} +
+ ) + })} +
+ )} +
+ )} + + {/* NiBiS Dokumente (info only) */} + {regulationCategory === 'nibis' && ( +
+
+
📚
+
+

NiBiS Erwartungshorizonte

+

Collection: bp_nibis_eh

+
+
+
+
+

Chunks

+

28.662

+
+
+

Vector Size

+

1024

+
+
+

Typ

+

BGE-M3

+
+
+

+ Bildungsinhalte aus dem Niedersaechsischen Bildungsserver (NiBiS). Enthaelt Erwartungshorizonte fuer + verschiedene Faecher und Schulformen. Wird ueber die Klausur-Korrektur fuer EH-Matching genutzt. + Diese Daten sind nicht direkt compliance-relevant. +

+
+ )} + + {/* Templates (info only) */} + {regulationCategory === 'templates' && ( +
+
+
📋
+
+

Legal Templates & Vorlagen

+

Collection: bp_legal_templates

+
+
+
+
+

Chunks

+

824

+
+
+

Vector Size

+

1024

+
+
+

Typ

+

BGE-M3

+
+
+

+ Vorlagen fuer VVT (Verzeichnis von Verarbeitungstaetigkeiten), TOM (Technisch-Organisatorische Massnahmen), + DSFA-Berichte und weitere Compliance-Dokumente. Werden vom AI Compliance SDK fuer die Dokumentgenerierung genutzt. +

+
+ )} +
)} {activeTab === 'map' && ( diff --git a/admin-v2/app/(admin)/architecture/page.tsx b/admin-v2/app/(admin)/architecture/page.tsx index f2b77e3..f93ec1f 100644 --- a/admin-v2/app/(admin)/architecture/page.tsx +++ b/admin-v2/app/(admin)/architecture/page.tsx @@ -30,7 +30,7 @@ export default function ArchitecturePage() { databases: ['PostgreSQL', 'Qdrant'] }} relatedPages={[ - { name: 'Compliance Hub', href: '/compliance/hub', description: 'Compliance-Module' }, + { name: 'Compliance Hub', href: '/sdk/compliance-hub', description: 'Compliance-Module' }, { name: 'AI Hub', href: '/ai', description: 'KI-Module' }, ]} /> diff --git a/admin-v2/app/(admin)/compliance/ai-act/page.tsx b/admin-v2/app/(admin)/compliance/ai-act/page.tsx deleted file mode 100644 index 96177bf..0000000 --- a/admin-v2/app/(admin)/compliance/ai-act/page.tsx +++ /dev/null @@ -1,831 +0,0 @@ -'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(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 ( -
- {/* Header */} -
-
-
-
- 🤖 -
-
-

EU-AI-Act Klassifizierung

-

Risikoklassifizierung und Compliance-Dokumentation

-
-
-
-
- -
- {/* Page Purpose */} - - - {/* Stats Cards */} -
-
-
Module gesamt
-
{stats.totalModules}
-
-
-
Minimal Risk
-
{stats.minimalRisk}
-
-
-
Limited Risk
-
{stats.limitedRisk}
-
-
-
Human-in-Loop
-
{stats.humanInLoop}/{stats.totalModules}
-
-
- - {/* Classification Banner */} -
-
-
- ⚖️ -
-
-

Gesamtklassifizierung: LIMITED RISK

-

- Breakpilot ist ein KI-Assistenzsystem mit begrenztem Risiko gemaess EU-AI-Act (Art. 52). - Es gelten Transparenzpflichten, aber keine Konformitaetsbewertung. -

-
- - Art. 52 Transparenz - - - Human-in-the-Loop - - - Assistiv, nicht autonom - -
-
-
-
- - {/* Tabs */} -
- {tabs.map((tab) => ( - - ))} -
- - {/* Tab Content */} -
- {/* Overview Tab */} - {activeTab === 'overview' && ( -
- {/* Risk Level Pyramid */} -
-

EU-AI-Act Risikopyramide

-
- {/* Unacceptable */} -
-
- - Unzulaessig - -
-
-
- - 0 Module - Social Scoring, Manipulation verboten - -
-
- {/* High */} -
-
- - Hoch - -
-
-
- - 0 Module - Keine automatischen Entscheidungen - -
-
- {/* Limited */} -
-
- - Begrenzt - -
-
-
- - {stats.limitedRisk} Module - Transparenzpflichten - -
-
- {/* Minimal */} -
-
- - Minimal - -
-
-
- - {stats.minimalRisk} Module - Freiwillige Kodizes - -
-
-
-
- - {/* Key Arguments */} -
-

Kernargumente fuer Limited Risk

-
-
- -
-
Assistiv, nicht autonom
-
KI liefert Vorschlaege, keine Entscheidungen
-
-
-
- -
-
Human-in-the-Loop
-
Lehrkraft hat immer das letzte Wort
-
-
-
- -
-
Keine Schuelerklassifikation
-
Keine Kategorisierung oder Profiling
-
-
-
- -
-
Transparente Kennzeichnung
-
KI-Inhalte sind klar markiert
-
-
-
-
- - {/* Management Statement */} -
-
- 💬 -
-

Management-Statement (Pitch-faehig)

-
- “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.” -
-
-
-
-
- )} - - {/* Modules Tab */} - {activeTab === 'modules' && ( -
-
- - - - - - - - - - - - {MODULE_ASSESSMENTS.map((module) => { - const riskInfo = getRiskLevelInfo(module.riskLevel) - const isExpanded = expandedModule === module.id - return ( - <> - - - - - - - - {isExpanded && ( - - - - )} - - ) - })} - -
ModulRisikostufeHuman-in-LoopAI-Act Artikel
-
{module.name}
-
{module.description}
-
- - {riskInfo.label} - - - {module.humanInLoop ? ( - ✓ Ja - ) : ( - ✗ Nein - )} - {module.aiActArticle} - -
-
-
-
Begruendung
-
{module.justification}
-
-
-
- Transparenzmassnahmen -
-
    - {module.transparencyMeasures.map((measure, i) => ( -
  • {measure}
  • - ))} -
-
-
-
-
-
- )} - - {/* Warnings Tab */} - {activeTab === 'warnings' && ( -
-
-
- 🚫 -
-

- Warnlinien: Was wir NIEMALS bauen duerfen -

-

- Diese Features wuerden Breakpilot in die High-Risk oder Unzulaessig-Kategorie verschieben. - Sie sind explizit von der Roadmap ausgeschlossen. -

-
-
-
- -
- {WARNING_LINES.map((warning) => { - const statusInfo = getStatusInfo(warning.currentStatus) - const riskInfo = getRiskLevelInfo(warning.wouldTrigger) - return ( -
-
-
-
- {statusInfo.icon} -
-
-

{warning.title}

-

{warning.description}

-
-
-
- Wuerde ausloesen: - - {riskInfo.label} - -
-
-
-
- Empfehlung: - {warning.recommendation} -
-
-
- ) - })} -
- - {/* Safe Zone Indicator */} -
-
-
- -
-
-

- Alle Warnlinien eingehalten: {stats.warningsSafe}/{stats.warningsTotal} -

-

- Breakpilot befindet sich sicher im Limited/Minimal Risk Bereich des EU-AI-Act. -

-
-
-
-
- )} - - {/* Documentation Tab */} - {activeTab === 'documentation' && ( -
-
-
-
-

Klassifizierungs-Memo exportieren

-

- Generiert ein vollstaendiges Compliance-Dokument zur Vorlage bei Auditoren oder Investoren. -

-
- -
-
- - {/* Preview */} -
-

Vorschau

-
-                  {generateMemo()}
-                
-
- - {/* Human-in-the-Loop Documentation */} -
-

Human-in-the-Loop Nachweis

-
-
-

Technische Massnahmen

-
    -
  • • Kein Auto-Accept Button fuer KI-Vorschlaege
  • -
  • • Mindestens 2 Klicks fuer Uebernahme erforderlich
  • -
  • • Alle KI-Outputs sind editierbar
  • -
  • • Pflicht-Review vor Freigabe an Schueler/Eltern
  • -
  • • Audit-Trail dokumentiert alle menschlichen Eingriffe
  • -
-
-
-

UI-Kennzeichnungen

-
    -
  • • “KI-Vorschlag” Label auf allen generierten Inhalten
  • -
  • • “Endverantwortung liegt bei der Lehrkraft” Hinweis
  • -
  • • Confidence-Scores wo relevant
  • -
  • • Quellenangaben fuer RAG-basierte Inhalte
  • -
-
-
-
-
- )} -
-
-
- ) -} diff --git a/admin-v2/app/(admin)/compliance/audit-checklist/page.tsx b/admin-v2/app/(admin)/compliance/audit-checklist/page.tsx deleted file mode 100644 index 5e9cc87..0000000 --- a/admin-v2/app/(admin)/compliance/audit-checklist/page.tsx +++ /dev/null @@ -1,775 +0,0 @@ -'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 = { - 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([]) - const [selectedSession, setSelectedSession] = useState(null) - const [checklist, setChecklist] = useState([]) - const [statistics, setStatistics] = useState(null) - const [loading, setLoading] = useState(true) - const [checklistLoading, setChecklistLoading] = useState(false) - const [error, setError] = useState(null) - const [findings, setFindings] = useState(null) - - // Filters - const [search, setSearch] = useState('') - const [statusFilter, setStatusFilter] = useState('') - const [regulationFilter, setRegulationFilter] = useState('') - 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(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 ( -
- - - {error && ( -
-

{error}

-
- )} - - {/* Haupt-/Nebenabweichungen Uebersicht */} - {findings && ( -
-
-

Audit Findings (ISMS)

- 0 - ? 'bg-red-100 text-red-700' - : 'bg-green-100 text-green-700' - }`}> - {findings.open_majors > 0 ? 'Zertifizierung blockiert' : 'Zertifizierungsfaehig'} - -
-
-
-

{findings.major_count}

-

Hauptabweichungen

-

(MAJOR)

- {findings.open_majors > 0 && ( -

- {findings.open_majors} offen -

- )} -
-
-

{findings.minor_count}

-

Nebenabweichungen

-

(MINOR)

- {findings.open_minors > 0 && ( -

- {findings.open_minors} offen -

- )} -
-
-

{findings.ofi_count}

-

Verbesserungen

-

(OFI)

-
-
-

{findings.total}

-

Gesamt Findings

-
-
-
- - {findings.open_majors === 0 ? ( - - ) : ( - - )} - -

Zertifizierung

-

- {findings.open_majors === 0 ? 'Moeglich' : 'Blockiert'} -

-
-
-
-
-

- Hauptabweichung (MAJOR): Signifikante Abweichung von Anforderungen - blockiert Zertifizierung bis zur Behebung.{' '} - Nebenabweichung (MINOR): Kleinere Abweichung - erfordert CAPA (Corrective Action) innerhalb 90 Tagen. -

-
-
- )} - -
- {/* Sessions Sidebar */} -
-
-

Audit Sessions

- -
- - {loading ? ( -
-
-
- ) : sessions.length === 0 ? ( -
-

Keine Sessions vorhanden

- -
- ) : ( -
- {sessions.map((session) => ( -
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' - }`} - > -
- - {session.status === 'completed' ? 'Abgeschlossen' : - session.status === 'in_progress' ? 'In Bearbeitung' : - session.status === 'archived' ? 'Archiviert' : 'Entwurf'} - - {session.completion_percentage.toFixed(0)}% -
-

{session.name}

-

{session.auditor_name}

-
-
-
-
- ))} -
- )} -
- - {/* Checklist Content */} -
- {!selectedSession ? ( -
- - - -

Waehlen Sie eine Session

-

Waehlen Sie eine Audit-Session aus der Liste oder erstellen Sie eine neue.

-
- ) : ( - <> - {/* Session Header */} -
-
-
-

{selectedSession.name}

-

{selectedSession.auditor_name} {selectedSession.auditor_organization && `- ${selectedSession.auditor_organization}`}

-
-
- {selectedSession.status === 'draft' && ( - - )} - {selectedSession.status === 'in_progress' && ( - - )} - {selectedSession.status === 'completed' && ( - - )} -
-
- - {/* Statistics */} - {statistics && ( -
-
-

{statistics.total}

-

Gesamt

-
-
-

{statistics.compliant + statistics.compliant_with_notes}

-

Konform

-
-
-

{statistics.non_compliant}

-

Nicht konform

-
-
-

{statistics.not_applicable}

-

N/A

-
-
-

{statistics.pending}

-

Ausstehend

-
-
-

{statistics.completion_percentage.toFixed(0)}%

-

Fortschritt

-
-
- )} -
- - {/* Filters */} -
- { 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" - /> - -
- - {/* Checklist Table */} -
- {checklistLoading ? ( -
-
-
- ) : checklist.length === 0 ? ( -
- Keine Eintraege gefunden -
- ) : ( - - - - - - - - - - - - - {checklist.map((item) => { - const resultConfig = RESULT_COLORS[item.current_result] || RESULT_COLORS.pending - return ( - - - - - - - - - ) - })} - -
RegulationArtikelTitelControlsStatusAktion
- {item.regulation_code} - - {item.article} - {item.paragraph && {item.paragraph}} - -

{item.title}

-
- 0 ? 'bg-green-100 text-green-700' : 'bg-slate-100 text-slate-500' - }`}> - {item.controls_mapped} - - - - {resultConfig.label} - - {item.is_signed && ( - - - - )} - - -
- )} - - {/* Pagination */} - {totalPages > 1 && ( -
- - - Seite {page} von {totalPages} - - -
- )} -
- - )} -
-
- - {/* Create Session Modal */} - {showCreateModal && ( -
-
-

Neue Audit Session

-
-
- - setNewSession({ ...newSession, name: e.target.value })} - className="w-full px-3 py-2 border rounded-lg" - placeholder="z.B. Q1 2026 DSGVO Audit" - /> -
-
- - setNewSession({ ...newSession, auditor_name: e.target.value })} - className="w-full px-3 py-2 border rounded-lg" - placeholder="Dr. Max Mustermann" - /> -
-
- - setNewSession({ ...newSession, auditor_organization: e.target.value })} - className="w-full px-3 py-2 border rounded-lg" - placeholder="TÜV Rheinland" - /> -
-
-
- - -
-
-
- )} - - {/* Sign Off Modal */} - {showSignOffModal && selectedItem && ( - { setShowSignOffModal(false); setSelectedItem(null) }} - onSignOff={signOffItem} - /> - )} -
- ) -} - -// 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 ( -
-
-

Anforderung bewerten

-

- {item.regulation_code} {item.article}: {item.title} -

- -
-
- -
- {[ - { 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) => ( - - ))} -
-
- -
- -