fix: Restore all files lost during destructive rebase

A previous `git pull --rebase origin main` dropped 177 local commits,
losing 3400+ files across admin-v2, backend, studio-v2, website,
klausur-service, and many other services. The partial restore attempt
(660295e2) only recovered some files.

This commit restores all missing files from pre-rebase ref 98933f5e
while preserving post-rebase additions (night-scheduler, night-mode UI,
NightModeWidget dashboard integration).

Restored features include:
- AI Module Sidebar (FAB), OCR Labeling, OCR Compare
- GPU Dashboard, RAG Pipeline, Magic Help
- Klausur-Korrektur (8 files), Abitur-Archiv (5+ files)
- Companion, Zeugnisse-Crawler, Screen Flow
- Full backend, studio-v2, website, klausur-service
- All compliance SDKs, agent-core, voice-service
- CI/CD configs, documentation, scripts

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-02-09 09:51:32 +01:00
parent f7487ee240
commit bfdaf63ba9
2009 changed files with 749983 additions and 1731 deletions

View File

@@ -0,0 +1,364 @@
/**
* Constants for Companion Module
* Phase colors, defaults, and configuration
*/
import { PhaseId, PhaseDurations, Phase, TeacherSettings } from './types'
// ============================================================================
// Phase Colors (Didactic Color Psychology)
// ============================================================================
export const PHASE_COLORS: Record<PhaseId, { hex: string; tailwind: string; gradient: string }> = {
einstieg: {
hex: '#4A90E2',
tailwind: 'bg-blue-500',
gradient: 'from-blue-500 to-blue-600',
},
erarbeitung: {
hex: '#F5A623',
tailwind: 'bg-orange-500',
gradient: 'from-orange-500 to-orange-600',
},
sicherung: {
hex: '#7ED321',
tailwind: 'bg-green-500',
gradient: 'from-green-500 to-green-600',
},
transfer: {
hex: '#9013FE',
tailwind: 'bg-purple-600',
gradient: 'from-purple-600 to-purple-700',
},
reflexion: {
hex: '#6B7280',
tailwind: 'bg-gray-500',
gradient: 'from-gray-500 to-gray-600',
},
}
// ============================================================================
// Phase Definitions
// ============================================================================
export const PHASE_SHORT_NAMES: Record<PhaseId, string> = {
einstieg: 'E',
erarbeitung: 'A',
sicherung: 'S',
transfer: 'T',
reflexion: 'R',
}
export const PHASE_DISPLAY_NAMES: Record<PhaseId, string> = {
einstieg: 'Einstieg',
erarbeitung: 'Erarbeitung',
sicherung: 'Sicherung',
transfer: 'Transfer',
reflexion: 'Reflexion',
}
export const PHASE_DESCRIPTIONS: Record<PhaseId, string> = {
einstieg: 'Motivation, Kontext setzen, Vorwissen aktivieren',
erarbeitung: 'Hauptinhalt, aktives Lernen, neue Konzepte',
sicherung: 'Konsolidierung, Zusammenfassung, Uebungen',
transfer: 'Anwendung, neue Kontexte, kreative Aufgaben',
reflexion: 'Rueckblick, Selbsteinschaetzung, Ausblick',
}
export const PHASE_ORDER: PhaseId[] = [
'einstieg',
'erarbeitung',
'sicherung',
'transfer',
'reflexion',
]
// ============================================================================
// Default Durations (in minutes)
// ============================================================================
export const DEFAULT_PHASE_DURATIONS: PhaseDurations = {
einstieg: 8,
erarbeitung: 20,
sicherung: 10,
transfer: 7,
reflexion: 5,
}
export const DEFAULT_LESSON_LENGTH = 45 // minutes (German standard)
export const EXTENDED_LESSON_LENGTH = 50 // minutes (with buffer)
// ============================================================================
// Timer Thresholds (in seconds)
// ============================================================================
export const TIMER_WARNING_THRESHOLD = 5 * 60 // 5 minutes = warning (yellow)
export const TIMER_CRITICAL_THRESHOLD = 2 * 60 // 2 minutes = critical (red)
// ============================================================================
// SVG Pie Timer Constants
// ============================================================================
export const PIE_TIMER_RADIUS = 42
export const PIE_TIMER_CIRCUMFERENCE = 2 * Math.PI * PIE_TIMER_RADIUS // ~263.89
export const PIE_TIMER_STROKE_WIDTH = 8
export const PIE_TIMER_SIZE = 120 // viewBox size
// ============================================================================
// Timer Color Classes
// ============================================================================
export const TIMER_COLOR_CLASSES = {
plenty: 'text-green-500 stroke-green-500',
warning: 'text-amber-500 stroke-amber-500',
critical: 'text-red-500 stroke-red-500',
overtime: 'text-red-600 stroke-red-600 animate-pulse',
}
export const TIMER_BG_COLORS = {
plenty: 'bg-green-500/10',
warning: 'bg-amber-500/10',
critical: 'bg-red-500/10',
overtime: 'bg-red-600/20',
}
// ============================================================================
// Keyboard Shortcuts
// ============================================================================
export const KEYBOARD_SHORTCUTS = {
PAUSE_RESUME: ' ', // Spacebar
EXTEND_5MIN: 'e',
NEXT_PHASE: 'n',
CLOSE_MODAL: 'Escape',
SHOW_HELP: '?',
} as const
export const KEYBOARD_SHORTCUT_DESCRIPTIONS: Record<string, string> = {
' ': 'Pause/Fortsetzen',
'e': '+5 Minuten',
'n': 'Naechste Phase',
'Escape': 'Modal schliessen',
'?': 'Hilfe anzeigen',
}
// ============================================================================
// Default Settings
// ============================================================================
export const DEFAULT_TEACHER_SETTINGS: TeacherSettings = {
defaultPhaseDurations: DEFAULT_PHASE_DURATIONS,
preferredLessonLength: DEFAULT_LESSON_LENGTH,
autoAdvancePhases: true,
soundNotifications: true,
showKeyboardShortcuts: true,
highContrastMode: false,
onboardingCompleted: false,
}
// ============================================================================
// System Templates
// ============================================================================
export const SYSTEM_TEMPLATES = [
{
templateId: 'standard-45',
name: 'Standard (45 Min)',
description: 'Klassische Unterrichtsstunde',
durations: DEFAULT_PHASE_DURATIONS,
isSystemTemplate: true,
},
{
templateId: 'double-90',
name: 'Doppelstunde (90 Min)',
description: 'Fuer laengere Arbeitsphasen',
durations: {
einstieg: 10,
erarbeitung: 45,
sicherung: 15,
transfer: 12,
reflexion: 8,
},
isSystemTemplate: true,
},
{
templateId: 'math-focused',
name: 'Mathematik-fokussiert',
description: 'Lange Erarbeitung und Sicherung',
durations: {
einstieg: 5,
erarbeitung: 25,
sicherung: 10,
transfer: 5,
reflexion: 5,
},
isSystemTemplate: true,
},
{
templateId: 'language-practice',
name: 'Sprachpraxis',
description: 'Betont kommunikative Phasen',
durations: {
einstieg: 10,
erarbeitung: 15,
sicherung: 8,
transfer: 10,
reflexion: 7,
},
isSystemTemplate: true,
},
]
// ============================================================================
// Suggestion Icons (Lucide icon names)
// ============================================================================
export const SUGGESTION_ICONS = {
grading: 'ClipboardCheck',
homework: 'BookOpen',
planning: 'Calendar',
meeting: 'Users',
deadline: 'Clock',
material: 'FileText',
communication: 'MessageSquare',
default: 'Lightbulb',
}
// ============================================================================
// Priority Colors
// ============================================================================
export const PRIORITY_COLORS = {
urgent: {
bg: 'bg-red-100',
text: 'text-red-700',
border: 'border-red-200',
dot: 'bg-red-500',
},
high: {
bg: 'bg-orange-100',
text: 'text-orange-700',
border: 'border-orange-200',
dot: 'bg-orange-500',
},
medium: {
bg: 'bg-yellow-100',
text: 'text-yellow-700',
border: 'border-yellow-200',
dot: 'bg-yellow-500',
},
low: {
bg: 'bg-slate-100',
text: 'text-slate-700',
border: 'border-slate-200',
dot: 'bg-slate-400',
},
}
// ============================================================================
// Event Type Icons & Colors
// ============================================================================
export const EVENT_TYPE_CONFIG = {
exam: {
icon: 'FileQuestion',
color: 'text-red-600',
bg: 'bg-red-50',
},
parent_meeting: {
icon: 'Users',
color: 'text-blue-600',
bg: 'bg-blue-50',
},
deadline: {
icon: 'Clock',
color: 'text-amber-600',
bg: 'bg-amber-50',
},
other: {
icon: 'Calendar',
color: 'text-slate-600',
bg: 'bg-slate-50',
},
}
// ============================================================================
// Storage Keys
// ============================================================================
export const STORAGE_KEYS = {
SETTINGS: 'companion_settings',
CURRENT_SESSION: 'companion_current_session',
ONBOARDING_STATE: 'companion_onboarding',
CUSTOM_TEMPLATES: 'companion_custom_templates',
LAST_MODE: 'companion_last_mode',
}
// ============================================================================
// API Endpoints (relative to backend)
// ============================================================================
export const API_ENDPOINTS = {
DASHBOARD: '/api/state/dashboard',
LESSON_START: '/api/classroom/sessions',
LESSON_UPDATE: '/api/classroom/sessions', // + /{id}
TEMPLATES: '/api/classroom/templates',
SETTINGS: '/api/teacher/settings',
FEEDBACK: '/api/feedback',
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Create default phases array from durations
*/
export function createDefaultPhases(durations: PhaseDurations = DEFAULT_PHASE_DURATIONS): Phase[] {
return PHASE_ORDER.map((phaseId, index) => ({
id: phaseId,
shortName: PHASE_SHORT_NAMES[phaseId],
displayName: PHASE_DISPLAY_NAMES[phaseId],
duration: durations[phaseId],
status: index === 0 ? 'active' : 'planned',
color: PHASE_COLORS[phaseId].hex,
}))
}
/**
* Calculate total duration from phase durations
*/
export function calculateTotalDuration(durations: PhaseDurations): number {
return Object.values(durations).reduce((sum, d) => sum + d, 0)
}
/**
* Get timer color status based on remaining time
*/
export function getTimerColorStatus(
remainingSeconds: number,
isOvertime: boolean
): 'plenty' | 'warning' | 'critical' | 'overtime' {
if (isOvertime) return 'overtime'
if (remainingSeconds <= TIMER_CRITICAL_THRESHOLD) return 'critical'
if (remainingSeconds <= TIMER_WARNING_THRESHOLD) return 'warning'
return 'plenty'
}
/**
* Format seconds as MM:SS
*/
export function formatTime(seconds: number): string {
const absSeconds = Math.abs(seconds)
const mins = Math.floor(absSeconds / 60)
const secs = absSeconds % 60
const sign = seconds < 0 ? '-' : ''
return `${sign}${mins.toString().padStart(2, '0')}:${secs.toString().padStart(2, '0')}`
}
/**
* Format minutes as "X Min"
*/
export function formatMinutes(minutes: number): string {
return `${minutes} Min`
}

View File

@@ -0,0 +1,2 @@
export * from './types'
export * from './constants'

View File

@@ -0,0 +1,329 @@
/**
* TypeScript Types for Companion Module
* Migration from Flask companion.py/companion_js.py
*/
// ============================================================================
// Phase System
// ============================================================================
export type PhaseId = 'einstieg' | 'erarbeitung' | 'sicherung' | 'transfer' | 'reflexion'
export interface Phase {
id: PhaseId
shortName: string // E, A, S, T, R
displayName: string
duration: number // minutes
status: 'planned' | 'active' | 'completed'
actualTime?: number // seconds (actual time spent)
color: string // hex color
}
export interface PhaseContext {
currentPhase: PhaseId
phaseDisplayName: string
}
// ============================================================================
// Dashboard / Companion Mode
// ============================================================================
export interface CompanionStats {
classesCount: number
studentsCount: number
learningUnitsCreated: number
gradesEntered: number
}
export interface Progress {
percentage: number
completed: number
total: number
}
export type SuggestionPriority = 'urgent' | 'high' | 'medium' | 'low'
export interface Suggestion {
id: string
title: string
description: string
priority: SuggestionPriority
icon: string // lucide icon name
actionTarget: string // navigation path
estimatedTime: number // minutes
}
export type EventType = 'exam' | 'parent_meeting' | 'deadline' | 'other'
export interface UpcomingEvent {
id: string
title: string
date: string // ISO date string
type: EventType
inDays: number
}
export interface CompanionData {
context: PhaseContext
stats: CompanionStats
phases: Phase[]
progress: Progress
suggestions: Suggestion[]
upcomingEvents: UpcomingEvent[]
}
// ============================================================================
// Lesson Mode
// ============================================================================
export type LessonStatus =
| 'not_started'
| 'in_progress'
| 'paused'
| 'completed'
| 'overtime'
export interface LessonPhase {
phase: PhaseId
duration: number // planned duration in minutes
status: 'planned' | 'active' | 'completed' | 'skipped'
actualTime: number // actual time spent in seconds
startedAt?: string // ISO timestamp
completedAt?: string // ISO timestamp
}
export interface Homework {
id: string
title: string
description?: string
dueDate: string // ISO date
attachments?: string[]
completed?: boolean
}
export interface Material {
id: string
title: string
type: 'document' | 'video' | 'presentation' | 'link' | 'other'
url?: string
fileName?: string
}
export interface LessonReflection {
rating: number // 1-5 stars
notes: string
nextSteps: string
savedAt?: string
}
export interface LessonSession {
sessionId: string
classId: string
className: string
subject: string
topic?: string
startTime: string // ISO timestamp
endTime?: string // ISO timestamp
phases: LessonPhase[]
totalPlannedDuration: number // minutes
currentPhaseIndex: number
elapsedTime: number // seconds
isPaused: boolean
pausedAt?: string
pauseDuration: number // total pause time in seconds
overtimeMinutes: number
status: LessonStatus
homeworkList: Homework[]
materials: Material[]
reflection?: LessonReflection
}
// ============================================================================
// Lesson Templates
// ============================================================================
export interface PhaseDurations {
einstieg: number
erarbeitung: number
sicherung: number
transfer: number
reflexion: number
}
export interface LessonTemplate {
templateId: string
name: string
description?: string
subject?: string
durations: PhaseDurations
isSystemTemplate: boolean
createdBy?: string
createdAt?: string
}
// ============================================================================
// Settings
// ============================================================================
export interface TeacherSettings {
defaultPhaseDurations: PhaseDurations
preferredLessonLength: number // minutes (default 45)
autoAdvancePhases: boolean
soundNotifications: boolean
showKeyboardShortcuts: boolean
highContrastMode: boolean
onboardingCompleted: boolean
selectedTemplateId?: string
}
// ============================================================================
// Timer State
// ============================================================================
export type TimerColorStatus = 'plenty' | 'warning' | 'critical' | 'overtime'
export interface TimerState {
isRunning: boolean
isPaused: boolean
elapsedSeconds: number
remainingSeconds: number
totalSeconds: number
progress: number // 0-1
colorStatus: TimerColorStatus
currentPhase: LessonPhase | null
}
// ============================================================================
// Forms
// ============================================================================
export interface LessonStartFormData {
classId: string
subject: string
topic?: string
templateId?: string
customDurations?: PhaseDurations
}
export interface Class {
id: string
name: string
grade: string
studentCount: number
}
// ============================================================================
// Feedback
// ============================================================================
export type FeedbackType = 'bug' | 'feature' | 'feedback'
export interface FeedbackSubmission {
type: FeedbackType
title: string
description: string
screenshot?: string // base64
sessionId?: string
metadata?: Record<string, unknown>
}
// ============================================================================
// Onboarding
// ============================================================================
export interface OnboardingStep {
step: number
title: string
description: string
completed: boolean
}
export interface OnboardingState {
currentStep: number
totalSteps: number
steps: OnboardingStep[]
selectedState?: string // Bundesland
selectedSchoolType?: string
completed: boolean
}
// ============================================================================
// WebSocket Messages
// ============================================================================
export type WSMessageType =
| 'phase_update'
| 'timer_tick'
| 'overtime_warning'
| 'pause_toggle'
| 'session_end'
| 'sync_request'
export interface WSMessage {
type: WSMessageType
payload: {
sessionId: string
phase?: number
elapsed?: number
isPaused?: boolean
overtimeMinutes?: number
[key: string]: unknown
}
timestamp: string
}
// ============================================================================
// API Responses
// ============================================================================
export interface APIResponse<T> {
success: boolean
data?: T
error?: string
message?: string
}
export interface DashboardResponse extends APIResponse<CompanionData> {}
export interface LessonResponse extends APIResponse<LessonSession> {}
export interface TemplatesResponse extends APIResponse<{ templates: LessonTemplate[] }> {}
export interface SettingsResponse extends APIResponse<TeacherSettings> {}
// ============================================================================
// Component Props
// ============================================================================
export type CompanionMode = 'companion' | 'lesson' | 'classic'
export interface ModeToggleProps {
currentMode: CompanionMode
onModeChange: (mode: CompanionMode) => void
}
export interface PhaseTimelineProps {
phases: Phase[]
currentPhaseIndex: number
onPhaseClick?: (index: number) => void
}
export interface VisualPieTimerProps {
progress: number // 0-1
remainingSeconds: number
totalSeconds: number
colorStatus: TimerColorStatus
isPaused: boolean
currentPhaseName: string
phaseColor: string
}
export interface QuickActionsBarProps {
onExtend: (minutes: number) => void
onPause: () => void
onResume: () => void
onSkip: () => void
isPaused: boolean
isLastPhase: boolean
disabled?: boolean
}

View File

@@ -0,0 +1,60 @@
/**
* Website Content Type Definitions
*
* Types for website content (no server-side imports)
*/
export interface HeroContent {
badge: string
title: string
titleHighlight1: string
titleHighlight2: string
subtitle: string
ctaPrimary: string
ctaSecondary: string
ctaHint: string
}
export interface FeatureContent {
id: string
icon: string
title: string
description: string
}
export interface FAQItem {
question: string
answer: string[]
}
export interface PricingPlan {
id: string
name: string
description: string
price: number
currency: string
interval: string
popular?: boolean
features: {
tasks: string
taskDescription: string
included: string[]
}
}
export interface WebsiteContent {
hero: HeroContent
features: FeatureContent[]
faq: FAQItem[]
pricing: PricingPlan[]
trust: {
item1: { value: string; label: string }
item2: { value: string; label: string }
item3: { value: string; label: string }
}
testimonial: {
quote: string
author: string
role: string
}
}

284
admin-v2/lib/content.ts Normal file
View File

@@ -0,0 +1,284 @@
/**
* Website Content Management (Server-only)
*
* Loads website texts from JSON files.
* Admin can edit texts via /development/content
*
* IMPORTANT: This file may only be used in Server Components!
* For Client Components use @/lib/content-types
*/
import { readFileSync, writeFileSync, existsSync, mkdirSync, accessSync, constants } from 'fs'
import { join, dirname } from 'path'
// Re-export types from content-types for backward compatibility
export type {
HeroContent,
FeatureContent,
FAQItem,
PricingPlan,
WebsiteContent,
} from './content-types'
import type { WebsiteContent } from './content-types'
// Content directory - use environment variable or relative path
function getContentDir(): string {
// Check environment variable first
if (process.env.CONTENT_DIR) {
return process.env.CONTENT_DIR
}
// Try various possible paths
const possiblePaths = [
join(process.cwd(), 'content'), // Standard: CWD/content
join(process.cwd(), 'admin-v2', 'content'), // If CWD is project root
'/app/content', // Docker container
join(dirname(__filename), '..', 'content'), // Relative to this file
]
// Check if any path exists and is writable
for (const path of possiblePaths) {
try {
if (existsSync(path)) {
accessSync(path, constants.W_OK)
return path
}
} catch {
// Path not writable, try next
}
}
// Fallback: Create in CWD
const fallbackPath = join(process.cwd(), 'content')
try {
mkdirSync(fallbackPath, { recursive: true, mode: 0o755 })
console.log(`[Content] Created content directory at: ${fallbackPath}`)
} catch (err) {
console.error(`[Content] Failed to create content directory: ${err}`)
}
return fallbackPath
}
const CONTENT_DIR = getContentDir()
// Default Content
const defaultContent: WebsiteContent = {
hero: {
badge: 'Entwickelt fuer deutsche Lehrkraefte',
title: 'Korrigieren Sie',
titleHighlight1: 'schneller',
titleHighlight2: 'besser',
subtitle: 'BreakPilot unterstuetzt Lehrkraefte mit intelligenter KI bei der Bewertung von Aufgaben. Sparen Sie bis zu 50% Ihrer Korrekturzeit und geben Sie besseres Feedback.',
ctaPrimary: '7 Tage kostenlos testen',
ctaSecondary: 'Mehr erfahren',
ctaHint: 'Keine Kreditkarte fuer den Start erforderlich',
},
features: [
{
id: 'ai-correction',
icon: '✍️',
title: 'KI-gestuetzte Korrektur',
description: 'Intelligente Analyse von Schuelerantworten mit Verbesserungsvorschlaegen und automatischer Bewertung nach Ihren Kriterien.',
},
{
id: 'templates',
icon: '📋',
title: 'Dokumentvorlagen',
description: 'Erstellen und verwalten Sie Ihre eigenen Arbeitsblatt-Vorlagen. Wiederverwendbar fuer verschiedene Klassen und Jahrgaenge.',
},
{
id: 'analytics',
icon: '📊',
title: 'Fortschrittsanalyse',
description: 'Verfolgen Sie die Entwicklung Ihrer Schueler ueber Zeit. Erkennen Sie Staerken und Schwaechen fruehzeitig.',
},
{
id: 'gdpr',
icon: '🔒',
title: 'DSGVO-konform',
description: 'Hosting in Deutschland, volle Datenschutzkonformitaet. Ihre Daten und die Ihrer Schueler sind sicher.',
},
{
id: 'team',
icon: '👥',
title: 'Team-Funktionen',
description: 'Arbeiten Sie im Fachbereich zusammen. Teilen Sie Vorlagen, Bewertungskriterien und Best Practices.',
},
{
id: 'mobile',
icon: '📱',
title: 'Ueberall verfuegbar',
description: 'Browserbasiert und responsive. Funktioniert auf Desktop, Tablet und Smartphone - ohne Installation.',
},
],
faq: [
{
question: 'Was ist bei Breakpilot eine „Aufgabe"?',
answer: [
'Eine Aufgabe ist ein abgeschlossener Arbeitsauftrag, den du mit Breakpilot erledigst.',
'Typische Beispiele:',
'• eine Klassenarbeit korrigieren (egal wie viele Seiten)',
'• mehrere Klassenarbeiten in einer Serie korrigieren',
'• einen Elternbrief erstellen',
'Wichtig: Die Anzahl der Seiten, Dateien oder Uploads spielt dabei keine Rolle.',
],
},
{
question: 'Kann ich Breakpilot kostenlos testen?',
answer: [
'Ja.',
'• Du kannst Breakpilot 7 Tage kostenlos testen',
'• Dafuer ist eine Kreditkarte erforderlich',
'• Wenn du innerhalb der Testphase kuendigst, entstehen keine Kosten',
],
},
{
question: 'Werden meine Daten fuer KI-Training verwendet?',
answer: [
'Nein.',
'• Deine Inhalte werden nicht fuer das Training oeffentlicher KI-Modelle genutzt',
'• Die Verarbeitung erfolgt DSGVO-konform',
'• Deine Daten bleiben unter deiner Kontrolle',
],
},
{
question: 'Kann ich meinen Tarif jederzeit aendern oder kuendigen?',
answer: [
'Ja.',
'• Upgrades sind jederzeit moeglich',
'• Downgrades greifen zum naechsten Abrechnungszeitraum',
'• Kuendigungen sind jederzeit moeglich',
],
},
],
pricing: [
{
id: 'basic',
name: 'Basic',
description: 'Perfekt fuer den Einstieg',
price: 9.90,
currency: 'EUR',
interval: 'Monat',
features: {
tasks: '30 Aufgaben',
taskDescription: 'pro Monat',
included: [
'KI-gestuetzte Korrektur',
'Basis-Dokumentvorlagen',
'E-Mail Support',
],
},
},
{
id: 'standard',
name: 'Standard',
description: 'Fuer regelmaessige Nutzer',
price: 19.90,
currency: 'EUR',
interval: 'Monat',
popular: true,
features: {
tasks: '100 Aufgaben',
taskDescription: 'pro Monat',
included: [
'Alles aus Basic',
'Eigene Vorlagen erstellen',
'Batch-Verarbeitung',
'Bis zu 3 Teammitglieder',
'Prioritaets-Support',
],
},
},
{
id: 'premium',
name: 'Premium',
description: 'Sorglos-Tarif fuer Vielnutzer',
price: 39.90,
currency: 'EUR',
interval: 'Monat',
features: {
tasks: 'Unbegrenzt',
taskDescription: 'Fair Use',
included: [
'Alles aus Standard',
'Unbegrenzte Aufgaben (Fair Use)',
'Bis zu 10 Teammitglieder',
'Admin-Panel & Audit-Log',
'API-Zugang',
'Eigenes Branding',
'Dedizierter Support',
],
},
},
],
trust: {
item1: { value: 'DSGVO', label: 'Konform & sicher' },
item2: { value: '7 Tage', label: 'Kostenlos testen' },
item3: { value: '100%', label: 'Made in Germany' },
},
testimonial: {
quote: 'BreakPilot hat meine Korrekturzeit halbiert. Ich habe endlich wieder Zeit fuer das Wesentliche: meine Schueler.',
author: 'Maria S.',
role: 'Deutschlehrerin, Gymnasium',
},
}
/**
* Loads content from JSON file or returns default
*/
export function getContent(): WebsiteContent {
const contentPath = join(CONTENT_DIR, 'website.json')
try {
if (existsSync(contentPath)) {
const fileContent = readFileSync(contentPath, 'utf-8')
return JSON.parse(fileContent) as WebsiteContent
}
} catch (error) {
console.error('Error loading content:', error)
}
return defaultContent
}
/**
* Saves content to JSON file
* @returns Object with success status and optional error message
*/
export function saveContent(content: WebsiteContent): { success: boolean; error?: string } {
const contentPath = join(CONTENT_DIR, 'website.json')
try {
// Ensure directory exists
if (!existsSync(CONTENT_DIR)) {
console.log(`[Content] Creating directory: ${CONTENT_DIR}`)
mkdirSync(CONTENT_DIR, { recursive: true, mode: 0o755 })
}
// Check write permissions
try {
accessSync(CONTENT_DIR, constants.W_OK)
} catch {
const error = `Directory not writable: ${CONTENT_DIR}`
console.error(`[Content] ${error}`)
return { success: false, error }
}
// Write file
writeFileSync(contentPath, JSON.stringify(content, null, 2), 'utf-8')
console.log(`[Content] Saved successfully to: ${contentPath}`)
return { success: true }
} catch (error) {
const errorMessage = error instanceof Error ? error.message : 'Unknown error'
console.error(`[Content] Error saving: ${errorMessage}`)
return { success: false, error: errorMessage }
}
}
/**
* Returns default content (for reset)
*/
export function getDefaultContent(): WebsiteContent {
return defaultContent
}

View File

@@ -0,0 +1,164 @@
/**
* Extended TypeScript types for Abitur-Archiv
* Builds upon abitur-docs-types.ts with additional search and integration features
*/
import { AbiturDokument, AbiturDocsFilter } from './abitur-docs-types'
// Theme suggestion for autocomplete search
export interface ThemaSuggestion {
label: string // "Gedichtanalyse Romantik"
count: number // 12 Dokumente
aufgabentyp: string // "gedichtanalyse"
zeitraum?: string // "Romantik"
kategorie?: string // Category for grouping
}
// Extended filter with theme search
export interface AbiturArchivFilter extends AbiturDocsFilter {
thema?: string // Semantic theme search query
aufgabentyp?: string // Specific task type filter
}
// Similar document result from RAG
export interface SimilarDocument {
id: string
dateiname: string
similarity_score: number
fach: string
jahr: number
niveau: 'eA' | 'gA'
typ: 'aufgabe' | 'erwartungshorizont'
aufgaben_nummer: string
}
// Extended document with similar documents
export interface AbiturDokumentExtended extends AbiturDokument {
similar_documents?: SimilarDocument[]
themes?: string[] // Extracted themes from content
}
// Response for archive listing
export interface AbiturArchivResponse {
documents: AbiturDokumentExtended[]
total: number
page: number
limit: number
total_pages: number
themes?: ThemaSuggestion[] // Available themes for current filter
}
// Response for theme suggestions
export interface ThemaSuggestResponse {
suggestions: ThemaSuggestion[]
query: string
}
// Response for similar documents
export interface SimilarDocsResponse {
document_id: string
similar: SimilarDocument[]
}
// Klausur creation from archive
export interface KlausurFromArchivRequest {
archiv_dokument_id: string
aufgabentyp: string
title?: string
}
export interface KlausurFromArchivResponse {
klausur_id: string
eh_id?: string
success: boolean
message?: string
}
// View mode for document display
export type ViewMode = 'grid' | 'list'
// Sort options
export type SortField = 'jahr' | 'fach' | 'datum' | 'dateiname'
export type SortDirection = 'asc' | 'desc'
export interface SortConfig {
field: SortField
direction: SortDirection
}
// Predefined theme categories
export const THEME_CATEGORIES = {
textanalyse: {
label: 'Textanalyse',
subcategories: ['pragmatisch', 'literarisch', 'sachtext', 'rede', 'kommentar']
},
gedichtanalyse: {
label: 'Gedichtanalyse',
subcategories: ['romantik', 'expressionismus', 'barock', 'klassik', 'moderne']
},
dramenanalyse: {
label: 'Dramenanalyse',
subcategories: ['klassisch', 'modern', 'episches_theater']
},
prosaanalyse: {
label: 'Prosaanalyse',
subcategories: ['roman', 'kurzgeschichte', 'novelle', 'erzaehlung']
},
eroerterung: {
label: 'Eroerterung',
subcategories: ['textgebunden', 'materialgestuetzt', 'frei']
},
sprachreflexion: {
label: 'Sprachreflexion',
subcategories: ['sprachwandel', 'sprachkritik', 'kommunikation']
}
} as const
// Popular theme suggestions (static fallback)
export const POPULAR_THEMES: ThemaSuggestion[] = [
{ label: 'Textanalyse', count: 45, aufgabentyp: 'textanalyse_pragmatisch', kategorie: 'Analyse' },
{ label: 'Gedichtanalyse', count: 38, aufgabentyp: 'gedichtanalyse', kategorie: 'Analyse' },
{ label: 'Eroerterung', count: 32, aufgabentyp: 'eroerterung_textgebunden', kategorie: 'Argumentation' },
{ label: 'Dramenanalyse', count: 28, aufgabentyp: 'dramenanalyse', kategorie: 'Analyse' },
{ label: 'Prosaanalyse', count: 25, aufgabentyp: 'prosaanalyse', kategorie: 'Analyse' },
]
// Quick action types for document cards
export type QuickAction = 'preview' | 'download' | 'add_to_klausur' | 'view_similar'
// Fullscreen viewer state
export interface ViewerState {
isFullscreen: boolean
zoom: number
currentPage: number
totalPages: number
}
// Default viewer state
export const DEFAULT_VIEWER_STATE: ViewerState = {
isFullscreen: false,
zoom: 100,
currentPage: 1,
totalPages: 1
}
// Zoom levels
export const ZOOM_LEVELS = [25, 50, 75, 100, 125, 150, 175, 200] as const
export const MIN_ZOOM = 25
export const MAX_ZOOM = 200
export const ZOOM_STEP = 25
// Helper functions
export function getThemeLabel(aufgabentyp: string): string {
const entries = Object.entries(THEME_CATEGORIES)
for (const [key, value] of entries) {
if (aufgabentyp.startsWith(key)) {
return value.label
}
}
return aufgabentyp
}
export function formatSimilarityScore(score: number): string {
return `${Math.round(score * 100)}%`
}

View File

@@ -0,0 +1,84 @@
/**
* TypeScript types for Abitur Documents (NiBiS, etc.)
*/
export interface AbiturDokument {
id: string
dateiname: string
original_dateiname: string
bundesland: string
fach: string
jahr: number
niveau: 'eA' | 'gA' // erhöhtes/grundlegendes Anforderungsniveau
typ: 'aufgabe' | 'erwartungshorizont'
aufgaben_nummer: string // I, II, III
status: 'pending' | 'indexed' | 'error'
confidence: number
file_path: string
file_size: number
indexed: boolean
vector_ids: string[]
created_at: string
updated_at: string
}
export interface AbiturDocsResponse {
documents: AbiturDokument[]
total: number
page: number
limit: number
total_pages: number
}
export interface AbiturDocsFilter {
fach?: string
jahr?: number
bundesland?: string
niveau?: 'eA' | 'gA'
typ?: 'aufgabe' | 'erwartungshorizont'
page?: number
limit?: number
}
// Available filter options
export const FAECHER = [
{ id: 'deutsch', label: 'Deutsch' },
{ id: 'mathematik', label: 'Mathematik' },
{ id: 'englisch', label: 'Englisch' },
{ id: 'biologie', label: 'Biologie' },
{ id: 'physik', label: 'Physik' },
{ id: 'chemie', label: 'Chemie' },
{ id: 'geschichte', label: 'Geschichte' },
] as const
export const JAHRE = [2025, 2024, 2023, 2022, 2021, 2020] as const
export const BUNDESLAENDER = [
{ id: 'niedersachsen', label: 'Niedersachsen' },
{ id: 'bayern', label: 'Bayern' },
{ id: 'nrw', label: 'Nordrhein-Westfalen' },
{ id: 'bw', label: 'Baden-Württemberg' },
] as const
export const NIVEAUS = [
{ id: 'eA', label: 'Erhöhtes Anforderungsniveau (eA)' },
{ id: 'gA', label: 'Grundlegendes Anforderungsniveau (gA)' },
] as const
export const TYPEN = [
{ id: 'aufgabe', label: 'Aufgabe' },
{ id: 'erwartungshorizont', label: 'Erwartungshorizont' },
] as const
// Helper functions
export function formatFileSize(bytes: number): string {
if (bytes < 1024) return bytes + ' B'
if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB'
return (bytes / (1024 * 1024)).toFixed(1) + ' MB'
}
export function formatDocumentTitle(doc: AbiturDokument): string {
const fachLabel = FAECHER.find(f => f.id === doc.fach)?.label || doc.fach
const typLabel = doc.typ === 'erwartungshorizont' ? 'EWH' : 'Aufgabe'
return `${fachLabel} ${doc.jahr} - ${doc.niveau} ${doc.aufgaben_nummer} (${typLabel})`
}

View File

@@ -110,10 +110,9 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/compliance/einwilligungen',
oldAdminPage: '/admin/consent (Users Tab)',
status: 'partial',
status: 'connected',
},
priority: 'critical',
notes: 'Admin-View fuer Einwilligungen noch nicht implementiert'
},
{
id: 'dsr-requests',
@@ -135,10 +134,9 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/compliance/dsr',
oldAdminPage: '/admin/dsr',
status: 'not-connected'
status: 'connected'
},
priority: 'high',
notes: 'DSR-Modul im alten Admin vorhanden, noch nicht migriert'
},
{
id: 'dsms',
@@ -159,7 +157,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/compliance/dsms',
oldAdminPage: '/admin/dsms',
status: 'not-connected'
status: 'connected'
},
priority: 'medium'
},
@@ -191,6 +189,51 @@ export const MODULE_REGISTRY: BackendModule[] = [
// ===========================================
// AI MODULES
// ===========================================
{
id: 'ai-agents',
name: 'AI Agents',
description: 'Multi-Agent System Verwaltung und Monitoring',
category: 'ai',
backend: {
service: 'voice-service',
port: 8088,
basePath: '/api/v1/agents',
endpoints: [
{ path: '/sessions', method: 'GET', description: 'Agent-Sessions' },
{ path: '/statistics', method: 'GET', description: 'Agent-Statistiken' },
{ path: '/{agentId}', method: 'GET', description: 'Agent-Details' },
{ path: '/{agentId}/soul', method: 'GET', description: 'SOUL-Konfiguration' },
]
},
frontend: {
adminV2Page: '/ai/agents',
oldAdminPage: undefined,
status: 'connected'
},
priority: 'high',
notes: 'Neues Multi-Agent System'
},
{
id: 'ai-quality',
name: 'AI Quality (BQAS)',
description: 'KI-Qualitaetssicherung und Evaluierung',
category: 'ai',
backend: {
service: 'voice-service',
port: 8088,
basePath: '/api/bqas',
endpoints: [
{ path: '/evaluate', method: 'POST', description: 'Antwort evaluieren' },
{ path: '/metrics', method: 'GET', description: 'Qualitaetsmetriken' },
]
},
frontend: {
adminV2Page: '/ai/quality',
oldAdminPage: '/admin/quality',
status: 'connected'
},
priority: 'high'
},
{
id: 'llm-compare',
name: 'LLM Vergleich',
@@ -209,10 +252,35 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/ai/llm-compare',
oldAdminPage: '/admin/llm-compare',
status: 'not-connected'
status: 'connected'
},
priority: 'medium'
},
{
id: 'magic-help',
name: 'Magic Help (TrOCR)',
description: 'Handschrifterkennung mit TrOCR und LoRA Fine-Tuning',
category: 'ai',
backend: {
service: 'klausur-service',
port: 8086,
basePath: '/api/klausur/trocr',
endpoints: [
{ path: '/status', method: 'GET', description: 'TrOCR Status' },
{ path: '/extract', method: 'POST', description: 'Text aus Bild extrahieren' },
{ path: '/training/examples', method: 'GET', description: 'Trainingsbeispiele' },
{ path: '/training/add', method: 'POST', description: 'Trainingsbeispiel hinzufuegen' },
{ path: '/training/fine-tune', method: 'POST', description: 'Fine-Tuning starten' },
]
},
frontend: {
adminV2Page: '/ai/magic-help',
oldAdminPage: '/admin/magic-help',
status: 'connected'
},
priority: 'medium',
notes: 'Lokale Handschrifterkennung mit Privacy-by-Design'
},
{
id: 'klausur-korrektur',
name: 'Klausur-Korrektur',
@@ -278,7 +346,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/ai/rag',
oldAdminPage: '/admin/rag',
status: 'not-connected'
status: 'connected'
},
priority: 'medium'
},
@@ -304,7 +372,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/infrastructure/gpu',
oldAdminPage: '/admin/gpu',
status: 'not-connected'
status: 'connected'
},
priority: 'medium'
},
@@ -326,7 +394,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/infrastructure/security',
oldAdminPage: '/admin/security',
status: 'not-connected'
status: 'connected'
},
priority: 'high'
},
@@ -348,7 +416,50 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/infrastructure/sbom',
oldAdminPage: '/admin/sbom',
status: 'not-connected'
status: 'connected'
},
priority: 'medium'
},
{
id: 'middleware',
name: 'Middleware Manager',
description: 'Verwaltung und Monitoring der Backend-Middleware',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/middleware',
endpoints: [
{ path: '/status', method: 'GET', description: 'Middleware-Status' },
{ path: '/config', method: 'GET', description: 'Konfiguration' },
]
},
frontend: {
adminV2Page: '/infrastructure/middleware',
oldAdminPage: '/admin/middleware',
status: 'connected'
},
priority: 'medium'
},
{
id: 'ci-cd',
name: 'CI/CD Pipeline',
description: 'Build-Pipeline und Deployment-Management',
category: 'infrastructure',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/builds',
endpoints: [
{ path: '/pipelines', method: 'GET', description: 'Pipeline-Status' },
{ path: '/builds', method: 'GET', description: 'Build-Historie' },
]
},
frontend: {
adminV2Page: '/infrastructure/ci-cd',
oldAdminPage: '/admin/builds',
status: 'connected'
},
priority: 'medium'
},
@@ -356,11 +467,52 @@ export const MODULE_REGISTRY: BackendModule[] = [
// ===========================================
// EDUCATION MODULES
// ===========================================
// Note: school-directory module removed (no person/school data crawling)
{
id: 'edu-search',
name: 'Bildungssuche',
description: 'Suche nach Bildungsinhalten und Ressourcen',
category: 'education',
backend: {
service: 'edu-search-service',
port: 8089,
basePath: '/api/edu',
endpoints: [
{ path: '/search', method: 'GET', description: 'Bildungssuche' },
{ path: '/resources', method: 'GET', description: 'Ressourcen' },
]
},
frontend: {
adminV2Page: '/education/edu-search',
oldAdminPage: '/admin/edu-search',
status: 'connected'
},
priority: 'medium'
},
// ===========================================
// COMMUNICATION MODULES
// ===========================================
{
id: 'alerts',
name: 'Alerts & Benachrichtigungen',
description: 'System-Benachrichtigungen und Alerts',
category: 'communication',
backend: {
service: 'python-backend',
port: 8000,
basePath: '/api/alerts',
endpoints: [
{ path: '/notifications', method: 'GET', description: 'Benachrichtigungen' },
{ path: '/alerts', method: 'GET', description: 'Aktive Alerts' },
]
},
frontend: {
adminV2Page: '/communication/alerts',
oldAdminPage: '/admin/alerts',
status: 'connected'
},
priority: 'medium'
},
{
id: 'unified-inbox',
name: 'Unified Inbox',
@@ -379,7 +531,7 @@ export const MODULE_REGISTRY: BackendModule[] = [
frontend: {
adminV2Page: '/communication/mail',
oldAdminPage: '/admin/mail',
status: 'not-connected'
status: 'connected'
},
priority: 'low'
},

View File

@@ -5,7 +5,7 @@
* DSGVO (Datenschutz) and Compliance (Audit & GRC) are now separate
*/
export type CategoryId = 'dsgvo' | 'compliance' | 'ai' | 'infrastructure' | 'education' | 'communication' | 'development'
export type CategoryId = 'dsgvo' | 'compliance' | 'ai' | 'infrastructure' | 'education' | 'communication' | 'development' | 'sdk-docs'
export interface NavModule {
id: string
@@ -16,6 +16,7 @@ export interface NavModule {
audience: string[]
gdprArticles?: string[]
oldAdminPath?: string // Reference to old admin for migration
subgroup?: string // Optional subgroup for visual grouping in sidebar
}
export interface NavCategory {
@@ -195,6 +196,14 @@ export const navigation: NavCategory[] = [
gdprArticles: ['Art. 5 (Rechenschaftspflicht)', 'Art. 24 (Verantwortung)', 'Art. 39 (Aufgaben des DSB)'],
oldAdminPath: '/admin/docs/audit',
},
{
id: 'quality',
name: 'Qualitaet & Audit',
href: '/compliance/quality',
description: 'KI-Compliance & Traceability',
purpose: 'Stichproben und Traceability fuer Compliance-Auditoren. Chunk-Suche, Requirements und Controls fuer KI-Systeme.',
audience: ['Auditoren', 'Compliance-Beauftragte', 'QA'],
},
{
id: 'modules',
name: 'Service Registry',
@@ -261,58 +270,61 @@ export const navigation: NavCategory[] = [
colorClass: 'ai',
description: 'LLM, OCR, RAG & Machine Learning',
modules: [
// -----------------------------------------------------------------------
// KI-Daten-Pipeline: Magic Help ⟷ OCR → Indexierung → Suche
// -----------------------------------------------------------------------
{
id: 'llm-compare',
name: 'LLM Vergleich',
href: '/ai/llm-compare',
description: 'KI-Provider Vergleich',
purpose: 'Vergleichen Sie verschiedene LLM-Anbieter (Ollama, OpenAI, Anthropic) hinsichtlich Qualitaet, Geschwindigkeit und Kosten.',
audience: ['Entwickler', 'Data Scientists'],
oldAdminPath: '/admin/llm-compare',
},
{
id: 'rag',
name: 'Daten & RAG',
href: '/ai/rag',
description: 'Training Data & RAG Management',
purpose: 'Verwalten Sie Trainingsdaten und RAG-Pipelines fuer domainspezifische KI-Anwendungen.',
audience: ['Entwickler', 'Data Scientists'],
oldAdminPath: '/admin/rag',
id: 'magic-help',
name: 'Magic Help (TrOCR)',
href: '/ai/magic-help',
description: 'TrOCR Testing & Fine-Tuning',
purpose: 'Testen und verbessern Sie die TrOCR-Handschrifterkennung. Laden Sie Bilder hoch, um die OCR-Qualitaet zu pruefen, und trainieren Sie das Modell mit LoRA Fine-Tuning. Bidirektionaler Austausch mit OCR-Labeling.',
audience: ['Entwickler', 'Administratoren', 'QA'],
oldAdminPath: '/admin/magic-help',
subgroup: 'KI-Daten-Pipeline',
},
{
id: 'ocr-labeling',
name: 'OCR-Labeling',
href: '/ai/ocr-labeling',
description: 'Handschrift-Training & Labels',
purpose: 'Labeln Sie Handschrift-Samples fuer das Training von TrOCR-Modellen.',
audience: ['Entwickler'],
purpose: 'Labeln Sie Handschrift-Samples fuer das Training von TrOCR-Modellen. Erstellen Sie Ground Truth Daten, die zur RAG Pipeline exportiert werden koennen.',
audience: ['Entwickler', 'Data Scientists', 'QA'],
oldAdminPath: '/admin/ocr-labeling',
subgroup: 'KI-Daten-Pipeline',
},
{
id: 'magic-help',
name: 'Magic Help (TrOCR)',
href: '/ai/magic-help',
description: 'Handschrift-OCR',
purpose: 'Testen und optimieren Sie die Handschrift-Erkennung fuer Schuelerarbeiten.',
audience: ['Entwickler'],
oldAdminPath: '/admin/magic-help',
id: 'rag-pipeline',
name: 'RAG Pipeline',
href: '/ai/rag-pipeline',
description: 'Dokument-Indexierung',
purpose: 'RAG-Pipeline fuer Bildungsdokumente: NiBiS Erwartungshorizonte, Schulordnungen, Custom EH. OCR, Chunking und Vektor-Indexierung in Qdrant.',
audience: ['Entwickler', 'Data Scientists', 'Bildungs-Admins'],
oldAdminPath: '/admin/training',
subgroup: 'KI-Daten-Pipeline',
},
{
id: 'klausur-korrektur',
name: 'Klausur-Korrektur',
href: '/ai/klausur-korrektur',
description: 'Abitur-Korrektur mit KI',
purpose: 'KI-gestuetzte Korrektur von Abitur- und Vorabitur-Klausuren.',
audience: ['Lehrer', 'Entwickler'],
oldAdminPath: '/admin/klausur-korrektur',
id: 'rag',
name: 'Daten & RAG',
href: '/ai/rag',
description: 'Vektor-Suche & Collections',
purpose: 'Verwalten und durchsuchen Sie indexierte Dokumente. Zeigt Status aller Qdrant Collections und ermoeglicht semantische Suche.',
audience: ['Entwickler', 'Data Scientists', 'Compliance Officer'],
oldAdminPath: '/admin/rag',
subgroup: 'KI-Daten-Pipeline',
},
// -----------------------------------------------------------------------
// KI-Werkzeuge: Standalone-Tools fuer Entwicklung & QA
// -----------------------------------------------------------------------
{
id: 'quality',
name: 'Qualitaet & Audit',
href: '/ai/quality',
description: 'Compliance-Audit & Traceability',
purpose: 'Stichproben und Traceability fuer Compliance-Auditoren. Chunk-Suche, Requirements und Controls.',
audience: ['Auditoren', 'Compliance-Beauftragte', 'QA'],
id: 'llm-compare',
name: 'LLM Vergleich',
href: '/ai/llm-compare',
description: 'KI-Provider Vergleich',
purpose: 'Vergleichen Sie verschiedene LLM-Anbieter (Ollama, OpenAI, Anthropic) hinsichtlich Qualitaet, Geschwindigkeit und Kosten. Standalone-Werkzeug fuer Modell-Evaluation.',
audience: ['Entwickler', 'Data Scientists'],
oldAdminPath: '/admin/llm-compare',
subgroup: 'KI-Werkzeuge',
},
{
id: 'test-quality',
@@ -322,7 +334,21 @@ export const navigation: NavCategory[] = [
purpose: 'BQAS Dashboard mit Golden Suite (97 Referenz-Tests), RAG/Korrektur Tests und Synthetic Test Generierung. Ueberwacht die Qualitaet der KI-Ausgaben.',
audience: ['Entwickler', 'Data Scientists', 'QA'],
oldAdminPath: '/admin/quality',
subgroup: 'KI-Werkzeuge',
},
{
id: 'gpu',
name: 'GPU Infrastruktur',
href: '/ai/gpu',
description: 'vast.ai GPU Management',
purpose: 'Verwalten Sie GPU-Instanzen auf vast.ai fuer ML-Training und Inferenz.',
audience: ['DevOps', 'Entwickler'],
oldAdminPath: '/admin/gpu',
subgroup: 'KI-Werkzeuge',
},
// -----------------------------------------------------------------------
// KI-Anwendungen: Endnutzer-orientierte KI-Module
// -----------------------------------------------------------------------
{
id: 'agents',
name: 'Agent Management',
@@ -330,6 +356,7 @@ export const navigation: NavCategory[] = [
description: 'Multi-Agent System & SOUL-Editor',
purpose: 'Verwaltung des Multi-Agent-Systems. Bearbeiten Sie Agent-Persoenlichkeiten (SOUL-Files), ueberwachen Sie Sessions und analysieren Sie Agent-Statistiken. Architektur-Dokumentation fuer Entwickler.',
audience: ['Entwickler', 'Lehrer', 'Admins'],
subgroup: 'KI-Anwendungen',
},
],
},
@@ -344,32 +371,24 @@ export const navigation: NavCategory[] = [
colorClass: 'infrastructure',
description: 'GPU, Security, CI/CD & Monitoring',
modules: [
// DevOps Pipeline Group (CI/CD -> Tests -> SBOM -> Security)
{
id: 'gpu',
name: 'GPU Infrastruktur',
href: '/infrastructure/gpu',
description: 'vast.ai GPU Management',
purpose: 'Verwalten Sie GPU-Instanzen auf vast.ai fuer ML-Training und Inferenz.',
id: 'ci-cd',
name: 'CI/CD',
href: '/infrastructure/ci-cd',
description: 'Pipelines, Deployments & Container',
purpose: 'CI/CD Dashboard mit Gitea Actions Pipelines, Deployment-Status und Container-Management.',
audience: ['DevOps', 'Entwickler'],
oldAdminPath: '/admin/gpu',
subgroup: 'DevOps Pipeline',
},
{
id: 'middleware',
name: 'Middleware',
href: '/infrastructure/middleware',
description: 'Middleware Stack & API Gateway',
purpose: 'Ueberwachen und testen Sie den Middleware-Stack und API Gateway.',
audience: ['DevOps'],
oldAdminPath: '/admin/middleware',
},
{
id: 'security',
name: 'Security',
href: '/infrastructure/security',
description: 'DevSecOps Dashboard & Scans',
purpose: 'Security-Scans, Vulnerability-Reports und OWASP-Compliance.',
audience: ['DevOps', 'Security'],
oldAdminPath: '/admin/security',
id: 'tests',
name: 'Test Dashboard',
href: '/infrastructure/tests',
description: 'Test-Suites, Coverage & CI/CD',
purpose: 'Zentrales Dashboard fuer alle 280+ Tests. Unit (Go, Python), Integration, E2E (Playwright) und BQAS Quality Tests. Aggregiert Tests aus allen Services ohne physische Migration.',
audience: ['Entwickler', 'QA', 'DevOps'],
subgroup: 'DevOps Pipeline',
},
{
id: 'sbom',
@@ -379,22 +398,28 @@ export const navigation: NavCategory[] = [
purpose: 'Verwalten Sie alle Software-Abhaengigkeiten und deren Lizenzen.',
audience: ['DevOps', 'Compliance'],
oldAdminPath: '/admin/sbom',
subgroup: 'DevOps Pipeline',
},
{
id: 'ci-cd',
name: 'CI/CD',
href: '/infrastructure/ci-cd',
description: 'Pipelines, Deployments & Container',
purpose: 'CI/CD Dashboard mit Gitea Actions Pipelines, Deployment-Status und Container-Management.',
audience: ['DevOps', 'Entwickler'],
id: 'security',
name: 'Security',
href: '/infrastructure/security',
description: 'DevSecOps Dashboard & Scans',
purpose: 'Security-Scans, Vulnerability-Reports und OWASP-Compliance.',
audience: ['DevOps', 'Security'],
oldAdminPath: '/admin/security',
subgroup: 'DevOps Pipeline',
},
// Infrastructure Group
{
id: 'tests',
name: 'Test Dashboard',
href: '/infrastructure/tests',
description: 'Test-Suites, Coverage & CI/CD',
purpose: 'Zentrales Dashboard fuer alle 195+ Tests. Unit (Go, Python), Integration, E2E (Playwright) und BQAS Quality Tests. Aggregiert Tests aus allen Services ohne physische Migration.',
audience: ['Entwickler', 'QA', 'DevOps'],
id: 'middleware',
name: 'Middleware',
href: '/infrastructure/middleware',
description: 'Middleware Stack & API Gateway',
purpose: 'Ueberwachen und testen Sie den Middleware-Stack und API Gateway.',
audience: ['DevOps'],
oldAdminPath: '/admin/middleware',
subgroup: 'Infrastructure',
},
],
},
@@ -427,15 +452,6 @@ export const navigation: NavCategory[] = [
audience: ['Entwickler'],
oldAdminPath: '/admin/zeugnisse-crawler',
},
{
id: 'training',
name: 'Training',
href: '/education/training',
description: 'Schulungsmodule',
purpose: 'Verwalten Sie Schulungsmodule fuer Lehrer und Admins.',
audience: ['Bildungs-Admins'],
oldAdminPath: '/admin/training',
},
{
id: 'foerderantrag',
name: 'Foerderantrag-Wizard',
@@ -444,6 +460,32 @@ export const navigation: NavCategory[] = [
purpose: '8-Schritt-Wizard fuer Schulfoerderantraege. Erstellt antragsfaehige Dokumente (Antragsschreiben, Kostenplan, Datenschutzkonzept) mit KI-Unterstuetzung. BreakPilot-Presets fuer schnellen Start.',
audience: ['Schulleitung', 'IT-Beauftragte', 'Schultraeger'],
},
{
id: 'abitur-archiv',
name: 'Abitur-Archiv',
href: '/education/abitur-archiv',
description: 'Zentralabitur-Materialien 2021-2025',
purpose: 'Durchsuchen und filtern Sie Abitur-Aufgaben und Erwartungshorizonte. Themensuche mit semantischer Suche via RAG. Integration mit Klausur-Korrektur fuer schnelle Vorlagen-Nutzung.',
audience: ['Lehrer', 'Entwickler'],
},
{
id: 'klausur-korrektur',
name: 'Klausur-Korrektur',
href: '/education/klausur-korrektur',
description: 'Abitur-Korrektur mit KI',
purpose: 'KI-gestuetzte Korrektur von Abitur- und Vorabitur-Klausuren. Nutzt die RAG-Pipeline fuer Erwartungshorizont-Vorschlaege.',
audience: ['Lehrer', 'Entwickler'],
oldAdminPath: '/admin/klausur-korrektur',
},
{
id: 'companion',
name: 'Companion',
href: '/education/companion',
description: 'Unterrichts-Timer & Phasen',
purpose: 'Strukturierter Unterricht mit 5-Phasen-Modell (E-A-S-T-R). Visual Timer, Hausaufgaben-Tracking und Reflexion.',
audience: ['Lehrer'],
oldAdminPath: '/admin/companion',
},
],
},
// =========================================================================
@@ -514,15 +556,6 @@ export const navigation: NavCategory[] = [
purpose: 'Entwicklungs-Workflow mit Git, CI/CD Pipeline und Team-Konventionen. Pflichtlektuere fuer alle Entwickler.',
audience: ['Entwickler', 'DevOps'],
},
{
id: 'voice',
name: 'Voice Service (Moved)',
href: '/communication/matrix',
description: 'Verschoben nach Kommunikation',
purpose: 'Der Voice Service wurde nach /communication/matrix verschoben.',
audience: ['Entwickler'],
oldAdminPath: '/admin/voice',
},
{
id: 'game',
name: 'Breakpilot Drive',
@@ -541,15 +574,6 @@ export const navigation: NavCategory[] = [
audience: ['Entwickler'],
oldAdminPath: '/admin/unity-bridge',
},
{
id: 'companion',
name: 'Companion Dev',
href: '/development/companion',
description: 'Lesson-Modus Entwicklung',
purpose: 'Entwickeln Sie den Companion-Modus fuer strukturiertes Lernen.',
audience: ['Entwickler'],
oldAdminPath: '/admin/companion',
},
{
id: 'docs',
name: 'Developer Docs',
@@ -588,6 +612,69 @@ export const navigation: NavCategory[] = [
},
],
},
// =========================================================================
// SDK Dokumentation
// =========================================================================
{
id: 'sdk-docs',
name: 'SDK Dokumentation',
icon: 'code-2',
color: '#06b6d4', // Cyan
colorClass: 'sdk-docs',
description: 'Consent SDK Dokumentation & Integration',
modules: [
{
id: 'consent-sdk',
name: 'Consent SDK',
href: '/developers/sdk/consent',
description: 'DSGVO/TTDSG-konformes Consent Management',
purpose: 'Vollstaendige Dokumentation des Consent SDK fuer Web, PWA und Mobile Apps. Inklusive Framework-Integrationen (React, Vue, Angular) und Mobile SDKs (iOS, Android, Flutter).',
audience: ['Entwickler', 'Frontend-Entwickler', 'Mobile-Entwickler'],
gdprArticles: ['Art. 6', 'Art. 7', 'Art. 13', 'Art. 14', 'Art. 17', 'Art. 20'],
},
{
id: 'sdk-installation',
name: 'Installation',
href: '/developers/sdk/consent/installation',
description: 'SDK Installation & Setup',
purpose: 'Schritt-fuer-Schritt Anleitung zur Installation des Consent SDK in verschiedenen Umgebungen.',
audience: ['Entwickler'],
},
{
id: 'sdk-frameworks',
name: 'Frameworks',
href: '/developers/sdk/consent/frameworks',
description: 'React, Vue, Angular Integration',
purpose: 'Framework-spezifische Integrationen mit Hooks, Composables und Services.',
audience: ['Frontend-Entwickler'],
},
{
id: 'sdk-mobile',
name: 'Mobile SDKs',
href: '/developers/sdk/consent/mobile',
description: 'iOS, Android, Flutter',
purpose: 'Native Mobile SDKs fuer iOS (Swift), Android (Kotlin) und Flutter (Dart).',
audience: ['Mobile-Entwickler'],
},
{
id: 'sdk-api',
name: 'API Referenz',
href: '/developers/sdk/consent/api-reference',
description: 'Vollstaendige API-Dokumentation',
purpose: 'Detaillierte Dokumentation aller Methoden, Konfigurationsoptionen und Events.',
audience: ['Entwickler'],
},
{
id: 'sdk-security',
name: 'Sicherheit',
href: '/developers/sdk/consent/security',
description: 'Security Best Practices',
purpose: 'Sicherheits-Features, DSGVO/TTDSG Compliance-Hinweise und Best Practices.',
audience: ['Entwickler', 'DSB', 'Security'],
gdprArticles: ['Art. 6', 'Art. 7', '§ 25 TTDSG'],
},
],
},
]
// Meta modules (always visible)

View File

@@ -11,21 +11,13 @@ import {
Risk,
Control,
UserPreferences,
CustomerType,
CompanyProfile,
ImportedDocument,
GapAnalysis,
SDKPackageId,
SDK_STEPS,
SDK_PACKAGES,
getStepById,
getStepByUrl,
getNextStep,
getPreviousStep,
getCompletionPercentage,
getPhaseCompletionPercentage,
getPackageCompletionPercentage,
getStepsForPackage,
} from './types'
import { exportToPDF, exportToZIP } from './export'
import { SDKApiClient, getSDKApiClient, resetSDKApiClient } from './api-client'
@@ -56,22 +48,12 @@ const initialState: SDKState = {
userId: '',
subscription: 'PROFESSIONAL',
// Customer Type
customerType: null,
// Company Profile
companyProfile: null,
// Progress
currentPhase: 1,
currentStep: 'company-profile',
currentStep: 'use-case-workshop',
completedSteps: [],
checkpoints: {},
// Imported Documents (for existing customers)
importedDocuments: [],
gapAnalysis: null,
// Phase 1 Data
useCases: [],
activeUseCase: null,
@@ -166,39 +148,6 @@ function sdkReducer(state: SDKState, action: ExtendedSDKAction): SDKState {
},
})
case 'SET_CUSTOMER_TYPE':
return updateState({ customerType: action.payload })
case 'SET_COMPANY_PROFILE':
return updateState({ companyProfile: action.payload })
case 'UPDATE_COMPANY_PROFILE':
return updateState({
companyProfile: state.companyProfile
? { ...state.companyProfile, ...action.payload }
: null,
})
case 'ADD_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: [...state.importedDocuments, action.payload],
})
case 'UPDATE_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: state.importedDocuments.map(doc =>
doc.id === action.payload.id ? { ...doc, ...action.payload.data } : doc
),
})
case 'DELETE_IMPORTED_DOCUMENT':
return updateState({
importedDocuments: state.importedDocuments.filter(doc => doc.id !== action.payload),
})
case 'SET_GAP_ANALYSIS':
return updateState({ gapAnalysis: action.payload })
case 'ADD_USE_CASE':
return updateState({
useCases: [...state.useCases, action.payload],
@@ -439,18 +388,6 @@ interface SDKContextValue {
completionPercentage: number
phase1Completion: number
phase2Completion: number
packageCompletion: Record<SDKPackageId, number>
// Customer Type
setCustomerType: (type: CustomerType) => void
// Company Profile
setCompanyProfile: (profile: CompanyProfile) => void
updateCompanyProfile: (updates: Partial<CompanyProfile>) => void
// Import (for existing customers)
addImportedDocument: (doc: ImportedDocument) => void
setGapAnalysis: (analysis: GapAnalysis) => void
// Checkpoints
validateCheckpoint: (checkpointId: string) => Promise<CheckpointStatus>
@@ -714,42 +651,6 @@ export function SDKProvider({
const phase1Completion = useMemo(() => getPhaseCompletionPercentage(state, 1), [state])
const phase2Completion = useMemo(() => getPhaseCompletionPercentage(state, 2), [state])
// Package Completion
const packageCompletion = useMemo(() => {
const completion: Record<SDKPackageId, number> = {
'vorbereitung': getPackageCompletionPercentage(state, 'vorbereitung'),
'analyse': getPackageCompletionPercentage(state, 'analyse'),
'dokumentation': getPackageCompletionPercentage(state, 'dokumentation'),
'rechtliche-texte': getPackageCompletionPercentage(state, 'rechtliche-texte'),
'betrieb': getPackageCompletionPercentage(state, 'betrieb'),
}
return completion
}, [state])
// Customer Type
const setCustomerType = useCallback((type: CustomerType) => {
dispatch({ type: 'SET_CUSTOMER_TYPE', payload: type })
}, [])
// Company Profile
const setCompanyProfile = useCallback((profile: CompanyProfile) => {
dispatch({ type: 'SET_COMPANY_PROFILE', payload: profile })
}, [])
const updateCompanyProfile = useCallback((updates: Partial<CompanyProfile>) => {
dispatch({ type: 'UPDATE_COMPANY_PROFILE', payload: updates })
}, [])
// Import Document
const addImportedDocument = useCallback((doc: ImportedDocument) => {
dispatch({ type: 'ADD_IMPORTED_DOCUMENT', payload: doc })
}, [])
// Gap Analysis
const setGapAnalysis = useCallback((analysis: GapAnalysis) => {
dispatch({ type: 'SET_GAP_ANALYSIS', payload: analysis })
}, [])
// Checkpoints
const validateCheckpoint = useCallback(
async (checkpointId: string): Promise<CheckpointStatus> => {
@@ -783,25 +684,13 @@ export function SDKProvider({
}
switch (checkpointId) {
case 'CP-PROF':
if (!state.companyProfile || !state.companyProfile.isComplete) {
status.passed = false
status.errors.push({
ruleId: 'prof-complete',
field: 'companyProfile',
message: 'Unternehmensprofil muss vollständig ausgefüllt werden',
severity: 'ERROR',
})
}
break
case 'CP-UC':
if (state.useCases.length === 0) {
status.passed = false
status.errors.push({
ruleId: 'uc-min-count',
field: 'useCases',
message: 'Mindestens ein Anwendungsfall muss erstellt werden',
message: 'Mindestens ein Use Case muss erstellt werden',
severity: 'ERROR',
})
}
@@ -1036,12 +925,6 @@ export function SDKProvider({
completionPercentage,
phase1Completion,
phase2Completion,
packageCompletion,
setCustomerType,
setCompanyProfile,
updateCompanyProfile,
addImportedDocument,
setGapAnalysis,
validateCheckpoint,
overrideCheckpoint,
getCheckpointStatus,

View File

@@ -56,44 +56,11 @@ export function generateDemoState(tenantId: string, userId: string): Partial<SDK
userId,
subscription: 'PROFESSIONAL',
// Customer Type
customerType: 'new',
// Company Profile (Demo: TechStart GmbH - SaaS-Startup aus Berlin)
companyProfile: {
companyName: 'TechStart GmbH',
legalForm: 'gmbh',
industry: 'Technologie / IT',
foundedYear: 2022,
businessModel: 'B2B_B2C',
offerings: ['app_web', 'software_saas', 'services_consulting'],
companySize: 'small',
employeeCount: '10-49',
annualRevenue: '2-10 Mio',
headquartersCountry: 'DE',
headquartersCity: 'Berlin',
hasInternationalLocations: false,
internationalCountries: [],
targetMarkets: ['germany_only', 'dach'],
primaryJurisdiction: 'DE',
isDataController: true,
isDataProcessor: true,
usesAI: true,
aiUseCases: ['KI-gestützte Kundenberatung', 'Automatisierte Dokumentenanalyse'],
dpoName: 'Max Mustermann',
dpoEmail: 'dsb@techstart.de',
legalContactName: null,
legalContactEmail: null,
isComplete: true,
completedAt: new Date('2026-01-14'),
},
// Progress - showing a realistic partially completed workflow
currentPhase: 2,
currentStep: 'tom',
completedSteps: [
'company-profile',
'use-case-assessment',
'use-case-workshop',
'screening',
'modules',
'requirements',
@@ -106,7 +73,6 @@ export function generateDemoState(tenantId: string, userId: string): Partial<SDK
'dsfa',
],
checkpoints: {
'CP-PROF': { checkpointId: 'CP-PROF', passed: true, validatedAt: new Date('2026-01-14'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-UC': { checkpointId: 'CP-UC', passed: true, validatedAt: new Date('2026-01-15'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-SCAN': { checkpointId: 'CP-SCAN', passed: true, validatedAt: new Date('2026-01-16'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-MOD': { checkpointId: 'CP-MOD', passed: true, validatedAt: new Date('2026-01-17'), validatedBy: 'demo-user', errors: [], warnings: [] },

View File

@@ -0,0 +1,355 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'
// Mock fetch globally
const mockFetch = vi.fn()
global.fetch = mockFetch
describe('DSFA API Client', () => {
beforeEach(() => {
mockFetch.mockClear()
})
afterEach(() => {
vi.restoreAllMocks()
})
describe('listDSFAs', () => {
it('should fetch DSFAs without status filter', async () => {
const mockDSFAs = [
{ id: 'dsfa-1', name: 'Test DSFA 1', status: 'draft' },
{ id: 'dsfa-2', name: 'Test DSFA 2', status: 'approved' },
]
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ dsfas: mockDSFAs }),
})
const { listDSFAs } = await import('../api')
const result = await listDSFAs()
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result).toHaveLength(2)
expect(result[0].name).toBe('Test DSFA 1')
})
it('should fetch DSFAs with status filter', async () => {
const mockDSFAs = [{ id: 'dsfa-1', name: 'Draft DSFA', status: 'draft' }]
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ dsfas: mockDSFAs }),
})
const { listDSFAs } = await import('../api')
const result = await listDSFAs('draft')
expect(mockFetch).toHaveBeenCalledTimes(1)
const calledUrl = mockFetch.mock.calls[0][0]
expect(calledUrl).toContain('status=draft')
})
it('should return empty array when no DSFAs', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ dsfas: null }),
})
const { listDSFAs } = await import('../api')
const result = await listDSFAs()
expect(result).toEqual([])
})
})
describe('getDSFA', () => {
it('should fetch a single DSFA by ID', async () => {
const mockDSFA = {
id: 'dsfa-123',
name: 'Test DSFA',
status: 'draft',
risks: [],
mitigations: [],
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(mockDSFA),
})
const { getDSFA } = await import('../api')
const result = await getDSFA('dsfa-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.id).toBe('dsfa-123')
expect(result.name).toBe('Test DSFA')
})
it('should throw error for non-existent DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: () => Promise.resolve('{"error": "DSFA not found"}'),
})
const { getDSFA } = await import('../api')
await expect(getDSFA('non-existent')).rejects.toThrow()
})
})
describe('createDSFA', () => {
it('should create a new DSFA', async () => {
const newDSFA = {
name: 'New DSFA',
description: 'Test description',
processing_purpose: 'Testing',
}
const createdDSFA = {
id: 'dsfa-new',
...newDSFA,
status: 'draft',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(createdDSFA),
})
const { createDSFA } = await import('../api')
const result = await createDSFA(newDSFA)
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.id).toBe('dsfa-new')
expect(result.name).toBe('New DSFA')
})
})
describe('updateDSFA', () => {
it('should update an existing DSFA', async () => {
const updates = {
name: 'Updated DSFA Name',
processing_purpose: 'Updated purpose',
}
const updatedDSFA = {
id: 'dsfa-123',
...updates,
status: 'draft',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(updatedDSFA),
})
const { updateDSFA } = await import('../api')
const result = await updateDSFA('dsfa-123', updates)
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.name).toBe('Updated DSFA Name')
})
})
describe('deleteDSFA', () => {
it('should delete a DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
})
const { deleteDSFA } = await import('../api')
await deleteDSFA('dsfa-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
const calledConfig = mockFetch.mock.calls[0][1]
expect(calledConfig.method).toBe('DELETE')
})
it('should throw error when deletion fails', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
statusText: 'Not Found',
})
const { deleteDSFA } = await import('../api')
await expect(deleteDSFA('non-existent')).rejects.toThrow()
})
})
describe('updateDSFASection', () => {
it('should update a specific section', async () => {
const sectionData = {
processing_purpose: 'Updated purpose',
data_categories: ['personal_data', 'contact_data'],
}
const updatedDSFA = {
id: 'dsfa-123',
section_progress: {
section_1_complete: true,
},
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(updatedDSFA),
})
const { updateDSFASection } = await import('../api')
const result = await updateDSFASection('dsfa-123', 1, sectionData)
expect(mockFetch).toHaveBeenCalledTimes(1)
const calledUrl = mockFetch.mock.calls[0][0]
expect(calledUrl).toContain('/sections/1')
})
})
describe('submitDSFAForReview', () => {
it('should submit DSFA for review', async () => {
const response = {
message: 'DSFA submitted for review',
dsfa: {
id: 'dsfa-123',
status: 'in_review',
},
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(response),
})
const { submitDSFAForReview } = await import('../api')
const result = await submitDSFAForReview('dsfa-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.dsfa.status).toBe('in_review')
})
})
describe('approveDSFA', () => {
it('should approve a DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ message: 'DSFA approved' }),
})
const { approveDSFA } = await import('../api')
const result = await approveDSFA('dsfa-123', {
dpo_opinion: 'Approved after review',
approved: true,
})
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.message).toBe('DSFA approved')
})
it('should reject a DSFA', async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve({ message: 'DSFA rejected' }),
})
const { approveDSFA } = await import('../api')
const result = await approveDSFA('dsfa-123', {
dpo_opinion: 'Needs more details',
approved: false,
})
expect(result.message).toBe('DSFA rejected')
})
})
describe('getDSFAStats', () => {
it('should fetch DSFA statistics', async () => {
const stats = {
total: 10,
status_stats: {
draft: 4,
in_review: 2,
approved: 3,
rejected: 1,
},
risk_stats: {
low: 3,
medium: 4,
high: 2,
very_high: 1,
},
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(stats),
})
const { getDSFAStats } = await import('../api')
const result = await getDSFAStats()
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.total).toBe(10)
expect(result.status_stats.approved).toBe(3)
})
})
describe('createDSFAFromAssessment', () => {
it('should create DSFA from UCCA assessment', async () => {
const response = {
dsfa: {
id: 'dsfa-new',
name: 'AI Chatbot DSFA',
status: 'draft',
},
prefilled: true,
message: 'DSFA created from assessment',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(response),
})
const { createDSFAFromAssessment } = await import('../api')
const result = await createDSFAFromAssessment('assessment-123')
expect(mockFetch).toHaveBeenCalledTimes(1)
expect(result.prefilled).toBe(true)
expect(result.dsfa.id).toBe('dsfa-new')
})
})
describe('getDSFAByAssessment', () => {
it('should return DSFA linked to assessment', async () => {
const dsfa = {
id: 'dsfa-123',
assessment_id: 'assessment-123',
name: 'Linked DSFA',
}
mockFetch.mockResolvedValueOnce({
ok: true,
json: () => Promise.resolve(dsfa),
})
const { getDSFAByAssessment } = await import('../api')
const result = await getDSFAByAssessment('assessment-123')
expect(result?.id).toBe('dsfa-123')
})
it('should return null when no DSFA exists for assessment', async () => {
mockFetch.mockResolvedValueOnce({
ok: false,
status: 404,
text: () => Promise.resolve('Not found'),
})
const { getDSFAByAssessment } = await import('../api')
const result = await getDSFAByAssessment('no-dsfa-assessment')
expect(result).toBeNull()
})
})
})

