feat(admin-v2): Major SDK/Compliance overhaul and new modules

SDK modules added/enhanced:
- compliance-hub, compliance-scope, consent-management, notfallplan
- audit-report, workflow, source-policy, dsms
- advisory-board documentation section
- TOM dashboard components, TOM generator SDM mapping
- DSFA: mitigation library, risk catalog, threshold analysis, source attribution
- VVT: baseline catalog, profiling engine, types
- Loeschfristen: baseline catalog, compliance engine, export, profiling, types
- Compliance scope: engine, profiling, golden tests, types

Existing SDK pages updated:
- dsfa/[id], tom, vvt, loeschfristen, advisory-board — expanded functionality
- SDKSidebar, StepHeader — new navigation items and layout
- SDK layout, context, types — expanded type system

Other admin-v2 changes:
- AI agents page, RAG pipeline DSFA integration
- GridOverlay component updates
- Companion feature (development + education)
- Compliance advisor SOUL definition

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
BreakPilot Dev
2026-02-10 00:01:04 +01:00
parent 53219e3eaf
commit dff2ef796b
94 changed files with 29706 additions and 1039 deletions

View File

@@ -0,0 +1,129 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* POST /api/admin/companion/feedback
* Submit feedback (bug report, feature request, general feedback)
* Proxy to backend /api/feedback
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Validate required fields
if (!body.type || !body.title || !body.description) {
return NextResponse.json(
{
success: false,
error: 'Missing required fields: type, title, description',
},
{ status: 400 }
)
}
// Validate feedback type
const validTypes = ['bug', 'feature', 'feedback']
if (!validTypes.includes(body.type)) {
return NextResponse.json(
{
success: false,
error: 'Invalid feedback type. Must be: bug, feature, or feedback',
},
{ status: 400 }
)
}
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/feedback`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// // Add auth headers
// },
// body: JSON.stringify({
// type: body.type,
// title: body.title,
// description: body.description,
// screenshot: body.screenshot,
// sessionId: body.sessionId,
// metadata: {
// ...body.metadata,
// source: 'companion',
// timestamp: new Date().toISOString(),
// userAgent: request.headers.get('user-agent'),
// },
// }),
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - just acknowledge the submission
const feedbackId = `fb-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
console.log('Feedback received:', {
id: feedbackId,
type: body.type,
title: body.title,
description: body.description.substring(0, 100) + '...',
hasScreenshot: !!body.screenshot,
sessionId: body.sessionId,
})
return NextResponse.json({
success: true,
message: 'Feedback submitted successfully',
data: {
feedbackId,
submittedAt: new Date().toISOString(),
},
})
} catch (error) {
console.error('Submit feedback error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* GET /api/admin/companion/feedback
* Get feedback history (admin only)
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const type = searchParams.get('type')
const limit = parseInt(searchParams.get('limit') || '10')
// TODO: Replace with actual backend call
// Mock response - empty list for now
return NextResponse.json({
success: true,
data: {
feedback: [],
total: 0,
page: 1,
limit,
},
})
} catch (error) {
console.error('Get feedback error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,194 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* POST /api/admin/companion/lesson
* Start a new lesson session
* Proxy to backend /api/classroom/sessions
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// TODO: Replace with actual backend call
// const response = await fetch(`${backendUrl}/api/classroom/sessions`, {
// method: 'POST',
// headers: {
// 'Content-Type': 'application/json',
// },
// body: JSON.stringify(body),
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - create a new session
const sessionId = `session-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
const mockSession = {
success: true,
data: {
sessionId,
classId: body.classId,
className: body.className || body.classId,
subject: body.subject,
topic: body.topic,
startTime: new Date().toISOString(),
phases: [
{ phase: 'einstieg', duration: 8, status: 'active', actualTime: 0 },
{ phase: 'erarbeitung', duration: 20, status: 'planned', actualTime: 0 },
{ phase: 'sicherung', duration: 10, status: 'planned', actualTime: 0 },
{ phase: 'transfer', duration: 7, status: 'planned', actualTime: 0 },
{ phase: 'reflexion', duration: 5, status: 'planned', actualTime: 0 },
],
totalPlannedDuration: 50,
currentPhaseIndex: 0,
elapsedTime: 0,
isPaused: false,
pauseDuration: 0,
overtimeMinutes: 0,
status: 'in_progress',
homeworkList: [],
materials: [],
},
}
return NextResponse.json(mockSession)
} catch (error) {
console.error('Start lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* GET /api/admin/companion/lesson
* Get current lesson session or list of recent sessions
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const url = sessionId
// ? `${backendUrl}/api/classroom/sessions/${sessionId}`
// : `${backendUrl}/api/classroom/sessions`
//
// const response = await fetch(url)
// const data = await response.json()
// return NextResponse.json(data)
// Mock response
if (sessionId) {
return NextResponse.json({
success: true,
data: null, // No active session stored on server in mock
})
}
return NextResponse.json({
success: true,
data: {
sessions: [], // Empty list for now
},
})
} catch (error) {
console.error('Get lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* PATCH /api/admin/companion/lesson
* Update lesson session (timer state, phase changes, etc.)
*/
export async function PATCH(request: NextRequest) {
try {
const body = await request.json()
const { sessionId, ...updates } = body
if (!sessionId) {
return NextResponse.json(
{ success: false, error: 'Session ID required' },
{ status: 400 }
)
}
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/classroom/sessions/${sessionId}`, {
// method: 'PATCH',
// headers: { 'Content-Type': 'application/json' },
// body: JSON.stringify(updates),
// })
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - just acknowledge the update
return NextResponse.json({
success: true,
message: 'Session updated',
})
} catch (error) {
console.error('Update lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* DELETE /api/admin/companion/lesson
* End/delete a lesson session
*/
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
if (!sessionId) {
return NextResponse.json(
{ success: false, error: 'Session ID required' },
{ status: 400 }
)
}
// TODO: Replace with actual backend call
return NextResponse.json({
success: true,
message: 'Session ended',
})
} catch (error) {
console.error('End lesson error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,102 @@
import { NextResponse } from 'next/server'
/**
* GET /api/admin/companion
* Proxy to backend /api/state/dashboard for companion dashboard data
*/
export async function GET() {
try {
const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// TODO: Replace with actual backend call when endpoint is available
// const response = await fetch(`${backendUrl}/api/state/dashboard`, {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// },
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response for development
const mockData = {
success: true,
data: {
context: {
currentPhase: 'erarbeitung',
phaseDisplayName: 'Erarbeitung',
},
stats: {
classesCount: 4,
studentsCount: 96,
learningUnitsCreated: 23,
gradesEntered: 156,
},
phases: [
{ id: 'einstieg', shortName: 'E', displayName: 'Einstieg', duration: 8, status: 'completed', color: '#4A90E2' },
{ id: 'erarbeitung', shortName: 'A', displayName: 'Erarbeitung', duration: 20, status: 'active', color: '#F5A623' },
{ id: 'sicherung', shortName: 'S', displayName: 'Sicherung', duration: 10, status: 'planned', color: '#7ED321' },
{ id: 'transfer', shortName: 'T', displayName: 'Transfer', duration: 7, status: 'planned', color: '#9013FE' },
{ id: 'reflexion', shortName: 'R', displayName: 'Reflexion', duration: 5, status: 'planned', color: '#6B7280' },
],
progress: {
percentage: 65,
completed: 13,
total: 20,
},
suggestions: [
{
id: '1',
title: 'Klausuren korrigieren',
description: 'Deutsch LK - 12 unkorrigierte Arbeiten warten',
priority: 'urgent',
icon: 'ClipboardCheck',
actionTarget: '/ai/klausur-korrektur',
estimatedTime: 120,
},
{
id: '2',
title: 'Elternsprechtag vorbereiten',
description: 'Notenuebersicht fuer 8b erstellen',
priority: 'high',
icon: 'Users',
actionTarget: '/education/grades',
estimatedTime: 30,
},
],
upcomingEvents: [
{
id: 'e1',
title: 'Mathe-Test 9b',
date: new Date(Date.now() + 2 * 24 * 60 * 60 * 1000).toISOString(),
type: 'exam',
inDays: 2,
},
{
id: 'e2',
title: 'Elternsprechtag',
date: new Date(Date.now() + 5 * 24 * 60 * 60 * 1000).toISOString(),
type: 'parent_meeting',
inDays: 5,
},
],
},
}
return NextResponse.json(mockData)
} catch (error) {
console.error('Companion dashboard error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,137 @@
import { NextRequest, NextResponse } from 'next/server'
const DEFAULT_SETTINGS = {
defaultPhaseDurations: {
einstieg: 8,
erarbeitung: 20,
sicherung: 10,
transfer: 7,
reflexion: 5,
},
preferredLessonLength: 45,
autoAdvancePhases: true,
soundNotifications: true,
showKeyboardShortcuts: true,
highContrastMode: false,
onboardingCompleted: false,
}
/**
* GET /api/admin/companion/settings
* Get teacher settings
* Proxy to backend /api/teacher/settings
*/
export async function GET() {
try {
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/teacher/settings`, {
// method: 'GET',
// headers: {
// 'Content-Type': 'application/json',
// // Add auth headers
// },
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - return default settings
return NextResponse.json({
success: true,
data: DEFAULT_SETTINGS,
})
} catch (error) {
console.error('Get settings error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* PUT /api/admin/companion/settings
* Update teacher settings
*/
export async function PUT(request: NextRequest) {
try {
const body = await request.json()
// Validate the settings structure
if (!body || typeof body !== 'object') {
return NextResponse.json(
{ success: false, error: 'Invalid settings data' },
{ status: 400 }
)
}
// TODO: Replace with actual backend call
// const backendUrl = process.env.BACKEND_URL || 'http://localhost:8000'
// const response = await fetch(`${backendUrl}/api/teacher/settings`, {
// method: 'PUT',
// headers: {
// 'Content-Type': 'application/json',
// // Add auth headers
// },
// body: JSON.stringify(body),
// })
//
// if (!response.ok) {
// throw new Error(`Backend responded with ${response.status}`)
// }
//
// const data = await response.json()
// return NextResponse.json(data)
// Mock response - just acknowledge the save
return NextResponse.json({
success: true,
message: 'Settings saved',
data: body,
})
} catch (error) {
console.error('Save settings error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
/**
* PATCH /api/admin/companion/settings
* Partially update teacher settings
*/
export async function PATCH(request: NextRequest) {
try {
const body = await request.json()
// TODO: Replace with actual backend call
return NextResponse.json({
success: true,
message: 'Settings updated',
data: body,
})
} catch (error) {
console.error('Update settings error:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,217 @@
/**
* Compliance Advisor Chat API
*
* Connects the ComplianceAdvisorWidget to:
* 1. RAG legal corpus search (klausur-service) for context
* 2. Ollama LLM (32B) for generating answers
*
* Streams the LLM response back as plain text.
*/
import { NextRequest, NextResponse } from 'next/server'
const KLAUSUR_SERVICE_URL = process.env.KLAUSUR_SERVICE_URL || 'http://klausur-service:8086'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
// SOUL system prompt (from agent-core/soul/compliance-advisor.soul.md)
const SYSTEM_PROMPT = `# Compliance Advisor Agent
## Identitaet
Du bist der BreakPilot Compliance-Berater. Du hilfst Nutzern des AI Compliance SDK,
Datenschutz- und Compliance-Fragen in verstaendlicher Sprache zu beantworten.
Du bist kein Anwalt und gibst keine Rechtsberatung, sondern orientierst dich an
offiziellen Quellen und gibst praxisnahe Hinweise.
## Kernprinzipien
- **Quellenbasiert**: Verweise immer auf konkrete Rechtsgrundlagen (DSGVO-Artikel, BDSG-Paragraphen)
- **Verstaendlich**: Erklaere rechtliche Konzepte in einfacher, praxisnaher Sprache
- **Ehrlich**: Bei Unsicherheit empfehle professionelle Rechtsberatung
- **Kontextbewusst**: Nutze das RAG-System fuer aktuelle Rechtstexte und Leitfaeden
- **Scope-bewusst**: Nutze alle verfuegbaren RAG-Quellen (DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM, BSI, Laender-Muss-Listen, EDPB Guidelines, etc.) AUSSER NIBIS-Dokumenten.
## Kompetenzbereich
- DSGVO Art. 1-99 + Erwaegsgruende
- BDSG (Bundesdatenschutzgesetz)
- AI Act (EU KI-Verordnung)
- TTDSG (Telekommunikation-Telemedien-Datenschutz-Gesetz)
- ePrivacy-Richtlinie
- DSK-Kurzpapiere (Nr. 1-20)
- SDM (Standard-Datenschutzmodell) V3.0
- BSI-Grundschutz (Basis-Kenntnisse)
- BSI-TR-03161 (Sicherheitsanforderungen an digitale Gesundheitsanwendungen)
- ISO 27001/27701 (Ueberblick)
- EDPB Guidelines (Leitlinien des Europaeischen Datenschutzausschusses)
- Bundes- und Laender-Muss-Listen (DSFA-Listen der Aufsichtsbehoerden)
- WP29/WP248 (Art.-29-Datenschutzgruppe Arbeitspapiere)
## RAG-Nutzung
Nutze das gesamte RAG-Corpus fuer Kontext und Quellenangaben — ausgenommen sind
NIBIS-Inhalte (Erwartungshorizonte, Bildungsstandards, curriculare Vorgaben).
Diese gehoeren nicht zum Datenschutz-Kompetenzbereich.
## Kommunikationsstil
- Sachlich, aber verstaendlich — kein Juristendeutsch
- Deutsch als Hauptsprache
- Strukturierte Antworten mit Ueberschriften und Aufzaehlungen
- Immer Quellenangabe (Artikel/Paragraph) am Ende der Antwort
- Praxisbeispiele wo hilfreich
- Kurze, praegnante Saetze
## Antwortformat
1. Kurze Zusammenfassung (1-2 Saetze)
2. Detaillierte Erklaerung
3. Praxishinweise / Handlungsempfehlungen
4. Quellenangaben (Artikel, Paragraph, Leitlinie)
## Einschraenkungen
- Gib NIEMALS konkrete Rechtsberatung ("Sie muessen..." -> "Es empfiehlt sich...")
- Keine Garantien fuer Rechtssicherheit
- Bei komplexen Einzelfaellen: Empfehle Rechtsanwalt/DSB
- Keine Aussagen zu laufenden Verfahren oder Bussgeldern
- Keine Interpretation von Urteilen (nur Verweis)
## Eskalation
- Bei Fragen ausserhalb des Kompetenzbereichs: Hoeflich ablehnen und auf Fachanwalt verweisen
- Bei widerspruechlichen Rechtslagen: Beide Positionen darstellen und DSB-Konsultation empfehlen
- Bei dringenden Datenpannen: Auf 72-Stunden-Frist (Art. 33 DSGVO) hinweisen und Notfallplan-Modul empfehlen`
/**
* Query the RAG legal corpus for relevant documents
*/
async function queryRAG(query: string): Promise<string> {
try {
const url = `${KLAUSUR_SERVICE_URL}/api/v1/admin/legal-corpus/search?query=${encodeURIComponent(query)}&top_k=5`
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(10000),
})
if (!res.ok) {
console.warn('RAG search failed:', res.status)
return ''
}
const data = await res.json()
if (data.results && data.results.length > 0) {
return data.results
.map(
(r: { metadata?: { regulation?: string; source?: string }; text?: string; content?: string }, i: number) =>
`[Quelle ${i + 1}: ${r.metadata?.regulation || r.metadata?.source || 'Unbekannt'}]\n${r.text || r.content || ''}`
)
.join('\n\n---\n\n')
}
return ''
} catch (error) {
console.warn('RAG query error (continuing without context):', error)
return ''
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { message, history = [], currentStep = 'default' } = body
if (!message || typeof message !== 'string') {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
}
// 1. Query RAG for relevant context
const ragContext = await queryRAG(message)
// 2. Build system prompt with RAG context
let systemContent = SYSTEM_PROMPT
if (ragContext) {
systemContent += `\n\n## Relevanter Kontext aus dem RAG-System\n\nNutze die folgenden Quellen fuer deine Antwort. Verweise in deiner Antwort auf die jeweilige Quelle:\n\n${ragContext}`
}
systemContent += `\n\n## Aktueller SDK-Schritt\nDer Nutzer befindet sich im SDK-Schritt: ${currentStep}`
// 3. Build messages array (limit history to last 10 messages)
const messages = [
{ role: 'system', content: systemContent },
...history.slice(-10).map((h: { role: string; content: string }) => ({
role: h.role === 'user' ? 'user' : 'assistant',
content: h.content,
})),
{ role: 'user', content: message },
]
// 4. Call Ollama with streaming
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: LLM_MODEL,
messages,
stream: true,
options: {
temperature: 0.3,
num_predict: 2048,
},
}),
signal: AbortSignal.timeout(120000),
})
if (!ollamaResponse.ok) {
const errorText = await ollamaResponse.text()
console.error('Ollama error:', ollamaResponse.status, errorText)
return NextResponse.json(
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status}). Ist Ollama mit dem Modell ${LLM_MODEL} gestartet?` },
{ status: 502 }
)
}
// 5. Stream response back as plain text
const encoder = new TextEncoder()
const stream = new ReadableStream({
async start(controller) {
const reader = ollamaResponse.body!.getReader()
const decoder = new TextDecoder()
try {
while (true) {
const { done, value } = await reader.read()
if (done) break
const chunk = decoder.decode(value, { stream: true })
const lines = chunk.split('\n').filter((l) => l.trim())
for (const line of lines) {
try {
const json = JSON.parse(line)
if (json.message?.content) {
controller.enqueue(encoder.encode(json.message.content))
}
} catch {
// Partial JSON line, skip
}
}
}
} catch (error) {
console.error('Stream read error:', error)
} finally {
controller.close()
}
},
})
return new NextResponse(stream, {
headers: {
'Content-Type': 'text/plain; charset=utf-8',
'Cache-Control': 'no-cache',
'Connection': 'keep-alive',
},
})
} catch (error) {
console.error('Compliance advisor chat error:', error)
return NextResponse.json(
{ error: 'Verbindung zum LLM fehlgeschlagen. Bitte pruefen Sie ob Ollama laeuft.' },
{ status: 503 }
)
}
}