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:
364
admin-v2/lib/companion/constants.ts
Normal file
364
admin-v2/lib/companion/constants.ts
Normal 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`
|
||||
}
|
||||
2
admin-v2/lib/companion/index.ts
Normal file
2
admin-v2/lib/companion/index.ts
Normal file
@@ -0,0 +1,2 @@
|
||||
export * from './types'
|
||||
export * from './constants'
|
||||
329
admin-v2/lib/companion/types.ts
Normal file
329
admin-v2/lib/companion/types.ts
Normal 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
|
||||
}
|
||||
60
admin-v2/lib/content-types.ts
Normal file
60
admin-v2/lib/content-types.ts
Normal 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
284
admin-v2/lib/content.ts
Normal 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
|
||||
}
|
||||
164
admin-v2/lib/education/abitur-archiv-types.ts
Normal file
164
admin-v2/lib/education/abitur-archiv-types.ts
Normal 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)}%`
|
||||
}
|
||||
84
admin-v2/lib/education/abitur-docs-types.ts
Normal file
84
admin-v2/lib/education/abitur-docs-types.ts
Normal 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})`
|
||||
}
|
||||
@@ -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'
|
||||
},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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: [] },
|
||||
|
||||
355
admin-v2/lib/sdk/dsfa/__tests__/api.test.ts
Normal file
355
admin-v2/lib/sdk/dsfa/__tests__/api.test.ts
Normal 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()
|
||||
})
|
||||
})
|
||||
})
|
||||
255
admin-v2/lib/sdk/dsfa/__tests__/types.test.ts
Normal file
255
admin-v2/lib/sdk/dsfa/__tests__/types.test.ts
Normal 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)
|
||||
})
|
||||
})
|
||||
399
admin-v2/lib/sdk/dsfa/api.ts
Normal file
399
admin-v2/lib/sdk/dsfa/api.ts
Normal 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'
|
||||
}
|
||||
8
admin-v2/lib/sdk/dsfa/index.ts
Normal file
8
admin-v2/lib/sdk/dsfa/index.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
/**
|
||||
* DSFA Module
|
||||
*
|
||||
* Exports for DSFA (Data Protection Impact Assessment) functionality.
|
||||
*/
|
||||
|
||||
export * from './types'
|
||||
export * from './api'
|
||||
365
admin-v2/lib/sdk/dsfa/types.ts
Normal file
365
admin-v2/lib/sdk/dsfa/types.ts
Normal 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 }
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user