View File

@@ -0,0 +1,255 @@
import { describe, it, expect } from 'vitest'
import {
calculateRiskLevel,
DSFA_SECTIONS,
DSFA_STATUS_LABELS,
DSFA_RISK_LEVEL_LABELS,
DSFA_LEGAL_BASES,
DSFA_AFFECTED_RIGHTS,
RISK_MATRIX,
type DSFARisk,
type DSFAMitigation,
type DSFASectionProgress,
type DSFA,
} from '../types'
describe('DSFA_SECTIONS', () => {
it('should have 5 sections defined', () => {
expect(DSFA_SECTIONS.length).toBe(5)
})
it('should have sections numbered 1-5', () => {
const numbers = DSFA_SECTIONS.map(s => s.number)
expect(numbers).toEqual([1, 2, 3, 4, 5])
})
it('should have GDPR references for all sections', () => {
DSFA_SECTIONS.forEach(section => {
expect(section.gdprRef).toBeDefined()
expect(section.gdprRef).toContain('Art. 35')
})
})
it('should mark first 4 sections as required', () => {
const requiredSections = DSFA_SECTIONS.filter(s => s.required)
expect(requiredSections.length).toBe(4)
expect(requiredSections.map(s => s.number)).toEqual([1, 2, 3, 4])
})
it('should mark section 5 as optional', () => {
const section5 = DSFA_SECTIONS.find(s => s.number === 5)
expect(section5?.required).toBe(false)
})
it('should have German titles for all sections', () => {
DSFA_SECTIONS.forEach(section => {
expect(section.titleDE).toBeDefined()
expect(section.titleDE.length).toBeGreaterThan(0)
})
})
})
describe('DSFA_STATUS_LABELS', () => {
it('should have all status labels defined', () => {
expect(DSFA_STATUS_LABELS.draft).toBe('Entwurf')
expect(DSFA_STATUS_LABELS.in_review).toBe('In Prüfung')
expect(DSFA_STATUS_LABELS.approved).toBe('Genehmigt')
expect(DSFA_STATUS_LABELS.rejected).toBe('Abgelehnt')
expect(DSFA_STATUS_LABELS.needs_update).toBe('Überarbeitung erforderlich')
})
})
describe('DSFA_RISK_LEVEL_LABELS', () => {
it('should have all risk level labels defined', () => {
expect(DSFA_RISK_LEVEL_LABELS.low).toBe('Niedrig')
expect(DSFA_RISK_LEVEL_LABELS.medium).toBe('Mittel')
expect(DSFA_RISK_LEVEL_LABELS.high).toBe('Hoch')
expect(DSFA_RISK_LEVEL_LABELS.very_high).toBe('Sehr Hoch')
})
})
describe('DSFA_LEGAL_BASES', () => {
it('should have 6 legal bases defined', () => {
expect(Object.keys(DSFA_LEGAL_BASES).length).toBe(6)
})
it('should reference GDPR Article 6', () => {
Object.values(DSFA_LEGAL_BASES).forEach(label => {
expect(label).toContain('Art. 6')
})
})
})
describe('DSFA_AFFECTED_RIGHTS', () => {
it('should have multiple affected rights defined', () => {
expect(DSFA_AFFECTED_RIGHTS.length).toBeGreaterThan(5)
})
it('should have id and label for each right', () => {
DSFA_AFFECTED_RIGHTS.forEach(right => {
expect(right.id).toBeDefined()
expect(right.label).toBeDefined()
})
})
it('should include GDPR data subject rights', () => {
const ids = DSFA_AFFECTED_RIGHTS.map(r => r.id)
expect(ids).toContain('right_of_access')
expect(ids).toContain('right_to_erasure')
expect(ids).toContain('right_to_data_portability')
})
})
describe('RISK_MATRIX', () => {
it('should have 9 cells defined (3x3 matrix)', () => {
expect(RISK_MATRIX.length).toBe(9)
})
it('should cover all combinations of likelihood and impact', () => {
const likelihoodValues = ['low', 'medium', 'high']
const impactValues = ['low', 'medium', 'high']
likelihoodValues.forEach(likelihood => {
impactValues.forEach(impact => {
const cell = RISK_MATRIX.find(
c => c.likelihood === likelihood && c.impact === impact
)
expect(cell).toBeDefined()
})
})
})
it('should have increasing scores for higher risks', () => {
const lowLow = RISK_MATRIX.find(c => c.likelihood === 'low' && c.impact === 'low')
const highHigh = RISK_MATRIX.find(c => c.likelihood === 'high' && c.impact === 'high')
expect(lowLow?.score).toBeLessThan(highHigh?.score || 0)
})
})
describe('calculateRiskLevel', () => {
it('should return low for low likelihood and low impact', () => {
const result = calculateRiskLevel('low', 'low')
expect(result.level).toBe('low')
expect(result.score).toBe(10)
})
it('should return very_high for high likelihood and high impact', () => {
const result = calculateRiskLevel('high', 'high')
expect(result.level).toBe('very_high')
expect(result.score).toBe(90)
})
it('should return medium for medium likelihood and medium impact', () => {
const result = calculateRiskLevel('medium', 'medium')
expect(result.level).toBe('medium')
expect(result.score).toBe(50)
})
it('should return high for high likelihood and medium impact', () => {
const result = calculateRiskLevel('high', 'medium')
expect(result.level).toBe('high')
expect(result.score).toBe(70)
})
it('should return medium for low likelihood and high impact', () => {
const result = calculateRiskLevel('low', 'high')
expect(result.level).toBe('medium')
expect(result.score).toBe(40)
})
})
describe('DSFARisk type', () => {
it('should accept valid risk data', () => {
const risk: DSFARisk = {
id: 'risk-001',
category: 'confidentiality',
description: 'Unauthorized access to personal data',
likelihood: 'medium',
impact: 'high',
risk_level: 'high',
affected_data: ['customer_data', 'financial_data'],
}
expect(risk.id).toBe('risk-001')
expect(risk.category).toBe('confidentiality')
expect(risk.likelihood).toBe('medium')
expect(risk.impact).toBe('high')
})
})
describe('DSFAMitigation type', () => {
it('should accept valid mitigation data', () => {
const mitigation: DSFAMitigation = {
id: 'mit-001',
risk_id: 'risk-001',
description: 'Implement encryption at rest',
type: 'technical',
status: 'implemented',
residual_risk: 'low',
responsible_party: 'IT Security Team',
}
expect(mitigation.id).toBe('mit-001')
expect(mitigation.type).toBe('technical')
expect(mitigation.status).toBe('implemented')
})
})
describe('DSFASectionProgress type', () => {
it('should track completion for all 5 sections', () => {
const progress: DSFASectionProgress = {
section_1_complete: true,
section_2_complete: true,
section_3_complete: false,
section_4_complete: false,
section_5_complete: false,
}
expect(progress.section_1_complete).toBe(true)
expect(progress.section_2_complete).toBe(true)
expect(progress.section_3_complete).toBe(false)
})
})
describe('DSFA type', () => {
it('should accept a complete DSFA object', () => {
const dsfa: DSFA = {
id: 'dsfa-001',
tenant_id: 'tenant-001',
name: 'AI Chatbot DSFA',
description: 'Data Protection Impact Assessment for AI Chatbot',
processing_description: 'Automated customer service using AI',
processing_purpose: 'Customer support automation',
data_categories: ['contact_data', 'inquiry_content'],
data_subjects: ['customers'],
recipients: ['internal_staff'],
legal_basis: 'legitimate_interest',
necessity_assessment: 'Required for efficient customer service',
proportionality_assessment: 'Minimal data processing for the purpose',
risks: [],
overall_risk_level: 'medium',
risk_score: 50,
mitigations: [],
dpo_consulted: false,
authority_consulted: false,
status: 'draft',
section_progress: {
section_1_complete: true,
section_2_complete: true,
section_3_complete: false,
section_4_complete: false,
section_5_complete: false,
},
conclusion: '',
created_at: new Date().toISOString(),
updated_at: new Date().toISOString(),
created_by: 'user-001',
}
expect(dsfa.id).toBe('dsfa-001')
expect(dsfa.name).toBe('AI Chatbot DSFA')
expect(dsfa.status).toBe('draft')
expect(dsfa.data_categories).toHaveLength(2)
})
})

View File

@@ -0,0 +1,399 @@
/**
* DSFA API Client
*
* API client functions for DSFA (Data Protection Impact Assessment) endpoints.
*/
import type {
DSFA,
DSFAListResponse,
DSFAStatsResponse,
CreateDSFARequest,
CreateDSFAFromAssessmentRequest,
CreateDSFAFromAssessmentResponse,
UpdateDSFASectionRequest,
SubmitForReviewResponse,
ApproveDSFARequest,
DSFATriggerInfo,
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const getBaseUrl = () => {
if (typeof window !== 'undefined') {
// Browser environment
return process.env.NEXT_PUBLIC_SDK_API_URL || '/api/sdk/v1'
}
// Server environment
return process.env.SDK_API_URL || 'http://localhost:8080/api/sdk/v1'
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
async function handleResponse<T>(response: Response): Promise<T> {
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep HTTP status message
}
throw new Error(errorMessage)
}
return response.json()
}
function getHeaders(): HeadersInit {
return {
'Content-Type': 'application/json',
}
}
// =============================================================================
// DSFA CRUD OPERATIONS
// =============================================================================
/**
* List all DSFAs for the current tenant
*/
export async function listDSFAs(status?: string): Promise<DSFA[]> {
const url = new URL(`${getBaseUrl()}/dsgvo/dsfas`, window.location.origin)
if (status) {
url.searchParams.set('status', status)
}
const response = await fetch(url.toString(), {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
const data = await handleResponse<DSFAListResponse>(response)
return data.dsfas || []
}
/**
* Get a single DSFA by ID
*/
export async function getDSFA(id: string): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<DSFA>(response)
}
/**
* Create a new DSFA
*/
export async function createDSFA(data: CreateDSFARequest): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<DSFA>(response)
}
/**
* Update an existing DSFA
*/
export async function updateDSFA(id: string, data: Partial<DSFA>): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
method: 'PUT',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<DSFA>(response)
}
/**
* Delete a DSFA
*/
export async function deleteDSFA(id: string): Promise<void> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}`, {
method: 'DELETE',
headers: getHeaders(),
credentials: 'include',
})
if (!response.ok) {
throw new Error(`Failed to delete DSFA: ${response.statusText}`)
}
}
// =============================================================================
// DSFA SECTION OPERATIONS
// =============================================================================
/**
* Update a specific section of a DSFA
*/
export async function updateDSFASection(
id: string,
sectionNumber: number,
data: UpdateDSFASectionRequest
): Promise<DSFA> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/sections/${sectionNumber}`, {
method: 'PUT',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<DSFA>(response)
}
// =============================================================================
// DSFA WORKFLOW OPERATIONS
// =============================================================================
/**
* Submit a DSFA for DPO review
*/
export async function submitDSFAForReview(id: string): Promise<SubmitForReviewResponse> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/submit-for-review`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<SubmitForReviewResponse>(response)
}
/**
* Approve or reject a DSFA (DPO/CISO/GF action)
*/
export async function approveDSFA(id: string, data: ApproveDSFARequest): Promise<{ message: string }> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/approve`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data),
})
return handleResponse<{ message: string }>(response)
}
// =============================================================================
// DSFA STATISTICS
// =============================================================================
/**
* Get DSFA statistics for the dashboard
*/
export async function getDSFAStats(): Promise<DSFAStatsResponse> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/stats`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<DSFAStatsResponse>(response)
}
// =============================================================================
// UCCA INTEGRATION
// =============================================================================
/**
* Create a DSFA from a UCCA assessment (pre-filled)
*/
export async function createDSFAFromAssessment(
assessmentId: string,
data?: CreateDSFAFromAssessmentRequest
): Promise<CreateDSFAFromAssessmentResponse> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/from-assessment/${assessmentId}`, {
method: 'POST',
headers: getHeaders(),
credentials: 'include',
body: JSON.stringify(data || {}),
})
return handleResponse<CreateDSFAFromAssessmentResponse>(response)
}
/**
* Get a DSFA by its linked UCCA assessment ID
*/
export async function getDSFAByAssessment(assessmentId: string): Promise<DSFA | null> {
try {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/by-assessment/${assessmentId}`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
if (response.status === 404) {
return null
}
return handleResponse<DSFA>(response)
} catch (error) {
// Return null if DSFA not found
return null
}
}
/**
* Check if a DSFA is required for a UCCA assessment
*/
export async function checkDSFARequired(assessmentId: string): Promise<DSFATriggerInfo> {
const response = await fetch(`${getBaseUrl()}/ucca/assessments/${assessmentId}/dsfa-required`, {
method: 'GET',
headers: getHeaders(),
credentials: 'include',
})
return handleResponse<DSFATriggerInfo>(response)
}
// =============================================================================
// EXPORT
// =============================================================================
/**
* Export a DSFA as JSON
*/
export async function exportDSFAAsJSON(id: string): Promise<Blob> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/export?format=json`, {
method: 'GET',
headers: {
'Accept': 'application/json',
},
credentials: 'include',
})
if (!response.ok) {
throw new Error(`Export failed: ${response.statusText}`)
}
return response.blob()
}
/**
* Export a DSFA as PDF
*/
export async function exportDSFAAsPDF(id: string): Promise<Blob> {
const response = await fetch(`${getBaseUrl()}/dsgvo/dsfas/${id}/export/pdf`, {
method: 'GET',
headers: {
'Accept': 'application/pdf',
},
credentials: 'include',
})
if (!response.ok) {
throw new Error(`PDF export failed: ${response.statusText}`)
}
return response.blob()
}
// =============================================================================
// RISK & MITIGATION OPERATIONS
// =============================================================================
/**
* Add a risk to a DSFA
*/
export async function addDSFARisk(dsfaId: string, risk: {
category: string
description: string
likelihood: 'low' | 'medium' | 'high'
impact: 'low' | 'medium' | 'high'
affected_data?: string[]
}): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const newRisk = {
id: crypto.randomUUID(),
...risk,
risk_level: calculateRiskLevelString(risk.likelihood, risk.impact),
affected_data: risk.affected_data || [],
}
const updatedRisks = [...(dsfa.risks || []), newRisk]
return updateDSFA(dsfaId, { risks: updatedRisks } as Partial<DSFA>)
}
/**
* Remove a risk from a DSFA
*/
export async function removeDSFARisk(dsfaId: string, riskId: string): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const updatedRisks = (dsfa.risks || []).filter(r => r.id !== riskId)
return updateDSFA(dsfaId, { risks: updatedRisks } as Partial<DSFA>)
}
/**
* Add a mitigation to a DSFA
*/
export async function addDSFAMitigation(dsfaId: string, mitigation: {
risk_id: string
description: string
type: 'technical' | 'organizational' | 'legal'
responsible_party: string
}): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const newMitigation = {
id: crypto.randomUUID(),
...mitigation,
status: 'planned' as const,
residual_risk: 'medium' as const,
}
const updatedMitigations = [...(dsfa.mitigations || []), newMitigation]
return updateDSFA(dsfaId, { mitigations: updatedMitigations } as Partial<DSFA>)
}
/**
* Update mitigation status
*/
export async function updateDSFAMitigationStatus(
dsfaId: string,
mitigationId: string,
status: 'planned' | 'in_progress' | 'implemented' | 'verified'
): Promise<DSFA> {
const dsfa = await getDSFA(dsfaId)
const updatedMitigations = (dsfa.mitigations || []).map(m => {
if (m.id === mitigationId) {
return {
...m,
status,
...(status === 'implemented' && { implemented_at: new Date().toISOString() }),
...(status === 'verified' && { verified_at: new Date().toISOString() }),
}
}
return m
})
return updateDSFA(dsfaId, { mitigations: updatedMitigations } as Partial<DSFA>)
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function calculateRiskLevelString(
likelihood: 'low' | 'medium' | 'high',
impact: 'low' | 'medium' | 'high'
): string {
const matrix: Record<string, Record<string, string>> = {
low: { low: 'low', medium: 'low', high: 'medium' },
medium: { low: 'low', medium: 'medium', high: 'high' },
high: { low: 'medium', medium: 'high', high: 'very_high' },
}
return matrix[likelihood]?.[impact] || 'medium'
}

View File

@@ -0,0 +1,8 @@
/**
* DSFA Module
*
* Exports for DSFA (Data Protection Impact Assessment) functionality.
*/
export * from './types'
export * from './api'

View File

@@ -0,0 +1,365 @@
/**
* DSFA Types - Datenschutz-Folgenabschätzung (Art. 35 DSGVO)
*
* TypeScript type definitions for DSFA (Data Protection Impact Assessment)
* aligned with the backend Go models.
*/
// =============================================================================
// ENUMS & CONSTANTS
// =============================================================================
export type DSFAStatus = 'draft' | 'in_review' | 'approved' | 'rejected' | 'needs_update'
export type DSFARiskLevel = 'low' | 'medium' | 'high' | 'very_high'
export type DSFARiskCategory = 'confidentiality' | 'integrity' | 'availability' | 'rights_freedoms'
export type DSFAMitigationType = 'technical' | 'organizational' | 'legal'
export type DSFAMitigationStatus = 'planned' | 'in_progress' | 'implemented' | 'verified'
export const DSFA_STATUS_LABELS: Record<DSFAStatus, string> = {
draft: 'Entwurf',
in_review: 'In Prüfung',
approved: 'Genehmigt',
rejected: 'Abgelehnt',
needs_update: 'Überarbeitung erforderlich',
}
export const DSFA_RISK_LEVEL_LABELS: Record<DSFARiskLevel, string> = {
low: 'Niedrig',
medium: 'Mittel',
high: 'Hoch',
very_high: 'Sehr Hoch',
}
export const DSFA_LEGAL_BASES = {
consent: 'Art. 6 Abs. 1 lit. a DSGVO - Einwilligung',
contract: 'Art. 6 Abs. 1 lit. b DSGVO - Vertrag',
legal_obligation: 'Art. 6 Abs. 1 lit. c DSGVO - Rechtliche Verpflichtung',
vital_interests: 'Art. 6 Abs. 1 lit. d DSGVO - Lebenswichtige Interessen',
public_interest: 'Art. 6 Abs. 1 lit. e DSGVO - Öffentliches Interesse',
legitimate_interest: 'Art. 6 Abs. 1 lit. f DSGVO - Berechtigtes Interesse',
}
export const DSFA_AFFECTED_RIGHTS = [
{ id: 'right_to_information', label: 'Recht auf Information (Art. 13/14)' },
{ id: 'right_of_access', label: 'Auskunftsrecht (Art. 15)' },
{ id: 'right_to_rectification', label: 'Recht auf Berichtigung (Art. 16)' },
{ id: 'right_to_erasure', label: 'Recht auf Löschung (Art. 17)' },
{ id: 'right_to_restriction', label: 'Recht auf Einschränkung (Art. 18)' },
{ id: 'right_to_data_portability', label: 'Recht auf Datenübertragbarkeit (Art. 20)' },
{ id: 'right_to_object', label: 'Widerspruchsrecht (Art. 21)' },
{ id: 'right_not_to_be_profiled', label: 'Recht bzgl. Profiling (Art. 22)' },
{ id: 'freedom_of_expression', label: 'Meinungsfreiheit' },
{ id: 'freedom_of_association', label: 'Versammlungsfreiheit' },
{ id: 'non_discrimination', label: 'Nichtdiskriminierung' },
{ id: 'data_security', label: 'Datensicherheit' },
]
// =============================================================================
// SUB-TYPES
// =============================================================================
export interface DSFARisk {
id: string
category: DSFARiskCategory
description: string
likelihood: 'low' | 'medium' | 'high'
impact: 'low' | 'medium' | 'high'
risk_level: string
affected_data: string[]
}
export interface DSFAMitigation {
id: string
risk_id: string
description: string
type: DSFAMitigationType
status: DSFAMitigationStatus
implemented_at?: string
verified_at?: string
residual_risk: 'low' | 'medium' | 'high'
tom_reference?: string
responsible_party: string
}
export interface DSFAReviewComment {
id: string
section: number
comment: string
created_by: string
created_at: string
resolved: boolean
}
export interface DSFASectionProgress {
section_1_complete: boolean
section_2_complete: boolean
section_3_complete: boolean
section_4_complete: boolean
section_5_complete: boolean
}
// =============================================================================
// MAIN DSFA TYPE
// =============================================================================
export interface DSFA {
id: string
tenant_id: string
namespace_id?: string
processing_activity_id?: string
assessment_id?: string
name: string
description: string
// Section 1: Systematische Beschreibung (Art. 35 Abs. 7 lit. a)
processing_description: string
processing_purpose: string
data_categories: string[]
data_subjects: string[]
recipients: string[]
legal_basis: string
legal_basis_details?: string
// Section 2: Notwendigkeit & Verhältnismäßigkeit (Art. 35 Abs. 7 lit. b)
necessity_assessment: string
proportionality_assessment: string
data_minimization?: string
alternatives_considered?: string
retention_justification?: string
// Section 3: Risikobewertung (Art. 35 Abs. 7 lit. c)
risks: DSFARisk[]
overall_risk_level: DSFARiskLevel
risk_score: number
affected_rights?: string[]
triggered_rule_codes?: string[]
// Section 4: Abhilfemaßnahmen (Art. 35 Abs. 7 lit. d)
mitigations: DSFAMitigation[]
tom_references?: string[]
// Section 5: Stellungnahme DSB (Art. 35 Abs. 2 + Art. 36)
dpo_consulted: boolean
dpo_consulted_at?: string
dpo_name?: string
dpo_opinion?: string
dpo_approved?: boolean
authority_consulted: boolean
authority_consulted_at?: string
authority_reference?: string
authority_decision?: string
// Workflow & Approval
status: DSFAStatus
submitted_for_review_at?: string
submitted_by?: string
conclusion: string
review_comments?: DSFAReviewComment[]
// Section Progress Tracking
section_progress: DSFASectionProgress
// Metadata & Audit
metadata?: Record<string, unknown>
created_at: string
updated_at: string
created_by: string
approved_by?: string
approved_at?: string
}
// =============================================================================
// API REQUEST/RESPONSE TYPES
// =============================================================================
export interface DSFAListResponse {
dsfas: DSFA[]
}
export interface DSFAStatsResponse {
status_stats: Record<DSFAStatus | 'total', number>
risk_stats: Record<DSFARiskLevel, number>
total: number
}
export interface CreateDSFARequest {
name: string
description?: string
processing_description?: string
processing_purpose?: string
data_categories?: string[]
legal_basis?: string
}
export interface CreateDSFAFromAssessmentRequest {
name?: string
description?: string
}
export interface CreateDSFAFromAssessmentResponse {
dsfa: DSFA
prefilled: boolean
assessment: unknown // UCCA Assessment
message: string
}
export interface UpdateDSFASectionRequest {
// Section 1
processing_description?: string
processing_purpose?: string
data_categories?: string[]
data_subjects?: string[]
recipients?: string[]
legal_basis?: string
legal_basis_details?: string
// Section 2
necessity_assessment?: string
proportionality_assessment?: string
data_minimization?: string
alternatives_considered?: string
retention_justification?: string
// Section 3
overall_risk_level?: DSFARiskLevel
risk_score?: number
affected_rights?: string[]
// Section 5
dpo_consulted?: boolean
dpo_name?: string
dpo_opinion?: string
authority_consulted?: boolean
authority_reference?: string
authority_decision?: string
}
export interface SubmitForReviewResponse {
message: string
dsfa: DSFA
}
export interface ApproveDSFARequest {
dpo_opinion: string
approved: boolean
}
// =============================================================================
// UCCA INTEGRATION TYPES
// =============================================================================
export interface DSFATriggerInfo {
required: boolean
reason: string
triggered_rules: string[]
assessment_id?: string
existing_dsfa_id?: string
}
export interface UCCATriggeredRule {
code: string
title: string
description: string
severity: 'INFO' | 'WARN' | 'BLOCK'
gdpr_ref?: string
}
// =============================================================================
// HELPER TYPES FOR UI
// =============================================================================
export interface DSFASectionConfig {
number: number
title: string
titleDE: string
description: string
gdprRef: string
fields: string[]
required: boolean
}
export const DSFA_SECTIONS: DSFASectionConfig[] = [
{
number: 1,
title: 'Processing Description',
titleDE: 'Systematische Beschreibung',
description: 'Beschreiben Sie die geplante Verarbeitung, ihren Zweck, die Datenkategorien und Rechtsgrundlage.',
gdprRef: 'Art. 35 Abs. 7 lit. a DSGVO',
fields: ['processing_description', 'processing_purpose', 'data_categories', 'data_subjects', 'recipients', 'legal_basis'],
required: true,
},
{
number: 2,
title: 'Necessity & Proportionality',
titleDE: 'Notwendigkeit & Verhältnismäßigkeit',
description: 'Begründen Sie, warum die Verarbeitung notwendig ist und welche Alternativen geprüft wurden.',
gdprRef: 'Art. 35 Abs. 7 lit. b DSGVO',
fields: ['necessity_assessment', 'proportionality_assessment', 'data_minimization', 'alternatives_considered'],
required: true,
},
{
number: 3,
title: 'Risk Assessment',
titleDE: 'Risikobewertung',
description: 'Identifizieren und bewerten Sie die Risiken für die Rechte und Freiheiten der Betroffenen.',
gdprRef: 'Art. 35 Abs. 7 lit. c DSGVO',
fields: ['risks', 'overall_risk_level', 'risk_score', 'affected_rights'],
required: true,
},
{
number: 4,
title: 'Mitigation Measures',
titleDE: 'Abhilfemaßnahmen',
description: 'Definieren Sie technische und organisatorische Maßnahmen zur Risikominimierung.',
gdprRef: 'Art. 35 Abs. 7 lit. d DSGVO',
fields: ['mitigations', 'tom_references'],
required: true,
},
{
number: 5,
title: 'DPO Opinion',
titleDE: 'Stellungnahme DSB',
description: 'Dokumentieren Sie die Konsultation des Datenschutzbeauftragten und ggf. der Aufsichtsbehörde.',
gdprRef: 'Art. 35 Abs. 2 + Art. 36 DSGVO',
fields: ['dpo_consulted', 'dpo_opinion', 'authority_consulted', 'authority_reference'],
required: false,
},
]
// =============================================================================
// RISK MATRIX HELPERS
// =============================================================================
export interface RiskMatrixCell {
likelihood: 'low' | 'medium' | 'high'
impact: 'low' | 'medium' | 'high'
level: DSFARiskLevel
score: number
}
export const RISK_MATRIX: RiskMatrixCell[] = [
// Low likelihood
{ likelihood: 'low', impact: 'low', level: 'low', score: 10 },
{ likelihood: 'low', impact: 'medium', level: 'low', score: 20 },
{ likelihood: 'low', impact: 'high', level: 'medium', score: 40 },
// Medium likelihood
{ likelihood: 'medium', impact: 'low', level: 'low', score: 20 },
{ likelihood: 'medium', impact: 'medium', level: 'medium', score: 50 },
{ likelihood: 'medium', impact: 'high', level: 'high', score: 70 },
// High likelihood
{ likelihood: 'high', impact: 'low', level: 'medium', score: 40 },
{ likelihood: 'high', impact: 'medium', level: 'high', score: 70 },
{ likelihood: 'high', impact: 'high', level: 'very_high', score: 90 },
]
export function calculateRiskLevel(
likelihood: 'low' | 'medium' | 'high',
impact: 'low' | 'medium' | 'high'
): { level: DSFARiskLevel; score: number } {
const cell = RISK_MATRIX.find(c => c.likelihood === likelihood && c.impact === impact)
return cell ? { level: cell.level, score: cell.score } : { level: 'medium', score: 50 }
}

View File

@@ -559,19 +559,6 @@ export const SDK_STEPS: SDKStep[] = [
prerequisiteSteps: ['consent'],
isOptional: false,
},
{
id: 'document-generator',
phase: 2,
package: 'rechtliche-texte',
order: 4,
name: 'Dokumentengenerator',
nameShort: 'Generator',
description: 'Rechtliche Dokumente aus Vorlagen erstellen',
url: '/sdk/document-generator',
checkpointId: 'CP-DOCGEN',
prerequisiteSteps: ['cookie-banner'],
isOptional: true,
},
// =============================================================================
// PAKET 5: BETRIEB (Operations)