feat: Sync SDK modules, API routes, blog and docs from admin-v2

- DSB Portal, Industry Templates, Multi-Tenant, SSO frontend pages
- All SDK API proxy routes (academy, crawler, incidents, vendors, whistleblower, etc.)
- Blog section with compliance articles
- BYOEH system documentation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-13 21:12:30 +01:00
parent 1bd1a0002a
commit 27f1899428
59 changed files with 16258 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,879 @@
'use client'
/**
* Branchenspezifische Module (Phase 3.3)
*
* Industry-specific compliance template packages:
* - Browse industry templates (grid view)
* - View full detail with VVT, TOM, Risk tabs
* - Apply template packages to current compliance setup
*/
import { useState, useEffect, useCallback } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface IndustrySummary {
slug: string
name: string
description: string
icon: string
regulation_count: number
template_count: number
}
interface IndustryTemplate {
slug: string
name: string
description: string
icon: string
regulations: string[]
vvt_templates: VVTTemplate[]
tom_recommendations: TOMRecommendation[]
risk_scenarios: RiskScenario[]
}
interface VVTTemplate {
name: string
purpose: string
legal_basis: string
data_categories: string[]
data_subjects: string[]
retention_period: string
}
interface TOMRecommendation {
category: string
name: string
description: string
priority: string
}
interface RiskScenario {
name: string
description: string
likelihood: string
impact: string
mitigation: string
}
// =============================================================================
// CONSTANTS
// =============================================================================
type DetailTab = 'vvt' | 'tom' | 'risks'
const DETAIL_TABS: { key: DetailTab; label: string }[] = [
{ key: 'vvt', label: 'VVT-Vorlagen' },
{ key: 'tom', label: 'TOM-Empfehlungen' },
{ key: 'risks', label: 'Risiko-Szenarien' },
]
const PRIORITY_COLORS: Record<string, { bg: string; text: string; border: string }> = {
critical: { bg: 'bg-red-50', text: 'text-red-700', border: 'border-red-200' },
high: { bg: 'bg-orange-50', text: 'text-orange-700', border: 'border-orange-200' },
medium: { bg: 'bg-yellow-50', text: 'text-yellow-700', border: 'border-yellow-200' },
low: { bg: 'bg-green-50', text: 'text-green-700', border: 'border-green-200' },
}
const PRIORITY_LABELS: Record<string, string> = {
critical: 'Kritisch',
high: 'Hoch',
medium: 'Mittel',
low: 'Niedrig',
}
const LIKELIHOOD_COLORS: Record<string, string> = {
low: 'bg-green-500',
medium: 'bg-yellow-500',
high: 'bg-orange-500',
}
const IMPACT_COLORS: Record<string, string> = {
low: 'bg-green-500',
medium: 'bg-yellow-500',
high: 'bg-orange-500',
critical: 'bg-red-600',
}
const TOM_CATEGORY_ICONS: Record<string, string> = {
'Zutrittskontrolle': '\uD83D\uDEAA',
'Zugangskontrolle': '\uD83D\uDD10',
'Zugriffskontrolle': '\uD83D\uDC65',
'Trennungskontrolle': '\uD83D\uDDC2\uFE0F',
'Pseudonymisierung': '\uD83C\uDFAD',
'Verschluesselung': '\uD83D\uDD12',
'Integritaet': '\u2705',
'Verfuegbarkeit': '\u2B06\uFE0F',
'Belastbarkeit': '\uD83D\uDEE1\uFE0F',
'Wiederherstellung': '\uD83D\uDD04',
'Datenschutz-Management': '\uD83D\uDCCB',
'Auftragsverarbeitung': '\uD83D\uDCDD',
'Incident Response': '\uD83D\uDEA8',
'Schulung': '\uD83C\uDF93',
'Netzwerksicherheit': '\uD83C\uDF10',
'Datensicherung': '\uD83D\uDCBE',
'Monitoring': '\uD83D\uDCCA',
'Physische Sicherheit': '\uD83C\uDFE2',
}
// =============================================================================
// SKELETON COMPONENTS
// =============================================================================
function GridSkeleton() {
return (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{[1, 2, 3, 4].map((i) => (
<div key={i} className="bg-white rounded-xl border border-slate-200 p-6 animate-pulse">
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-xl bg-slate-200" />
<div className="flex-1 space-y-3">
<div className="h-5 bg-slate-200 rounded w-2/3" />
<div className="h-4 bg-slate-100 rounded w-full" />
<div className="h-4 bg-slate-100 rounded w-4/5" />
</div>
</div>
<div className="flex gap-3 mt-5">
<div className="h-6 bg-slate-100 rounded-full w-28" />
<div className="h-6 bg-slate-100 rounded-full w-24" />
</div>
</div>
))}
</div>
)
}
function DetailSkeleton() {
return (
<div className="space-y-6 animate-pulse">
<div className="bg-white rounded-xl border p-6 space-y-4">
<div className="flex items-center gap-4">
<div className="w-16 h-16 rounded-xl bg-slate-200" />
<div className="space-y-2 flex-1">
<div className="h-6 bg-slate-200 rounded w-1/3" />
<div className="h-4 bg-slate-100 rounded w-2/3" />
</div>
</div>
<div className="flex gap-2">
{[1, 2, 3].map((i) => (
<div key={i} className="h-7 bg-slate-100 rounded-full w-20" />
))}
</div>
</div>
<div className="bg-white rounded-xl border p-6 space-y-4">
<div className="flex gap-2 border-b pb-4">
{[1, 2, 3].map((i) => (
<div key={i} className="h-9 bg-slate-100 rounded-lg w-32" />
))}
</div>
{[1, 2, 3].map((i) => (
<div key={i} className="h-28 bg-slate-50 rounded-lg" />
))}
</div>
</div>
)
}
// =============================================================================
// MAIN PAGE COMPONENT
// =============================================================================
export default function IndustryTemplatesPage() {
// ---------------------------------------------------------------------------
// State
// ---------------------------------------------------------------------------
const [industries, setIndustries] = useState<IndustrySummary[]>([])
const [selectedDetail, setSelectedDetail] = useState<IndustryTemplate | null>(null)
const [selectedSlug, setSelectedSlug] = useState<string | null>(null)
const [activeTab, setActiveTab] = useState<DetailTab>('vvt')
const [loading, setLoading] = useState(true)
const [detailLoading, setDetailLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const [detailError, setDetailError] = useState<string | null>(null)
const [applying, setApplying] = useState(false)
const [toastMessage, setToastMessage] = useState<string | null>(null)
// ---------------------------------------------------------------------------
// Data fetching
// ---------------------------------------------------------------------------
const loadIndustries = useCallback(async () => {
setLoading(true)
setError(null)
try {
const res = await fetch('/api/sdk/v1/industry/templates')
if (!res.ok) {
throw new Error(`HTTP ${res.status}: ${res.statusText}`)
}
const data = await res.json()
setIndustries(Array.isArray(data) ? data : data.industries || data.templates || [])
} catch (err) {
console.error('Failed to load industries:', err)
setError('Branchenvorlagen konnten nicht geladen werden. Bitte pruefen Sie die Verbindung zum Backend.')
} finally {
setLoading(false)
}
}, [])
const loadDetail = useCallback(async (slug: string) => {
setDetailLoading(true)
setDetailError(null)
setSelectedSlug(slug)
setActiveTab('vvt')
try {
const [detailRes, vvtRes, tomRes, risksRes] = await Promise.all([
fetch(`/api/sdk/v1/industry/templates/${slug}`),
fetch(`/api/sdk/v1/industry/templates/${slug}/vvt`),
fetch(`/api/sdk/v1/industry/templates/${slug}/tom`),
fetch(`/api/sdk/v1/industry/templates/${slug}/risks`),
])
if (!detailRes.ok) {
throw new Error(`HTTP ${detailRes.status}: ${detailRes.statusText}`)
}
const detail: IndustryTemplate = await detailRes.json()
// Merge sub-resources if the detail endpoint did not include them
if (vvtRes.ok) {
const vvtData = await vvtRes.json()
detail.vvt_templates = vvtData.vvt_templates || vvtData.templates || vvtData || []
}
if (tomRes.ok) {
const tomData = await tomRes.json()
detail.tom_recommendations = tomData.tom_recommendations || tomData.recommendations || tomData || []
}
if (risksRes.ok) {
const risksData = await risksRes.json()
detail.risk_scenarios = risksData.risk_scenarios || risksData.scenarios || risksData || []
}
setSelectedDetail(detail)
} catch (err) {
console.error('Failed to load industry detail:', err)
setDetailError('Details konnten nicht geladen werden. Bitte versuchen Sie es erneut.')
} finally {
setDetailLoading(false)
}
}, [])
useEffect(() => {
loadIndustries()
}, [loadIndustries])
// ---------------------------------------------------------------------------
// Handlers
// ---------------------------------------------------------------------------
const handleBackToGrid = useCallback(() => {
setSelectedSlug(null)
setSelectedDetail(null)
setDetailError(null)
}, [])
const handleApplyPackage = useCallback(async () => {
if (!selectedDetail) return
setApplying(true)
try {
// Placeholder: In production this would POST to an import endpoint
await new Promise((resolve) => setTimeout(resolve, 1500))
setToastMessage(
`Branchenpaket "${selectedDetail.name}" wurde erfolgreich angewendet. ` +
`${selectedDetail.vvt_templates?.length || 0} VVT-Vorlagen, ` +
`${selectedDetail.tom_recommendations?.length || 0} TOM-Empfehlungen und ` +
`${selectedDetail.risk_scenarios?.length || 0} Risiko-Szenarien wurden importiert.`
)
} catch {
setToastMessage('Fehler beim Anwenden des Branchenpakets. Bitte versuchen Sie es erneut.')
} finally {
setApplying(false)
}
}, [selectedDetail])
// Auto-dismiss toast
useEffect(() => {
if (!toastMessage) return
const timer = setTimeout(() => setToastMessage(null), 6000)
return () => clearTimeout(timer)
}, [toastMessage])
// ---------------------------------------------------------------------------
// Render: Header
// ---------------------------------------------------------------------------
const renderHeader = () => (
<div className="bg-white rounded-xl shadow-sm border border-slate-200 p-6">
<div className="flex items-center gap-3">
<div className="w-10 h-10 rounded-lg bg-gradient-to-br from-emerald-500 to-teal-600 flex items-center justify-center text-white text-lg">
{'\uD83C\uDFED'}
</div>
<div>
<h1 className="text-2xl font-bold text-slate-900">Branchenspezifische Module</h1>
<p className="text-slate-500 mt-0.5">
Vorkonfigurierte Compliance-Pakete nach Branche
</p>
</div>
</div>
</div>
)
// ---------------------------------------------------------------------------
// Render: Error
// ---------------------------------------------------------------------------
const renderError = (message: string, onRetry: () => void) => (
<div className="bg-red-50 border border-red-200 rounded-xl p-5 flex items-start gap-3">
<svg className="w-5 h-5 text-red-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div className="flex-1">
<p className="text-red-700 font-medium">Fehler</p>
<p className="text-red-600 text-sm mt-1">{message}</p>
</div>
<button
onClick={onRetry}
className="px-4 py-1.5 text-sm font-medium text-red-700 bg-red-100 hover:bg-red-200 rounded-lg transition-colors"
>
Erneut versuchen
</button>
</div>
)
// ---------------------------------------------------------------------------
// Render: Industry Grid
// ---------------------------------------------------------------------------
const renderGrid = () => (
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{industries.map((industry) => (
<button
key={industry.slug}
onClick={() => loadDetail(industry.slug)}
className="bg-white rounded-xl border border-slate-200 p-6 text-left hover:border-emerald-300 hover:shadow-md transition-all duration-200 group"
>
<div className="flex items-start gap-4">
<div className="w-14 h-14 rounded-xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 flex items-center justify-center text-3xl flex-shrink-0 group-hover:from-emerald-100 group-hover:to-teal-100 transition-colors">
{industry.icon}
</div>
<div className="flex-1 min-w-0">
<h3 className="text-lg font-semibold text-slate-900 group-hover:text-emerald-700 transition-colors">
{industry.name}
</h3>
<p className="text-sm text-slate-500 mt-1 line-clamp-2">
{industry.description}
</p>
</div>
</div>
<div className="flex flex-wrap gap-2 mt-4">
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-100">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
{industry.regulation_count} Regulierungen
</span>
<span className="inline-flex items-center gap-1.5 px-3 py-1 rounded-full text-xs font-medium bg-teal-50 text-teal-700 border border-teal-100">
<svg className="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 5a1 1 0 011-1h14a1 1 0 011 1v2a1 1 0 01-1 1H5a1 1 0 01-1-1V5zM4 13a1 1 0 011-1h6a1 1 0 011 1v6a1 1 0 01-1 1H5a1 1 0 01-1-1v-6zM16 13a1 1 0 011-1h2a1 1 0 011 1v6a1 1 0 01-1 1h-2a1 1 0 01-1-1v-6z" />
</svg>
{industry.template_count} Vorlagen
</span>
</div>
</button>
))}
</div>
)
// ---------------------------------------------------------------------------
// Render: Detail View - Header
// ---------------------------------------------------------------------------
const renderDetailHeader = () => {
if (!selectedDetail) return null
return (
<div className="bg-white rounded-xl border border-slate-200 p-6">
<button
onClick={handleBackToGrid}
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors mb-4"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
<div className="flex items-start gap-4">
<div className="w-16 h-16 rounded-xl bg-gradient-to-br from-emerald-50 to-teal-50 border border-emerald-100 flex items-center justify-center text-4xl flex-shrink-0">
{selectedDetail.icon}
</div>
<div className="flex-1">
<h2 className="text-xl font-bold text-slate-900">{selectedDetail.name}</h2>
<p className="text-slate-500 mt-1">{selectedDetail.description}</p>
</div>
</div>
{/* Regulation Badges */}
{selectedDetail.regulations && selectedDetail.regulations.length > 0 && (
<div className="mt-4">
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-2">
Relevante Regulierungen
</p>
<div className="flex flex-wrap gap-2">
{selectedDetail.regulations.map((reg) => (
<span
key={reg}
className="inline-flex items-center px-3 py-1 rounded-full text-xs font-medium bg-emerald-50 text-emerald-700 border border-emerald-200"
>
{reg}
</span>
))}
</div>
</div>
)}
{/* Summary stats */}
<div className="grid grid-cols-3 gap-4 mt-5 pt-5 border-t border-slate-100">
<div className="text-center">
<p className="text-2xl font-bold text-emerald-600">
{selectedDetail.vvt_templates?.length || 0}
</p>
<p className="text-xs text-slate-500 mt-0.5">VVT-Vorlagen</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-teal-600">
{selectedDetail.tom_recommendations?.length || 0}
</p>
<p className="text-xs text-slate-500 mt-0.5">TOM-Empfehlungen</p>
</div>
<div className="text-center">
<p className="text-2xl font-bold text-amber-600">
{selectedDetail.risk_scenarios?.length || 0}
</p>
<p className="text-xs text-slate-500 mt-0.5">Risiko-Szenarien</p>
</div>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Render: VVT Tab
// ---------------------------------------------------------------------------
const renderVVTTab = () => {
const templates = selectedDetail?.vvt_templates || []
if (templates.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<p className="text-lg">Keine VVT-Vorlagen verfuegbar</p>
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine Verarbeitungsvorlagen definiert.</p>
</div>
)
}
return (
<div className="space-y-4">
{templates.map((vvt, idx) => (
<div
key={idx}
className="bg-white border border-slate-200 rounded-lg p-5 hover:border-emerald-200 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<div className="flex-1">
<h4 className="font-semibold text-slate-900">{vvt.name}</h4>
<p className="text-sm text-slate-500 mt-1">{vvt.purpose}</p>
</div>
<span className="inline-flex items-center px-2.5 py-1 rounded-md text-xs font-medium bg-slate-100 text-slate-600 flex-shrink-0">
{vvt.retention_period}
</span>
</div>
<div className="mt-3 pt-3 border-t border-slate-100">
<div className="grid grid-cols-1 sm:grid-cols-2 gap-3">
{/* Legal Basis */}
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1">Rechtsgrundlage</p>
<p className="text-sm text-slate-700">{vvt.legal_basis}</p>
</div>
{/* Retention Period (mobile only, since shown in badge on desktop) */}
<div className="sm:hidden">
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1">Aufbewahrungsfrist</p>
<p className="text-sm text-slate-700">{vvt.retention_period}</p>
</div>
{/* Data Categories */}
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">Datenkategorien</p>
<div className="flex flex-wrap gap-1.5">
{vvt.data_categories.map((cat) => (
<span
key={cat}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-emerald-50 text-emerald-700 border border-emerald-100"
>
{cat}
</span>
))}
</div>
</div>
{/* Data Subjects */}
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider mb-1.5">Betroffene</p>
<div className="flex flex-wrap gap-1.5">
{vvt.data_subjects.map((sub) => (
<span
key={sub}
className="inline-flex items-center px-2 py-0.5 rounded text-xs bg-teal-50 text-teal-700 border border-teal-100"
>
{sub}
</span>
))}
</div>
</div>
</div>
</div>
</div>
))}
</div>
)
}
// ---------------------------------------------------------------------------
// Render: TOM Tab
// ---------------------------------------------------------------------------
const renderTOMTab = () => {
const recommendations = selectedDetail?.tom_recommendations || []
if (recommendations.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<p className="text-lg">Keine TOM-Empfehlungen verfuegbar</p>
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine technisch-organisatorischen Massnahmen definiert.</p>
</div>
)
}
// Group by category
const grouped: Record<string, TOMRecommendation[]> = {}
recommendations.forEach((tom) => {
if (!grouped[tom.category]) {
grouped[tom.category] = []
}
grouped[tom.category].push(tom)
})
return (
<div className="space-y-6">
{Object.entries(grouped).map(([category, items]) => {
const icon = TOM_CATEGORY_ICONS[category] || '\uD83D\uDD27'
return (
<div key={category}>
<div className="flex items-center gap-2 mb-3">
<span className="text-lg">{icon}</span>
<h4 className="font-semibold text-slate-800">{category}</h4>
<span className="text-xs text-slate-400 ml-1">({items.length})</span>
</div>
<div className="space-y-3 ml-7">
{items.map((tom, idx) => {
const prio = PRIORITY_COLORS[tom.priority] || PRIORITY_COLORS.medium
const prioLabel = PRIORITY_LABELS[tom.priority] || tom.priority
return (
<div
key={idx}
className="bg-white border border-slate-200 rounded-lg p-4 hover:border-emerald-200 transition-colors"
>
<div className="flex items-start justify-between gap-3">
<div className="flex-1">
<h5 className="font-medium text-slate-900">{tom.name}</h5>
<p className="text-sm text-slate-500 mt-1">{tom.description}</p>
</div>
<span
className={`inline-flex items-center px-2.5 py-0.5 rounded-full text-xs font-semibold border flex-shrink-0 ${prio.bg} ${prio.text} ${prio.border}`}
>
{prioLabel}
</span>
</div>
</div>
)
})}
</div>
</div>
)
})}
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Risk Tab
// ---------------------------------------------------------------------------
const renderRiskTab = () => {
const scenarios = selectedDetail?.risk_scenarios || []
if (scenarios.length === 0) {
return (
<div className="text-center py-12 text-slate-400">
<p className="text-lg">Keine Risiko-Szenarien verfuegbar</p>
<p className="text-sm mt-1">Fuer diese Branche wurden noch keine Risiko-Szenarien definiert.</p>
</div>
)
}
return (
<div className="space-y-4">
{scenarios.map((risk, idx) => {
const likelihoodColor = LIKELIHOOD_COLORS[risk.likelihood] || 'bg-slate-400'
const impactColor = IMPACT_COLORS[risk.impact] || 'bg-slate-400'
const likelihoodLabel = PRIORITY_LABELS[risk.likelihood] || risk.likelihood
const impactLabel = PRIORITY_LABELS[risk.impact] || risk.impact
return (
<div
key={idx}
className="bg-white border border-slate-200 rounded-lg p-5 hover:border-emerald-200 transition-colors"
>
<div className="flex items-start justify-between gap-4">
<h4 className="font-semibold text-slate-900">{risk.name}</h4>
<div className="flex items-center gap-2 flex-shrink-0">
{/* Likelihood badge */}
<div className="flex items-center gap-1.5">
<span className={`w-2.5 h-2.5 rounded-full ${likelihoodColor}`} />
<span className="text-xs text-slate-500">
Wahrsch.: <span className="font-medium text-slate-700">{likelihoodLabel}</span>
</span>
</div>
<span className="text-slate-300">|</span>
{/* Impact badge */}
<div className="flex items-center gap-1.5">
<span className={`w-2.5 h-2.5 rounded-full ${impactColor}`} />
<span className="text-xs text-slate-500">
Auswirkung: <span className="font-medium text-slate-700">{impactLabel}</span>
</span>
</div>
</div>
</div>
<p className="text-sm text-slate-500 mt-2">{risk.description}</p>
{/* Mitigation */}
<div className="mt-3 pt-3 border-t border-slate-100">
<div className="flex items-start gap-2">
<svg className="w-4 h-4 text-emerald-500 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
<div>
<p className="text-xs font-medium text-slate-400 uppercase tracking-wider">Massnahme</p>
<p className="text-sm text-slate-700 mt-0.5">{risk.mitigation}</p>
</div>
</div>
</div>
</div>
)
})}
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Detail Tabs + Content
// ---------------------------------------------------------------------------
const renderDetailContent = () => {
if (!selectedDetail) return null
return (
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
{/* Tab Navigation */}
<div className="flex border-b border-slate-200 bg-slate-50">
{DETAIL_TABS.map((tab) => {
const isActive = activeTab === tab.key
let count = 0
if (tab.key === 'vvt') count = selectedDetail.vvt_templates?.length || 0
if (tab.key === 'tom') count = selectedDetail.tom_recommendations?.length || 0
if (tab.key === 'risks') count = selectedDetail.risk_scenarios?.length || 0
return (
<button
key={tab.key}
onClick={() => setActiveTab(tab.key)}
className={`flex-1 px-4 py-3 text-sm font-medium transition-colors relative ${
isActive
? 'text-emerald-700 bg-white border-b-2 border-emerald-500'
: 'text-slate-500 hover:text-slate-700 hover:bg-slate-100'
}`}
>
{tab.label}
{count > 0 && (
<span
className={`ml-1.5 inline-flex items-center justify-center px-1.5 py-0.5 rounded-full text-xs ${
isActive
? 'bg-emerald-100 text-emerald-700'
: 'bg-slate-200 text-slate-500'
}`}
>
{count}
</span>
)}
</button>
)
})}
</div>
{/* Tab Content */}
<div className="p-6">
{activeTab === 'vvt' && renderVVTTab()}
{activeTab === 'tom' && renderTOMTab()}
{activeTab === 'risks' && renderRiskTab()}
</div>
{/* Apply Button */}
<div className="px-6 py-4 border-t border-slate-200 bg-slate-50">
<div className="flex items-center justify-between">
<p className="text-sm text-slate-500">
Importiert alle Vorlagen, Empfehlungen und Szenarien in Ihr System.
</p>
<button
onClick={handleApplyPackage}
disabled={applying}
className={`inline-flex items-center gap-2 px-5 py-2.5 rounded-lg text-sm font-semibold text-white transition-all ${
applying
? 'bg-emerald-400 cursor-not-allowed'
: 'bg-gradient-to-r from-emerald-500 to-teal-600 hover:from-emerald-600 hover:to-teal-700 shadow-sm hover:shadow-md'
}`}
>
{applying ? (
<>
<svg className="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z" />
</svg>
Wird angewendet...
</>
) : (
<>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 16v1a3 3 0 003 3h10a3 3 0 003-3v-1m-4-4l-4 4m0 0l-4-4m4 4V4" />
</svg>
Branchenpaket anwenden
</>
)}
</button>
</div>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Toast
// ---------------------------------------------------------------------------
const renderToast = () => {
if (!toastMessage) return null
return (
<div className="fixed bottom-6 right-6 z-50 max-w-md animate-slide-up">
<div className="bg-slate-900 text-white rounded-xl shadow-2xl px-5 py-4 flex items-start gap-3">
<svg className="w-5 h-5 text-emerald-400 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<p className="text-sm leading-relaxed">{toastMessage}</p>
<button
onClick={() => setToastMessage(null)}
className="text-slate-400 hover:text-white flex-shrink-0 ml-2"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
)
}
// ---------------------------------------------------------------------------
// Render: Empty state
// ---------------------------------------------------------------------------
const renderEmptyState = () => (
<div className="bg-white rounded-xl border border-slate-200 p-12 text-center">
<div className="w-16 h-16 rounded-2xl bg-emerald-50 flex items-center justify-center text-3xl mx-auto mb-4">
{'\uD83C\uDFED'}
</div>
<h3 className="text-lg font-semibold text-slate-900">Keine Branchenvorlagen verfuegbar</h3>
<p className="text-slate-500 mt-2 max-w-md mx-auto">
Es sind derzeit keine branchenspezifischen Compliance-Pakete im System hinterlegt.
Bitte kontaktieren Sie den Administrator oder versuchen Sie es spaeter erneut.
</p>
<button
onClick={loadIndustries}
className="mt-4 px-4 py-2 text-sm font-medium text-emerald-700 bg-emerald-50 hover:bg-emerald-100 rounded-lg transition-colors"
>
Erneut laden
</button>
</div>
)
// ---------------------------------------------------------------------------
// Main Render
// ---------------------------------------------------------------------------
return (
<div className="space-y-6">
{/* Inline keyframe for toast animation */}
<style>{`
@keyframes slide-up {
from { opacity: 0; transform: translateY(16px); }
to { opacity: 1; transform: translateY(0); }
}
.animate-slide-up {
animation: slide-up 0.3s ease-out;
}
`}</style>
{renderHeader()}
{/* Error state */}
{error && renderError(error, loadIndustries)}
{/* Main Content */}
{loading ? (
selectedSlug ? <DetailSkeleton /> : <GridSkeleton />
) : selectedSlug ? (
// Detail View
<div className="space-y-6">
{detailLoading ? (
<DetailSkeleton />
) : detailError ? (
<>
<button
onClick={handleBackToGrid}
className="inline-flex items-center gap-1.5 text-sm text-slate-500 hover:text-emerald-600 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zur Uebersicht
</button>
{renderError(detailError, () => loadDetail(selectedSlug))}
</>
) : (
<>
{renderDetailHeader()}
{renderDetailContent()}
</>
)}
</div>
) : industries.length === 0 && !error ? (
renderEmptyState()
) : (
renderGrid()
)}
{/* Toast notification */}
{renderToast()}
</div>
)
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,225 @@
/**
* 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`
interface RAGResult {
content: string
source_name: string
source_code: string
attribution_text: string
score: number
}
/**
* Query the DSFA RAG corpus for relevant documents
*/
async function queryRAG(query: string): Promise<string> {
try {
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/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: RAGResult, i: number) =>
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${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: 8192,
},
}),
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 }
)
}
}

View File

@@ -0,0 +1,184 @@
/**
* Drafting Engine Chat API
*
* Verbindet das DraftingEngineWidget mit dem LLM Backend.
* Unterstuetzt alle 4 Modi: explain, ask, draft, validate.
* Nutzt State-Projection fuer token-effiziente Kontextgabe.
*/
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/drafting-agent.soul.md)
const DRAFTING_SYSTEM_PROMPT = `# Drafting Agent - Compliance-Dokumententwurf
## Identitaet
Du bist der BreakPilot Drafting Agent. Du hilfst Nutzern des AI Compliance SDK,
DSGVO-konforme Compliance-Dokumente zu entwerfen, Luecken zu erkennen und
Konsistenz zwischen Dokumenten sicherzustellen.
## Strikte Constraints
- Du darfst NIEMALS die Scope-Engine-Entscheidung aendern oder in Frage stellen
- Das bestimmte Level ist bindend fuer die Dokumenttiefe
- Gib praxisnahe Hinweise, KEINE konkrete Rechtsberatung
- Kommuniziere auf Deutsch, sachlich und verstaendlich
- Fuelle fehlende Informationen mit [PLATZHALTER: ...] Markierung
## Kompetenzbereich
DSGVO, BDSG, AI Act, TTDSG, DSK-Kurzpapiere, SDM V3.0, BSI-Grundschutz, ISO 27001/27701, EDPB Guidelines, WP248`
/**
* Query the RAG corpus for relevant documents
*/
async function queryRAG(query: string): Promise<string> {
try {
const url = `${KLAUSUR_SERVICE_URL}/api/v1/dsfa-rag/search?query=${encodeURIComponent(query)}&top_k=3`
const res = await fetch(url, {
headers: { 'Content-Type': 'application/json' },
signal: AbortSignal.timeout(10000),
})
if (!res.ok) return ''
const data = await res.json()
if (data.results?.length > 0) {
return data.results
.map(
(r: { source_name?: string; source_code?: string; content?: string }, i: number) =>
`[Quelle ${i + 1}: ${r.source_name || r.source_code || 'Unbekannt'}]\n${r.content || ''}`
)
.join('\n\n---\n\n')
}
return ''
} catch {
return ''
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const {
message,
history = [],
sdkStateProjection,
mode = 'explain',
documentType,
} = body
if (!message || typeof message !== 'string') {
return NextResponse.json({ error: 'Message is required' }, { status: 400 })
}
// 1. Query RAG for legal context
const ragContext = await queryRAG(message)
// 2. Build system prompt with mode-specific instructions + state projection
let systemContent = DRAFTING_SYSTEM_PROMPT
// Mode-specific instructions
const modeInstructions: Record<string, string> = {
explain: '\n\n## Aktueller Modus: EXPLAIN\nBeantworte Fragen verstaendlich mit Quellenangaben.',
ask: '\n\n## Aktueller Modus: ASK\nAnalysiere Luecken und stelle gezielte Fragen. Eine Frage pro Antwort.',
draft: `\n\n## Aktueller Modus: DRAFT\nEntwirf strukturierte Dokument-Sections. Dokumenttyp: ${documentType || 'nicht spezifiziert'}.\nAntworte mit JSON wenn ein Draft angefragt wird.`,
validate: '\n\n## Aktueller Modus: VALIDATE\nPruefe Cross-Dokument-Konsistenz. Gib Errors, Warnings und Suggestions zurueck.',
}
systemContent += modeInstructions[mode] || modeInstructions.explain
// Add state projection context
if (sdkStateProjection) {
systemContent += `\n\n## SDK-State Projektion (${mode}-Kontext)\n${JSON.stringify(sdkStateProjection, null, 0).slice(0, 3000)}`
}
// Add RAG context
if (ragContext) {
systemContent += `\n\n## Relevanter Rechtskontext\n${ragContext}`
}
// 3. Build messages array
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 LLM 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: mode === 'draft' ? 0.2 : 0.3,
num_predict: mode === 'draft' ? 16384 : 8192,
},
}),
signal: AbortSignal.timeout(120000),
})
if (!ollamaResponse.ok) {
const errorText = await ollamaResponse.text()
console.error('LLM error:', ollamaResponse.status, errorText)
return NextResponse.json(
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
{ status: 502 }
)
}
// 5. Stream response back
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, skip
}
}
}
} catch (error) {
console.error('Stream 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('Drafting engine chat error:', error)
return NextResponse.json(
{ error: 'Verbindung zum LLM fehlgeschlagen.' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,168 @@
/**
* Drafting Engine - Draft API
*
* Erstellt strukturierte Compliance-Dokument-Entwuerfe.
* Baut dokument-spezifische Prompts aus SOUL-Template + State-Projection.
* Gibt strukturiertes JSON zurueck.
*/
import { NextRequest, NextResponse } from 'next/server'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
// Import prompt builders
import { buildVVTDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-vvt'
import { buildTOMDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-tom'
import { buildDSFADraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-dsfa'
import { buildPrivacyPolicyDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-privacy-policy'
import { buildLoeschfristenDraftPrompt } from '@/lib/sdk/drafting-engine/prompts/draft-loeschfristen'
import type { DraftContext, DraftResponse, DraftRevision, DraftSection } from '@/lib/sdk/drafting-engine/types'
import type { ScopeDocumentType } from '@/lib/sdk/compliance-scope-types'
import { ConstraintEnforcer } from '@/lib/sdk/drafting-engine/constraint-enforcer'
const constraintEnforcer = new ConstraintEnforcer()
const DRAFTING_SYSTEM_PROMPT = `Du bist ein DSGVO-Compliance-Experte und erstellst strukturierte Dokument-Entwuerfe.
Du MUSST immer im JSON-Format antworten mit einem "sections" Array.
Jede Section hat: id, title, content, schemaField.
Halte die Tiefe strikt am vorgegebenen Level.
Markiere fehlende Informationen mit [PLATZHALTER: Beschreibung].
Sprache: Deutsch.`
function buildPromptForDocumentType(
documentType: ScopeDocumentType,
context: DraftContext,
instructions?: string
): string {
switch (documentType) {
case 'vvt':
return buildVVTDraftPrompt({ context, instructions })
case 'tom':
return buildTOMDraftPrompt({ context, instructions })
case 'dsfa':
return buildDSFADraftPrompt({ context, instructions })
case 'dsi':
return buildPrivacyPolicyDraftPrompt({ context, instructions })
case 'lf':
return buildLoeschfristenDraftPrompt({ context, instructions })
default:
return `## Aufgabe: Entwurf fuer ${documentType}
### Level: ${context.decisions.level}
### Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte:
${context.constraints.depthRequirements.detailItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${instructions ? `### Anweisungen: ${instructions}` : ''}
Antworte als JSON mit "sections" Array.`
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { documentType, draftContext, instructions, existingDraft } = body
if (!documentType || !draftContext) {
return NextResponse.json(
{ error: 'documentType und draftContext sind erforderlich' },
{ status: 400 }
)
}
// 1. Constraint Check (Hard Gate)
const constraintCheck = constraintEnforcer.checkFromContext(documentType, draftContext)
if (!constraintCheck.allowed) {
return NextResponse.json({
draft: null,
constraintCheck,
tokensUsed: 0,
error: 'Constraint-Verletzung: ' + constraintCheck.violations.join('; '),
}, { status: 403 })
}
// 2. Build document-specific prompt
const draftPrompt = buildPromptForDocumentType(documentType, draftContext, instructions)
// 3. Build messages
const messages = [
{ role: 'system', content: DRAFTING_SYSTEM_PROMPT },
...(existingDraft ? [{
role: 'assistant',
content: `Bisheriger Entwurf:\n${JSON.stringify(existingDraft.sections, null, 2)}`,
}] : []),
{ role: 'user', content: draftPrompt },
]
// 4. Call LLM (non-streaming for structured output)
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: LLM_MODEL,
messages,
stream: false,
options: {
temperature: 0.15,
num_predict: 16384,
},
format: 'json',
}),
signal: AbortSignal.timeout(180000),
})
if (!ollamaResponse.ok) {
return NextResponse.json(
{ error: `LLM nicht erreichbar (Status ${ollamaResponse.status})` },
{ status: 502 }
)
}
const result = await ollamaResponse.json()
const content = result.message?.content || ''
// 5. Parse JSON response
let sections: DraftSection[] = []
try {
const parsed = JSON.parse(content)
sections = (parsed.sections || []).map((s: Record<string, unknown>, i: number) => ({
id: String(s.id || `section-${i}`),
title: String(s.title || ''),
content: String(s.content || ''),
schemaField: s.schemaField ? String(s.schemaField) : undefined,
}))
} catch {
// If not JSON, wrap raw content as single section
sections = [{
id: 'raw',
title: 'Entwurf',
content: content,
}]
}
const draft: DraftRevision = {
id: `draft-${Date.now()}`,
content: sections.map(s => `## ${s.title}\n\n${s.content}`).join('\n\n'),
sections,
createdAt: new Date().toISOString(),
instruction: instructions,
}
const response: DraftResponse = {
draft,
constraintCheck,
tokensUsed: result.eval_count || 0,
}
return NextResponse.json(response)
} catch (error) {
console.error('Draft generation error:', error)
return NextResponse.json(
{ error: 'Draft-Generierung fehlgeschlagen.' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,188 @@
/**
* Drafting Engine - Validate API
*
* Stufe 1: Deterministische Pruefung gegen DOCUMENT_SCOPE_MATRIX
* Stufe 2: LLM Cross-Consistency Check
*/
import { NextRequest, NextResponse } from 'next/server'
import { DOCUMENT_SCOPE_MATRIX, DOCUMENT_TYPE_LABELS, getDepthLevelNumeric } from '@/lib/sdk/compliance-scope-types'
import type { ScopeDocumentType, ComplianceDepthLevel } from '@/lib/sdk/compliance-scope-types'
import type { ValidationContext, ValidationResult, ValidationFinding } from '@/lib/sdk/drafting-engine/types'
import { buildCrossCheckPrompt } from '@/lib/sdk/drafting-engine/prompts/validate-cross-check'
const OLLAMA_URL = process.env.OLLAMA_URL || 'http://host.docker.internal:11434'
const LLM_MODEL = process.env.COMPLIANCE_LLM_MODEL || 'qwen2.5vl:32b'
/**
* Stufe 1: Deterministische Pruefung
*/
function deterministicCheck(
documentType: ScopeDocumentType,
validationContext: ValidationContext
): ValidationFinding[] {
const findings: ValidationFinding[] = []
const level = validationContext.scopeLevel
const levelNumeric = getDepthLevelNumeric(level)
const req = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
// Check 1: Ist das Dokument auf diesem Level erforderlich?
if (req && !req.required && levelNumeric < 3) {
findings.push({
id: `DET-OPT-${documentType}`,
severity: 'suggestion',
category: 'scope_violation',
title: `${DOCUMENT_TYPE_LABELS[documentType] ?? documentType} ist optional`,
description: `Auf Level ${level} ist dieses Dokument nicht verpflichtend.`,
documentType,
})
}
// Check 2: VVT vorhanden wenn erforderlich?
const vvtReq = DOCUMENT_SCOPE_MATRIX.vvt[level]
if (vvtReq.required && validationContext.crossReferences.vvtCategories.length === 0) {
findings.push({
id: 'DET-VVT-MISSING',
severity: 'error',
category: 'missing_content',
title: 'VVT fehlt',
description: `Auf Level ${level} ist ein VVT Pflicht, aber keine Eintraege vorhanden.`,
documentType: 'vvt',
legalReference: 'Art. 30 DSGVO',
})
}
// Check 3: TOM vorhanden wenn VVT existiert?
if (validationContext.crossReferences.vvtCategories.length > 0
&& validationContext.crossReferences.tomControls.length === 0) {
findings.push({
id: 'DET-TOM-MISSING-FOR-VVT',
severity: 'warning',
category: 'cross_reference',
title: 'TOM fehlt bei vorhandenem VVT',
description: 'VVT-Eintraege existieren, aber keine TOM-Massnahmen sind definiert.',
documentType: 'tom',
crossReferenceType: 'vvt',
legalReference: 'Art. 32 DSGVO',
suggestion: 'TOM-Massnahmen erstellen, die die VVT-Taetigkeiten absichern.',
})
}
// Check 4: Loeschfristen fuer VVT-Kategorien
if (validationContext.crossReferences.vvtCategories.length > 0
&& validationContext.crossReferences.retentionCategories.length === 0) {
findings.push({
id: 'DET-LF-MISSING-FOR-VVT',
severity: 'warning',
category: 'cross_reference',
title: 'Loeschfristen fehlen',
description: 'VVT-Eintraege existieren, aber keine Loeschfristen sind definiert.',
documentType: 'lf',
crossReferenceType: 'vvt',
legalReference: 'Art. 17 DSGVO',
suggestion: 'Loeschfristen fuer alle VVT-Datenkategorien definieren.',
})
}
return findings
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { documentType, draftContent, validationContext } = body
if (!documentType || !validationContext) {
return NextResponse.json(
{ error: 'documentType und validationContext sind erforderlich' },
{ status: 400 }
)
}
// ---------------------------------------------------------------
// Stufe 1: Deterministische Pruefung
// ---------------------------------------------------------------
const deterministicFindings = deterministicCheck(documentType, validationContext)
// ---------------------------------------------------------------
// Stufe 2: LLM Cross-Consistency Check
// ---------------------------------------------------------------
let llmFindings: ValidationFinding[] = []
try {
const crossCheckPrompt = buildCrossCheckPrompt({
context: validationContext,
})
const ollamaResponse = await fetch(`${OLLAMA_URL}/api/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: LLM_MODEL,
messages: [
{
role: 'system',
content: 'Du bist ein DSGVO-Compliance-Validator. Antworte NUR im JSON-Format.',
},
{ role: 'user', content: crossCheckPrompt },
],
stream: false,
options: { temperature: 0.1, num_predict: 8192 },
format: 'json',
}),
signal: AbortSignal.timeout(120000),
})
if (ollamaResponse.ok) {
const result = await ollamaResponse.json()
try {
const parsed = JSON.parse(result.message?.content || '{}')
llmFindings = [
...(parsed.errors || []),
...(parsed.warnings || []),
...(parsed.suggestions || []),
].map((f: Record<string, unknown>, i: number) => ({
id: String(f.id || `LLM-${i}`),
severity: (String(f.severity || 'suggestion')) as 'error' | 'warning' | 'suggestion',
category: (String(f.category || 'inconsistency')) as ValidationFinding['category'],
title: String(f.title || ''),
description: String(f.description || ''),
documentType: (String(f.documentType || documentType)) as ScopeDocumentType,
crossReferenceType: f.crossReferenceType ? String(f.crossReferenceType) as ScopeDocumentType : undefined,
legalReference: f.legalReference ? String(f.legalReference) : undefined,
suggestion: f.suggestion ? String(f.suggestion) : undefined,
}))
} catch {
// LLM response not parseable, skip
}
}
} catch {
// LLM unavailable, continue with deterministic results only
}
// ---------------------------------------------------------------
// Combine results
// ---------------------------------------------------------------
const allFindings = [...deterministicFindings, ...llmFindings]
const errors = allFindings.filter(f => f.severity === 'error')
const warnings = allFindings.filter(f => f.severity === 'warning')
const suggestions = allFindings.filter(f => f.severity === 'suggestion')
const result: ValidationResult = {
passed: errors.length === 0,
timestamp: new Date().toISOString(),
scopeLevel: validationContext.scopeLevel,
errors,
warnings,
suggestions,
}
return NextResponse.json(result)
} catch (error) {
console.error('Validation error:', error)
return NextResponse.json(
{ error: 'Validierung fehlgeschlagen.' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,136 @@
/**
* Academy API Proxy - Catch-all route
* Proxies all /api/sdk/v1/academy/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/academy`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const authHeader = request.headers.get('authorization')
if (authHeader) {
headers['Authorization'] = authHeader
}
const tenantHeader = request.headers.get('x-tenant-id')
if (tenantHeader) {
headers['X-Tenant-Id'] = tenantHeader
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body - continue without
}
}
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (e.g., PDF certificates)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Academy API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,235 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Checkpoints API
*
* GET /api/sdk/v1/checkpoints - Get all checkpoint statuses
* POST /api/sdk/v1/checkpoints - Validate a checkpoint
*/
// Checkpoint definitions
const CHECKPOINTS = {
'CP-PROF': {
id: 'CP-PROF',
step: 'company-profile',
name: 'Unternehmensprofil Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-UC': {
id: 'CP-UC',
step: 'use-case-assessment',
name: 'Anwendungsfall Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-SCAN': {
id: 'CP-SCAN',
step: 'screening',
name: 'Screening Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-MOD': {
id: 'CP-MOD',
step: 'modules',
name: 'Modules Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-REQ': {
id: 'CP-REQ',
step: 'requirements',
name: 'Requirements Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-CTRL': {
id: 'CP-CTRL',
step: 'controls',
name: 'Controls Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
'CP-EVI': {
id: 'CP-EVI',
step: 'evidence',
name: 'Evidence Checkpoint',
type: 'RECOMMENDED',
blocksProgress: false,
requiresReview: 'NONE',
},
'CP-CHK': {
id: 'CP-CHK',
step: 'audit-checklist',
name: 'Checklist Checkpoint',
type: 'RECOMMENDED',
blocksProgress: false,
requiresReview: 'NONE',
},
'CP-RISK': {
id: 'CP-RISK',
step: 'risks',
name: 'Risk Matrix Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
'CP-AI': {
id: 'CP-AI',
step: 'ai-act',
name: 'AI Act Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'LEGAL',
},
'CP-DSFA': {
id: 'CP-DSFA',
step: 'dsfa',
name: 'DSFA Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
'CP-TOM': {
id: 'CP-TOM',
step: 'tom',
name: 'TOMs Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'NONE',
},
'CP-VVT': {
id: 'CP-VVT',
step: 'vvt',
name: 'VVT Checkpoint',
type: 'REQUIRED',
blocksProgress: true,
requiresReview: 'DSB',
},
}
export async function GET() {
try {
return NextResponse.json({
success: true,
checkpoints: CHECKPOINTS,
count: Object.keys(CHECKPOINTS).length,
})
} catch (error) {
console.error('Failed to get checkpoints:', error)
return NextResponse.json(
{ error: 'Failed to get checkpoints' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { checkpointId, state, context } = body
if (!checkpointId) {
return NextResponse.json(
{ error: 'checkpointId is required' },
{ status: 400 }
)
}
const checkpoint = CHECKPOINTS[checkpointId as keyof typeof CHECKPOINTS]
if (!checkpoint) {
return NextResponse.json(
{ error: 'Checkpoint not found', checkpointId },
{ status: 404 }
)
}
// Perform validation based on checkpoint
const errors: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
const warnings: Array<{ ruleId: string; field: string; message: string; severity: string }> = []
// Basic validation rules
switch (checkpointId) {
case 'CP-UC':
if (!state?.useCases || state.useCases.length === 0) {
errors.push({
ruleId: 'uc-min-count',
field: 'useCases',
message: 'Mindestens ein Use Case muss erstellt werden',
severity: 'ERROR',
})
}
break
case 'CP-SCAN':
if (!state?.screening || state.screening.status !== 'COMPLETED') {
errors.push({
ruleId: 'scan-complete',
field: 'screening',
message: 'Security Scan muss abgeschlossen sein',
severity: 'ERROR',
})
}
break
case 'CP-MOD':
if (!state?.modules || state.modules.length === 0) {
errors.push({
ruleId: 'mod-min-count',
field: 'modules',
message: 'Mindestens ein Modul muss zugewiesen werden',
severity: 'ERROR',
})
}
break
case 'CP-RISK':
if (state?.risks) {
const criticalRisks = state.risks.filter(
(r: { severity: string; mitigation: unknown[] }) =>
(r.severity === 'CRITICAL' || r.severity === 'HIGH') && r.mitigation.length === 0
)
if (criticalRisks.length > 0) {
errors.push({
ruleId: 'critical-risks-mitigated',
field: 'risks',
message: `${criticalRisks.length} kritische Risiken ohne Mitigationsmaßnahmen`,
severity: 'ERROR',
})
}
}
break
}
const passed = errors.length === 0
const result = {
checkpointId,
passed,
validatedAt: new Date().toISOString(),
validatedBy: context?.userId || 'SYSTEM',
errors,
warnings,
checkpoint,
}
return NextResponse.json({
success: true,
...result,
})
} catch (error) {
console.error('Failed to validate checkpoint:', error)
return NextResponse.json(
{ error: 'Failed to validate checkpoint' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,52 @@
/**
* Demo Data Clear API Endpoint
*
* Clears demo data from the storage (same mechanism as real customer data).
*/
import { NextRequest, NextResponse } from 'next/server'
// Shared store reference (same as seed endpoint)
declare global {
// eslint-disable-next-line no-var
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
}
if (!global.demoStateStore) {
global.demoStateStore = new Map()
}
const stateStore = global.demoStateStore
export async function DELETE(request: NextRequest) {
try {
const body = await request.json()
const { tenantId = 'demo-tenant' } = body
const existed = stateStore.has(tenantId)
stateStore.delete(tenantId)
return NextResponse.json({
success: true,
message: existed
? `Demo data cleared for tenant ${tenantId}`
: `No data found for tenant ${tenantId}`,
tenantId,
existed,
})
} catch (error) {
console.error('Failed to clear demo data:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
// Also support POST for clearing (for clients that don't support DELETE)
return DELETE(request)
}

View File

@@ -0,0 +1,77 @@
/**
* Demo Data Seed API Endpoint
*
* This endpoint seeds demo data via the same storage mechanism as real customer data.
* Demo data is NOT hardcoded - it goes through the normal API/database path.
*/
import { NextRequest, NextResponse } from 'next/server'
import { generateDemoState } from '@/lib/sdk/demo-data'
// In-memory store (same as state endpoint - will be replaced with PostgreSQL)
declare global {
// eslint-disable-next-line no-var
var demoStateStore: Map<string, { state: unknown; version: number; updatedAt: Date }> | undefined
}
if (!global.demoStateStore) {
global.demoStateStore = new Map()
}
const stateStore = global.demoStateStore
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId = 'demo-tenant', userId = 'demo-user' } = body
// Generate demo state using the seed data templates
const demoState = generateDemoState(tenantId, userId)
// Store via the same mechanism as real data
const storedState = {
state: demoState,
version: 1,
updatedAt: new Date(),
}
stateStore.set(tenantId, storedState)
return NextResponse.json({
success: true,
message: `Demo data seeded for tenant ${tenantId}`,
tenantId,
version: 1,
})
} catch (error) {
console.error('Failed to seed demo data:', error)
return NextResponse.json(
{
success: false,
error: error instanceof Error ? error.message : 'Unknown error',
},
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId') || 'demo-tenant'
const stored = stateStore.get(tenantId)
if (!stored) {
return NextResponse.json({
hasData: false,
tenantId,
})
}
return NextResponse.json({
hasData: true,
tenantId,
version: stored.version,
updatedAt: stored.updatedAt,
})
}

View File

@@ -0,0 +1,214 @@
import { NextRequest, NextResponse } from 'next/server'
// Types
interface ExtractedSection {
title: string
content: string
type?: string
}
interface ExtractedContent {
title?: string
version?: string
lastModified?: string
sections?: ExtractedSection[]
metadata?: Record<string, string>
}
interface UploadResponse {
success: boolean
documentId: string
filename: string
documentType: string
extractedVersion?: string
extractedContent?: ExtractedContent
suggestedNextVersion?: string
}
// Helper: Detect version from filename
function detectVersionFromFilename(filename: string): string | undefined {
const patterns = [
/[vV](\d+(?:\.\d+)*)/,
/version[_-]?(\d+(?:\.\d+)*)/i,
/[_-]v?(\d+\.\d+(?:\.\d+)?)[_-]/,
]
for (const pattern of patterns) {
const match = filename.match(pattern)
if (match) {
return match[1]
}
}
return undefined
}
// Helper: Suggest next version
function suggestNextVersion(currentVersion?: string): string {
if (!currentVersion) return '1.0'
const parts = currentVersion.split('.').map(Number)
if (parts.length >= 2) {
parts[parts.length - 1] += 1
} else {
parts.push(1)
}
return parts.join('.')
}
// Helper: Extract content from PDF/DOCX (simplified - would need proper libraries in production)
async function extractDocumentContent(
_file: File,
documentType: string
): Promise<ExtractedContent> {
// In production, this would use libraries like:
// - pdf-parse for PDFs
// - mammoth for DOCX
// For now, return mock extracted content based on document type
const mockContentByType: Record<string, ExtractedContent> = {
tom: {
title: 'Technische und Organisatorische Maßnahmen',
sections: [
{ title: 'Vertraulichkeit', content: 'Zugangskontrollen, Zugriffsbeschränkungen...', type: 'category' },
{ title: 'Integrität', content: 'Eingabekontrollen, Änderungsprotokolle...', type: 'category' },
{ title: 'Verfügbarkeit', content: 'Backup-Strategien, Disaster Recovery...', type: 'category' },
{ title: 'Belastbarkeit', content: 'Redundanz, Lasttests...', type: 'category' },
],
metadata: {
lastReview: new Date().toISOString(),
responsible: 'Datenschutzbeauftragter',
},
},
dsfa: {
title: 'Datenschutz-Folgenabschätzung',
sections: [
{ title: 'Beschreibung der Verarbeitung', content: 'Systematische Beschreibung...', type: 'section' },
{ title: 'Erforderlichkeit und Verhältnismäßigkeit', content: 'Bewertung...', type: 'section' },
{ title: 'Risiken für Betroffene', content: 'Risikoanalyse...', type: 'section' },
{ title: 'Abhilfemaßnahmen', content: 'Geplante Maßnahmen...', type: 'section' },
],
},
vvt: {
title: 'Verzeichnis von Verarbeitungstätigkeiten',
sections: [
{ title: 'Verantwortlicher', content: 'Name und Kontaktdaten...', type: 'field' },
{ title: 'Verarbeitungszwecke', content: 'Liste der Zwecke...', type: 'list' },
{ title: 'Datenkategorien', content: 'Personenbezogene Daten...', type: 'list' },
{ title: 'Empfängerkategorien', content: 'Interne und externe Empfänger...', type: 'list' },
],
},
loeschfristen: {
title: 'Löschkonzept und Aufbewahrungsfristen',
sections: [
{ title: 'Personalakten', content: '10 Jahre nach Ausscheiden', type: 'retention' },
{ title: 'Kundendaten', content: '3 Jahre nach letzter Aktivität', type: 'retention' },
{ title: 'Buchhaltungsbelege', content: '10 Jahre (HGB)', type: 'retention' },
{ title: 'Bewerbungsunterlagen', content: '6 Monate nach Absage', type: 'retention' },
],
},
consent: {
title: 'Einwilligungserklärungen',
sections: [
{ title: 'Newsletter-Einwilligung', content: 'Vorlage für Newsletter...', type: 'template' },
{ title: 'Marketing-Einwilligung', content: 'Vorlage für Marketing...', type: 'template' },
],
},
policy: {
title: 'Datenschutzrichtlinie',
sections: [
{ title: 'Geltungsbereich', content: 'Diese Richtlinie gilt für...', type: 'section' },
{ title: 'Verantwortlichkeiten', content: 'Rollen und Pflichten...', type: 'section' },
],
},
}
return mockContentByType[documentType] || {
title: 'Unbekanntes Dokument',
sections: [],
}
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File | null
const documentType = formData.get('documentType') as string || 'custom'
const sessionId = formData.get('sessionId') as string || 'default'
if (!file) {
return NextResponse.json(
{ error: 'Keine Datei hochgeladen' },
{ status: 400 }
)
}
// Validate file type
const allowedTypes = [
'application/pdf',
'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/msword',
]
if (!allowedTypes.includes(file.type)) {
return NextResponse.json(
{ error: 'Ungültiger Dateityp. Erlaubt: PDF, DOCX, DOC' },
{ status: 400 }
)
}
// Generate document ID
const documentId = `doc-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Extract version from filename
const extractedVersion = detectVersionFromFilename(file.name)
// Extract content (in production, this would parse the actual file)
const extractedContent = await extractDocumentContent(file, documentType)
// Add version to extracted content if found
if (extractedVersion) {
extractedContent.version = extractedVersion
}
// Store file (in production, save to MinIO/S3)
// For now, we just process and return metadata
console.log(`[SDK Documents] Uploaded: ${file.name} (${file.size} bytes) for session ${sessionId}`)
const response: UploadResponse = {
success: true,
documentId,
filename: file.name,
documentType,
extractedVersion,
extractedContent,
suggestedNextVersion: suggestNextVersion(extractedVersion),
}
return NextResponse.json(response)
} catch (error) {
console.error('[SDK Documents] Upload error:', error)
return NextResponse.json(
{ error: 'Upload fehlgeschlagen' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
const { searchParams } = new URL(request.url)
const sessionId = searchParams.get('sessionId')
if (!sessionId) {
return NextResponse.json(
{ error: 'Session ID erforderlich' },
{ status: 400 }
)
}
// In production, fetch uploaded documents from storage
// For now, return empty list
return NextResponse.json({
uploads: [],
sessionId,
})
}

View File

@@ -0,0 +1,109 @@
/**
* DSB Portal API Proxy - Catch-all route
* Proxies all /api/sdk/v1/dsb/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/dsb`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
try {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
} catch {
// No body to forward
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('DSB API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,130 @@
/**
* DSGVO API Proxy - Catch-all route
* Proxies all /api/sdk/v1/dsgvo/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[],
method: string
) {
const pathStr = pathSegments.join('/')
const searchParams = request.nextUrl.searchParams.toString()
const url = `${SDK_BACKEND_URL}/sdk/v1/dsgvo/${pathStr}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward auth headers if present
const authHeader = request.headers.get('authorization')
if (authHeader) {
headers['Authorization'] = authHeader
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
// Add body for POST/PUT/PATCH methods
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body - continue without
}
}
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (e.g., PDF export)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('DSGVO API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,255 @@
/**
* API Route: Datenpunktkatalog
*
* GET - Katalog abrufen (inkl. kundenspezifischer Datenpunkte)
* POST - Katalog speichern/aktualisieren
*/
import { NextRequest, NextResponse } from 'next/server'
import {
DataPointCatalog,
CompanyInfo,
CookieBannerConfig,
DataPoint,
} from '@/lib/sdk/einwilligungen/types'
import { createDefaultCatalog, PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage (in Produktion: Datenbank)
const catalogStorage = new Map<string, {
catalog: DataPointCatalog
companyInfo: CompanyInfo | null
cookieBannerConfig: CookieBannerConfig | null
}>()
/**
* GET /api/sdk/v1/einwilligungen/catalog
*
* Laedt den Datenpunktkatalog fuer einen Tenant
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
// Hole gespeicherte Daten oder erstelle Default
let stored = catalogStorage.get(tenantId)
if (!stored) {
// Erstelle Default-Katalog
const defaultCatalog = createDefaultCatalog(tenantId)
stored = {
catalog: defaultCatalog,
companyInfo: null,
cookieBannerConfig: null,
}
catalogStorage.set(tenantId, stored)
}
return NextResponse.json({
catalog: stored.catalog,
companyInfo: stored.companyInfo,
cookieBannerConfig: stored.cookieBannerConfig,
})
} catch (error) {
console.error('Error loading catalog:', error)
return NextResponse.json(
{ error: 'Failed to load catalog' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/catalog
*
* Speichert den Datenpunktkatalog fuer einen Tenant
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { catalog, companyInfo, cookieBannerConfig } = body
if (!catalog) {
return NextResponse.json(
{ error: 'Catalog data required' },
{ status: 400 }
)
}
// Validiere den Katalog
if (!catalog.tenantId || catalog.tenantId !== tenantId) {
return NextResponse.json(
{ error: 'Tenant ID mismatch' },
{ status: 400 }
)
}
// Aktualisiere den Katalog
const updatedCatalog: DataPointCatalog = {
...catalog,
updatedAt: new Date(),
}
// Speichere
catalogStorage.set(tenantId, {
catalog: updatedCatalog,
companyInfo: companyInfo || null,
cookieBannerConfig: cookieBannerConfig || null,
})
return NextResponse.json({
success: true,
catalog: updatedCatalog,
})
} catch (error) {
console.error('Error saving catalog:', error)
return NextResponse.json(
{ error: 'Failed to save catalog' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/catalog/customize
*
* Fuegt einen kundenspezifischen Datenpunkt hinzu
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { action, dataPoint, dataPointId } = body
let stored = catalogStorage.get(tenantId)
if (!stored) {
const defaultCatalog = createDefaultCatalog(tenantId)
stored = {
catalog: defaultCatalog,
companyInfo: null,
cookieBannerConfig: null,
}
}
switch (action) {
case 'add': {
if (!dataPoint) {
return NextResponse.json(
{ error: 'Data point required' },
{ status: 400 }
)
}
// Generiere eindeutige ID
const newDataPoint: DataPoint = {
...dataPoint,
id: `custom-${tenantId}-${Date.now()}`,
isCustom: true,
}
stored.catalog.customDataPoints.push(newDataPoint)
stored.catalog.updatedAt = new Date()
catalogStorage.set(tenantId, stored)
return NextResponse.json({
success: true,
dataPoint: newDataPoint,
})
}
case 'update': {
if (!dataPointId || !dataPoint) {
return NextResponse.json(
{ error: 'Data point ID and data required' },
{ status: 400 }
)
}
// Pruefe ob es ein kundenspezifischer Datenpunkt ist
const customIndex = stored.catalog.customDataPoints.findIndex(
(dp) => dp.id === dataPointId
)
if (customIndex !== -1) {
stored.catalog.customDataPoints[customIndex] = {
...stored.catalog.customDataPoints[customIndex],
...dataPoint,
}
} else {
// Vordefinierter Datenpunkt - nur isActive aendern
const predefinedIndex = stored.catalog.dataPoints.findIndex(
(dp) => dp.id === dataPointId
)
if (predefinedIndex !== -1 && 'isActive' in dataPoint) {
stored.catalog.dataPoints[predefinedIndex] = {
...stored.catalog.dataPoints[predefinedIndex],
isActive: dataPoint.isActive,
}
}
}
stored.catalog.updatedAt = new Date()
catalogStorage.set(tenantId, stored)
return NextResponse.json({
success: true,
})
}
case 'delete': {
if (!dataPointId) {
return NextResponse.json(
{ error: 'Data point ID required' },
{ status: 400 }
)
}
stored.catalog.customDataPoints = stored.catalog.customDataPoints.filter(
(dp) => dp.id !== dataPointId
)
stored.catalog.updatedAt = new Date()
catalogStorage.set(tenantId, stored)
return NextResponse.json({
success: true,
})
}
default:
return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
}
} catch (error) {
console.error('Error customizing catalog:', error)
return NextResponse.json(
{ error: 'Failed to customize catalog' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,369 @@
/**
* API Route: Consent Management
*
* POST - Consent erfassen
* GET - Consent-Status abrufen
*/
import { NextRequest, NextResponse } from 'next/server'
import {
ConsentEntry,
ConsentStatistics,
DataPointCategory,
LegalBasis,
} from '@/lib/sdk/einwilligungen/types'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer Consents
const consentStorage = new Map<string, ConsentEntry[]>() // tenantId -> consents
// Hilfsfunktion: Generiere eindeutige ID
function generateId(): string {
return `consent-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
}
/**
* POST /api/sdk/v1/einwilligungen/consent
*
* Erfasst eine neue Einwilligung
*
* Body:
* - userId: string - Benutzer-ID
* - dataPointId: string - ID des Datenpunkts
* - granted: boolean - Einwilligung erteilt?
* - consentVersion?: string - Version der Einwilligung
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { userId, dataPointId, granted, consentVersion = '1.0.0' } = body
if (!userId || !dataPointId || typeof granted !== 'boolean') {
return NextResponse.json(
{ error: 'userId, dataPointId, and granted required' },
{ status: 400 }
)
}
// Hole IP und User-Agent
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
const userAgent = request.headers.get('user-agent') || null
// Erstelle Consent-Eintrag
const consent: ConsentEntry = {
id: generateId(),
userId,
dataPointId,
granted,
grantedAt: new Date(),
revokedAt: undefined,
ipAddress: ipAddress || undefined,
userAgent: userAgent || undefined,
consentVersion,
}
// Hole bestehende Consents
const tenantConsents = consentStorage.get(tenantId) || []
// Pruefe auf bestehende Einwilligung fuer diesen Datenpunkt
const existingIndex = tenantConsents.findIndex(
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
)
if (existingIndex !== -1) {
if (!granted) {
// Widerruf: Setze revokedAt
tenantConsents[existingIndex].revokedAt = new Date()
}
// Bei granted=true: Keine Aenderung noetig, Consent existiert bereits
} else if (granted) {
// Neuer Consent
tenantConsents.push(consent)
}
consentStorage.set(tenantId, tenantConsents)
return NextResponse.json({
success: true,
consent: {
id: consent.id,
dataPointId: consent.dataPointId,
granted: consent.granted,
grantedAt: consent.grantedAt,
},
})
} catch (error) {
console.error('Error recording consent:', error)
return NextResponse.json(
{ error: 'Failed to record consent' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/einwilligungen/consent
*
* Ruft Consent-Status und Statistiken ab
*
* Query Parameters:
* - userId?: string - Fuer spezifischen Benutzer
* - stats?: boolean - Statistiken inkludieren
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const { searchParams } = new URL(request.url)
const userId = searchParams.get('userId')
const includeStats = searchParams.get('stats') === 'true'
const tenantConsents = consentStorage.get(tenantId) || []
if (userId) {
// Spezifischer Benutzer
const userConsents = tenantConsents.filter((c) => c.userId === userId)
// Gruppiere nach Datenpunkt
const consentMap: Record<string, { granted: boolean; grantedAt: Date; revokedAt?: Date }> = {}
for (const consent of userConsents) {
consentMap[consent.dataPointId] = {
granted: consent.granted && !consent.revokedAt,
grantedAt: consent.grantedAt,
revokedAt: consent.revokedAt,
}
}
return NextResponse.json({
userId,
consents: consentMap,
totalConsents: Object.keys(consentMap).length,
activeConsents: Object.values(consentMap).filter((c) => c.granted).length,
})
}
// Statistiken fuer alle Consents
if (includeStats) {
const stats = calculateStatistics(tenantConsents)
return NextResponse.json({
statistics: stats,
recentConsents: tenantConsents
.sort((a, b) => new Date(b.grantedAt).getTime() - new Date(a.grantedAt).getTime())
.slice(0, 10)
.map((c) => ({
id: c.id,
userId: c.userId.substring(0, 8) + '...', // Anonymisiert
dataPointId: c.dataPointId,
granted: c.granted,
grantedAt: c.grantedAt,
})),
})
}
// Standard: Alle Consents (anonymisiert)
return NextResponse.json({
totalConsents: tenantConsents.length,
activeConsents: tenantConsents.filter((c) => c.granted && !c.revokedAt).length,
revokedConsents: tenantConsents.filter((c) => c.revokedAt).length,
})
} catch (error) {
console.error('Error fetching consents:', error)
return NextResponse.json(
{ error: 'Failed to fetch consents' },
{ status: 500 }
)
}
}
/**
* PUT /api/sdk/v1/einwilligungen/consent
*
* Batch-Update von Consents (z.B. Cookie-Banner)
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { userId, consents, consentVersion = '1.0.0' } = body
if (!userId || !consents || typeof consents !== 'object') {
return NextResponse.json(
{ error: 'userId and consents object required' },
{ status: 400 }
)
}
const ipAddress = request.headers.get('x-forwarded-for') || request.headers.get('x-real-ip') || null
const userAgent = request.headers.get('user-agent') || null
const tenantConsents = consentStorage.get(tenantId) || []
const now = new Date()
// Verarbeite jeden Consent
for (const [dataPointId, granted] of Object.entries(consents)) {
if (typeof granted !== 'boolean') continue
const existingIndex = tenantConsents.findIndex(
(c) => c.userId === userId && c.dataPointId === dataPointId && !c.revokedAt
)
if (existingIndex !== -1) {
const existing = tenantConsents[existingIndex]
if (existing.granted !== granted) {
if (!granted) {
// Widerruf
tenantConsents[existingIndex].revokedAt = now
} else {
// Neuer Consent nach Widerruf
tenantConsents.push({
id: generateId(),
userId,
dataPointId,
granted: true,
grantedAt: now,
ipAddress: ipAddress || undefined,
userAgent: userAgent || undefined,
consentVersion,
})
}
}
} else if (granted) {
// Neuer Consent
tenantConsents.push({
id: generateId(),
userId,
dataPointId,
granted: true,
grantedAt: now,
ipAddress: ipAddress || undefined,
userAgent: userAgent || undefined,
consentVersion,
})
}
}
consentStorage.set(tenantId, tenantConsents)
// Zaehle aktive Consents fuer diesen User
const activeConsents = tenantConsents.filter(
(c) => c.userId === userId && c.granted && !c.revokedAt
).length
return NextResponse.json({
success: true,
userId,
activeConsents,
updatedAt: now,
})
} catch (error) {
console.error('Error updating consents:', error)
return NextResponse.json(
{ error: 'Failed to update consents' },
{ status: 500 }
)
}
}
/**
* Berechnet Consent-Statistiken
*/
function calculateStatistics(consents: ConsentEntry[]): ConsentStatistics {
const activeConsents = consents.filter((c) => c.granted && !c.revokedAt)
const revokedConsents = consents.filter((c) => c.revokedAt)
// Gruppiere nach Kategorie (18 Kategorien A-R)
const byCategory: Record<DataPointCategory, { total: number; active: number; revoked: number }> = {
MASTER_DATA: { total: 0, active: 0, revoked: 0 },
CONTACT_DATA: { total: 0, active: 0, revoked: 0 },
AUTHENTICATION: { total: 0, active: 0, revoked: 0 },
CONSENT: { total: 0, active: 0, revoked: 0 },
COMMUNICATION: { total: 0, active: 0, revoked: 0 },
PAYMENT: { total: 0, active: 0, revoked: 0 },
USAGE_DATA: { total: 0, active: 0, revoked: 0 },
LOCATION: { total: 0, active: 0, revoked: 0 },
DEVICE_DATA: { total: 0, active: 0, revoked: 0 },
MARKETING: { total: 0, active: 0, revoked: 0 },
ANALYTICS: { total: 0, active: 0, revoked: 0 },
SOCIAL_MEDIA: { total: 0, active: 0, revoked: 0 },
HEALTH_DATA: { total: 0, active: 0, revoked: 0 },
EMPLOYEE_DATA: { total: 0, active: 0, revoked: 0 },
CONTRACT_DATA: { total: 0, active: 0, revoked: 0 },
LOG_DATA: { total: 0, active: 0, revoked: 0 },
AI_DATA: { total: 0, active: 0, revoked: 0 },
SECURITY: { total: 0, active: 0, revoked: 0 },
}
for (const consent of consents) {
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
if (dataPoint) {
byCategory[dataPoint.category].total++
if (consent.granted && !consent.revokedAt) {
byCategory[dataPoint.category].active++
}
if (consent.revokedAt) {
byCategory[dataPoint.category].revoked++
}
}
}
// Gruppiere nach Rechtsgrundlage (7 Rechtsgrundlagen)
const byLegalBasis: Record<LegalBasis, { total: number; active: number }> = {
CONTRACT: { total: 0, active: 0 },
CONSENT: { total: 0, active: 0 },
EXPLICIT_CONSENT: { total: 0, active: 0 },
LEGITIMATE_INTEREST: { total: 0, active: 0 },
LEGAL_OBLIGATION: { total: 0, active: 0 },
VITAL_INTERESTS: { total: 0, active: 0 },
PUBLIC_INTEREST: { total: 0, active: 0 },
}
for (const consent of consents) {
const dataPoint = PREDEFINED_DATA_POINTS.find((dp) => dp.id === consent.dataPointId)
if (dataPoint) {
byLegalBasis[dataPoint.legalBasis].total++
if (consent.granted && !consent.revokedAt) {
byLegalBasis[dataPoint.legalBasis].active++
}
}
}
// Berechne Conversion Rate (Unique Users mit mindestens einem Consent)
const uniqueUsers = new Set(consents.map((c) => c.userId))
const usersWithActiveConsent = new Set(activeConsents.map((c) => c.userId))
const conversionRate = uniqueUsers.size > 0
? (usersWithActiveConsent.size / uniqueUsers.size) * 100
: 0
return {
totalConsents: consents.length,
activeConsents: activeConsents.length,
revokedConsents: revokedConsents.length,
byCategory,
byLegalBasis,
conversionRate: Math.round(conversionRate * 10) / 10,
}
}

View File

@@ -0,0 +1,215 @@
/**
* API Route: Cookie Banner Configuration
*
* GET - Cookie Banner Konfiguration abrufen
* POST - Cookie Banner Konfiguration speichern
*/
import { NextRequest, NextResponse } from 'next/server'
import {
CookieBannerConfig,
CookieBannerStyling,
CookieBannerTexts,
DataPoint,
} from '@/lib/sdk/einwilligungen/types'
import {
generateCookieBannerConfig,
DEFAULT_COOKIE_BANNER_STYLING,
DEFAULT_COOKIE_BANNER_TEXTS,
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer Cookie Banner Configs
const configStorage = new Map<string, CookieBannerConfig>()
/**
* GET /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Laedt die Cookie Banner Konfiguration fuer einen Tenant
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
let config = configStorage.get(tenantId)
if (!config) {
// Generiere Default-Konfiguration
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
configStorage.set(tenantId, config)
}
return NextResponse.json(config)
} catch (error) {
console.error('Error loading cookie banner config:', error)
return NextResponse.json(
{ error: 'Failed to load cookie banner config' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Speichert oder aktualisiert die Cookie Banner Konfiguration
*
* Body:
* - dataPointIds?: string[] - IDs der Datenpunkte (fuer Neuberechnung)
* - styling?: Partial<CookieBannerStyling> - Styling-Optionen
* - texts?: Partial<CookieBannerTexts> - Text-Optionen
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const {
dataPointIds,
styling,
texts,
customDataPoints = [],
} = body
// Hole bestehende Konfiguration oder erstelle neue
let config = configStorage.get(tenantId)
if (dataPointIds && Array.isArray(dataPointIds)) {
// Neu berechnen basierend auf Datenpunkten
const allDataPoints: DataPoint[] = [
...PREDEFINED_DATA_POINTS,
...customDataPoints,
]
const selectedDataPoints = dataPointIds
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
.filter((dp): dp is DataPoint => dp !== undefined)
config = generateCookieBannerConfig(
tenantId,
selectedDataPoints,
texts,
styling
)
} else if (config) {
// Nur Styling/Texts aktualisieren
if (styling) {
config.styling = {
...config.styling,
...styling,
}
}
if (texts) {
config.texts = {
...config.texts,
...texts,
}
}
config.updatedAt = new Date()
} else {
// Erstelle Default
config = generateCookieBannerConfig(
tenantId,
PREDEFINED_DATA_POINTS,
texts,
styling
)
}
configStorage.set(tenantId, config)
return NextResponse.json({
success: true,
config,
})
} catch (error) {
console.error('Error saving cookie banner config:', error)
return NextResponse.json(
{ error: 'Failed to save cookie banner config' },
{ status: 500 }
)
}
}
/**
* PUT /api/sdk/v1/einwilligungen/cookie-banner/config
*
* Aktualisiert einzelne Kategorien
*/
export async function PUT(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const { categoryId, enabled } = body
if (!categoryId || typeof enabled !== 'boolean') {
return NextResponse.json(
{ error: 'categoryId and enabled required' },
{ status: 400 }
)
}
let config = configStorage.get(tenantId)
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
}
// Finde und aktualisiere die Kategorie
const categoryIndex = config.categories.findIndex((c) => c.id === categoryId)
if (categoryIndex === -1) {
return NextResponse.json(
{ error: 'Category not found' },
{ status: 404 }
)
}
// Essenzielle Cookies koennen nicht deaktiviert werden
if (config.categories[categoryIndex].isRequired && !enabled) {
return NextResponse.json(
{ error: 'Essential cookies cannot be disabled' },
{ status: 400 }
)
}
config.categories[categoryIndex].defaultEnabled = enabled
config.updatedAt = new Date()
configStorage.set(tenantId, config)
return NextResponse.json({
success: true,
category: config.categories[categoryIndex],
})
} catch (error) {
console.error('Error updating cookie category:', error)
return NextResponse.json(
{ error: 'Failed to update cookie category' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,256 @@
/**
* API Route: Cookie Banner Embed Code
*
* GET - Generiert den Embed-Code fuer den Cookie Banner
*/
import { NextRequest, NextResponse } from 'next/server'
import { CookieBannerConfig, CookieBannerEmbedCode } from '@/lib/sdk/einwilligungen/types'
import {
generateCookieBannerConfig,
generateEmbedCode,
} from '@/lib/sdk/einwilligungen/generator/cookie-banner'
import { PREDEFINED_DATA_POINTS } from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage (in Produktion mit configStorage aus config/route.ts teilen)
const configStorage = new Map<string, CookieBannerConfig>()
/**
* GET /api/sdk/v1/einwilligungen/cookie-banner/embed-code
*
* Generiert den Embed-Code fuer den Cookie Banner
*
* Query Parameters:
* - privacyPolicyUrl: string - URL zur Datenschutzerklaerung (default: /datenschutz)
* - format: 'combined' | 'separate' - Ausgabeformat (default: combined)
*/
export async function GET(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const { searchParams } = new URL(request.url)
const privacyPolicyUrl = searchParams.get('privacyPolicyUrl') || '/datenschutz'
const format = searchParams.get('format') || 'combined'
// Hole oder erstelle Konfiguration
let config = configStorage.get(tenantId)
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS)
configStorage.set(tenantId, config)
}
// Generiere Embed-Code
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
if (format === 'separate') {
// Separate Dateien zurueckgeben
return NextResponse.json({
html: embedCode.html,
css: embedCode.css,
js: embedCode.js,
scriptTag: embedCode.scriptTag,
instructions: {
de: `
Fuegen Sie den folgenden Code in Ihre Website ein:
1. CSS in den <head>-Bereich:
<style>${embedCode.css}</style>
2. HTML vor dem schliessenden </body>-Tag:
${embedCode.html}
3. JavaScript vor dem schliessenden </body>-Tag:
<script>${embedCode.js}</script>
Alternativ koennen Sie die Dateien separat einbinden:
- /cookie-banner.css
- /cookie-banner.js
`,
en: `
Add the following code to your website:
1. CSS in the <head> section:
<style>${embedCode.css}</style>
2. HTML before the closing </body> tag:
${embedCode.html}
3. JavaScript before the closing </body> tag:
<script>${embedCode.js}</script>
Alternatively, you can include the files separately:
- /cookie-banner.css
- /cookie-banner.js
`,
},
})
}
// Combined: Alles in einem HTML-Block
const combinedCode = `
<!-- Cookie Banner - Start -->
<style>
${embedCode.css}
</style>
${embedCode.html}
<script>
${embedCode.js}
</script>
<!-- Cookie Banner - End -->
`.trim()
return NextResponse.json({
embedCode: combinedCode,
scriptTag: embedCode.scriptTag,
config: {
tenantId: config.tenantId,
categories: config.categories.map((c) => ({
id: c.id,
name: c.name,
isRequired: c.isRequired,
defaultEnabled: c.defaultEnabled,
})),
styling: config.styling,
},
instructions: {
de: `Fuegen Sie den folgenden Code vor dem schliessenden </body>-Tag Ihrer Website ein.`,
en: `Add the following code before the closing </body> tag of your website.`,
},
})
} catch (error) {
console.error('Error generating embed code:', error)
return NextResponse.json(
{ error: 'Failed to generate embed code' },
{ status: 500 }
)
}
}
/**
* POST /api/sdk/v1/einwilligungen/cookie-banner/embed-code
*
* Generiert Embed-Code mit benutzerdefinierten Optionen
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const {
privacyPolicyUrl = '/datenschutz',
styling,
texts,
language = 'de',
} = body
// Hole oder erstelle Konfiguration
let config = configStorage.get(tenantId)
if (!config) {
config = generateCookieBannerConfig(tenantId, PREDEFINED_DATA_POINTS, texts, styling)
} else {
// Wende temporaere Anpassungen an
if (styling) {
config = {
...config,
styling: { ...config.styling, ...styling },
}
}
if (texts) {
config = {
...config,
texts: { ...config.texts, ...texts },
}
}
}
const embedCode = generateEmbedCode(config, privacyPolicyUrl)
// Generiere Preview HTML
const previewHtml = `
<!DOCTYPE html>
<html lang="${language}">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Cookie Banner Preview</title>
<style>
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: #f1f5f9;
min-height: 100vh;
margin: 0;
padding: 20px;
}
.preview-content {
max-width: 800px;
margin: 0 auto;
padding: 40px;
background: white;
border-radius: 12px;
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
}
h1 { color: #1e293b; }
p { color: #64748b; line-height: 1.6; }
${embedCode.css}
</style>
</head>
<body>
<div class="preview-content">
<h1>Cookie Banner Preview</h1>
<p>Dies ist eine Vorschau des Cookie Banners. In der produktiven Umgebung wird der Banner auf Ihrer Website angezeigt.</p>
</div>
${embedCode.html}
<script>
${embedCode.js}
// Force show banner for preview
setTimeout(() => {
document.getElementById('cookieBanner')?.classList.add('active');
document.getElementById('cookieBannerOverlay')?.classList.add('active');
}, 100);
</script>
</body>
</html>
`.trim()
return NextResponse.json({
embedCode: {
html: embedCode.html,
css: embedCode.css,
js: embedCode.js,
scriptTag: embedCode.scriptTag,
},
previewHtml,
config: {
tenantId: config.tenantId,
categories: config.categories.length,
styling: config.styling,
},
})
} catch (error) {
console.error('Error generating custom embed code:', error)
return NextResponse.json(
{ error: 'Failed to generate embed code' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,186 @@
/**
* API Route: Privacy Policy Generator
*
* POST - Generiert eine Datenschutzerklaerung aus dem Datenpunktkatalog
*/
import { NextRequest, NextResponse } from 'next/server'
import {
CompanyInfo,
DataPoint,
SupportedLanguage,
ExportFormat,
GeneratedPrivacyPolicy,
} from '@/lib/sdk/einwilligungen/types'
import {
generatePrivacyPolicy,
generatePrivacyPolicySections,
} from '@/lib/sdk/einwilligungen/generator/privacy-policy'
import {
PREDEFINED_DATA_POINTS,
getDataPointById,
} from '@/lib/sdk/einwilligungen/catalog/loader'
// In-Memory Storage fuer generierte Policies
const policyStorage = new Map<string, GeneratedPrivacyPolicy>()
/**
* POST /api/sdk/v1/einwilligungen/privacy-policy/generate
*
* Generiert eine Datenschutzerklaerung
*
* Body:
* - dataPointIds: string[] - IDs der zu inkludierenden Datenpunkte
* - companyInfo: CompanyInfo - Firmeninformationen
* - language: 'de' | 'en' - Sprache
* - format: 'HTML' | 'MARKDOWN' | 'PDF' | 'DOCX' - Ausgabeformat
* - customDataPoints?: DataPoint[] - Kundenspezifische Datenpunkte
*/
export async function POST(request: NextRequest) {
try {
const tenantId = request.headers.get('X-Tenant-ID')
if (!tenantId) {
return NextResponse.json(
{ error: 'Tenant ID required' },
{ status: 400 }
)
}
const body = await request.json()
const {
dataPointIds,
companyInfo,
language = 'de',
format = 'HTML',
customDataPoints = [],
} = body
// Validierung
if (!companyInfo || !companyInfo.name || !companyInfo.address || !companyInfo.email) {
return NextResponse.json(
{ error: 'Company info (name, address, email) required' },
{ status: 400 }
)
}
if (!dataPointIds || !Array.isArray(dataPointIds) || dataPointIds.length === 0) {
return NextResponse.json(
{ error: 'At least one data point ID required' },
{ status: 400 }
)
}
// Validiere Sprache
const validLanguages: SupportedLanguage[] = ['de', 'en']
if (!validLanguages.includes(language)) {
return NextResponse.json(
{ error: 'Invalid language. Must be "de" or "en"' },
{ status: 400 }
)
}
// Validiere Format
const validFormats: ExportFormat[] = ['HTML', 'MARKDOWN', 'PDF', 'DOCX']
if (!validFormats.includes(format)) {
return NextResponse.json(
{ error: 'Invalid format. Must be HTML, MARKDOWN, PDF, or DOCX' },
{ status: 400 }
)
}
// Sammle alle Datenpunkte
const allDataPoints: DataPoint[] = [
...PREDEFINED_DATA_POINTS,
...customDataPoints,
]
// Filtere nach ausgewaehlten IDs
const selectedDataPoints = dataPointIds
.map((id: string) => allDataPoints.find((dp) => dp.id === id))
.filter((dp): dp is DataPoint => dp !== undefined)
if (selectedDataPoints.length === 0) {
return NextResponse.json(
{ error: 'No valid data points found for the provided IDs' },
{ status: 400 }
)
}
// Generiere die Privacy Policy
const policy = generatePrivacyPolicy(
tenantId,
selectedDataPoints,
companyInfo as CompanyInfo,
language as SupportedLanguage,
format as ExportFormat
)
// Speichere fuer spaeteres Abrufen
policyStorage.set(policy.id, policy)
// Fuer PDF/DOCX: Nur Metadaten zurueckgeben, Download separat
if (format === 'PDF' || format === 'DOCX') {
return NextResponse.json({
id: policy.id,
tenantId: policy.tenantId,
language: policy.language,
format: policy.format,
generatedAt: policy.generatedAt,
version: policy.version,
sections: policy.sections.map((s) => ({
id: s.id,
title: s.title,
order: s.order,
})),
downloadUrl: `/api/sdk/v1/einwilligungen/privacy-policy/${policy.id}/download`,
})
}
// Fuer HTML/Markdown: Vollstaendige Policy zurueckgeben
return NextResponse.json(policy)
} catch (error) {
console.error('Error generating privacy policy:', error)
return NextResponse.json(
{ error: 'Failed to generate privacy policy' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/einwilligungen/privacy-policy/generate
*
* Liefert eine Vorschau der Abschnitte ohne vollstaendige Generierung
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const language = (searchParams.get('language') as SupportedLanguage) || 'de'
// Liefere die Standard-Abschnittsstruktur
const sections = [
{ id: 'controller', order: 1, title: { de: '1. Verantwortlicher', en: '1. Data Controller' } },
{ id: 'data-collection', order: 2, title: { de: '2. Erhobene personenbezogene Daten', en: '2. Personal Data We Collect' } },
{ id: 'purposes', order: 3, title: { de: '3. Zwecke der Datenverarbeitung', en: '3. Purposes of Data Processing' } },
{ id: 'legal-basis', order: 4, title: { de: '4. Rechtsgrundlagen der Verarbeitung', en: '4. Legal Basis for Processing' } },
{ id: 'recipients', order: 5, title: { de: '5. Empfaenger und Datenweitergabe', en: '5. Recipients and Data Sharing' } },
{ id: 'retention', order: 6, title: { de: '6. Speicherdauer', en: '6. Data Retention' } },
{ id: 'rights', order: 7, title: { de: '7. Ihre Rechte als betroffene Person', en: '7. Your Rights as a Data Subject' } },
{ id: 'cookies', order: 8, title: { de: '8. Cookies und aehnliche Technologien', en: '8. Cookies and Similar Technologies' } },
{ id: 'changes', order: 9, title: { de: '9. Aenderungen dieser Datenschutzerklaerung', en: '9. Changes to this Privacy Policy' } },
]
return NextResponse.json({
sections,
availableLanguages: ['de', 'en'],
availableFormats: ['HTML', 'MARKDOWN', 'PDF', 'DOCX'],
})
} catch (error) {
console.error('Error fetching sections:', error)
return NextResponse.json(
{ error: 'Failed to fetch sections' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Export API
*
* GET /api/sdk/v1/export?format=json|pdf|zip - Export SDK data
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const format = searchParams.get('format') || 'json'
const tenantId = searchParams.get('tenantId') || 'default'
switch (format) {
case 'json':
return exportJSON(tenantId)
case 'pdf':
return exportPDF(tenantId)
case 'zip':
return exportZIP(tenantId)
default:
return NextResponse.json(
{ error: `Unknown export format: ${format}` },
{ status: 400 }
)
}
} catch (error) {
console.error('Failed to export:', error)
return NextResponse.json(
{ error: 'Failed to export' },
{ status: 500 }
)
}
}
function exportJSON(tenantId: string) {
// In production, this would fetch the actual state from the database
const exportData = {
version: '1.0.0',
exportedAt: new Date().toISOString(),
tenantId,
data: {
useCases: [],
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
risks: [],
dsfa: null,
toms: [],
vvt: [],
documents: [],
},
}
return new NextResponse(JSON.stringify(exportData, null, 2), {
status: 200,
headers: {
'Content-Type': 'application/json',
'Content-Disposition': `attachment; filename="compliance-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json"`,
},
})
}
function exportPDF(tenantId: string) {
// In production, this would generate a proper PDF using a library like pdfkit or puppeteer
// For now, return a placeholder response
return NextResponse.json({
success: false,
error: 'PDF export not yet implemented',
message: 'PDF generation requires server-side rendering. Use JSON export for now.',
}, { status: 501 })
}
function exportZIP(tenantId: string) {
// In production, this would create a ZIP file with multiple documents
// For now, return a placeholder response
return NextResponse.json({
success: false,
error: 'ZIP export not yet implemented',
message: 'ZIP generation requires additional server-side processing. Use JSON export for now.',
}, { status: 501 })
}

View File

@@ -0,0 +1,150 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Flow API
*
* GET /api/sdk/v1/flow - Get current flow state and suggestions
* POST /api/sdk/v1/flow/next - Navigate to next step
* POST /api/sdk/v1/flow/previous - Navigate to previous step
*/
const SDK_STEPS = [
// Phase 1
{ id: 'company-profile', phase: 1, order: 1, name: 'Unternehmensprofil', url: '/sdk/company-profile' },
{ id: 'use-case-assessment', phase: 1, order: 2, name: 'Anwendungsfall-Erfassung', url: '/sdk/advisory-board' },
{ id: 'screening', phase: 1, order: 3, name: 'System Screening', url: '/sdk/screening' },
{ id: 'modules', phase: 1, order: 4, name: 'Compliance Modules', url: '/sdk/modules' },
{ id: 'requirements', phase: 1, order: 5, name: 'Requirements', url: '/sdk/requirements' },
{ id: 'controls', phase: 1, order: 6, name: 'Controls', url: '/sdk/controls' },
{ id: 'evidence', phase: 1, order: 7, name: 'Evidence', url: '/sdk/evidence' },
{ id: 'audit-checklist', phase: 1, order: 8, name: 'Audit Checklist', url: '/sdk/audit-checklist' },
{ id: 'risks', phase: 1, order: 9, name: 'Risk Matrix', url: '/sdk/risks' },
// Phase 2
{ id: 'ai-act', phase: 2, order: 1, name: 'AI Act Klassifizierung', url: '/sdk/ai-act' },
{ id: 'obligations', phase: 2, order: 2, name: 'Pflichtenübersicht', url: '/sdk/obligations' },
{ id: 'dsfa', phase: 2, order: 3, name: 'DSFA', url: '/sdk/dsfa' },
{ id: 'tom', phase: 2, order: 4, name: 'TOMs', url: '/sdk/tom' },
{ id: 'loeschfristen', phase: 2, order: 5, name: 'Löschfristen', url: '/sdk/loeschfristen' },
{ id: 'vvt', phase: 2, order: 6, name: 'Verarbeitungsverzeichnis', url: '/sdk/vvt' },
{ id: 'consent', phase: 2, order: 7, name: 'Rechtliche Vorlagen', url: '/sdk/consent' },
{ id: 'cookie-banner', phase: 2, order: 8, name: 'Cookie Banner', url: '/sdk/cookie-banner' },
{ id: 'einwilligungen', phase: 2, order: 9, name: 'Einwilligungen', url: '/sdk/einwilligungen' },
{ id: 'dsr', phase: 2, order: 10, name: 'DSR Portal', url: '/sdk/dsr' },
{ id: 'escalations', phase: 2, order: 11, name: 'Escalations', url: '/sdk/escalations' },
]
function getStepIndex(stepId: string): number {
return SDK_STEPS.findIndex(s => s.id === stepId)
}
function getNextStep(currentStepId: string) {
const currentIndex = getStepIndex(currentStepId)
if (currentIndex === -1 || currentIndex >= SDK_STEPS.length - 1) {
return null
}
return SDK_STEPS[currentIndex + 1]
}
function getPreviousStep(currentStepId: string) {
const currentIndex = getStepIndex(currentStepId)
if (currentIndex <= 0) {
return null
}
return SDK_STEPS[currentIndex - 1]
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const currentStepId = searchParams.get('currentStep') || 'company-profile'
const currentStep = SDK_STEPS.find(s => s.id === currentStepId)
const nextStep = getNextStep(currentStepId)
const previousStep = getPreviousStep(currentStepId)
// Generate suggestions based on context
const suggestions = [
{
type: 'NAVIGATION',
label: nextStep ? `Weiter zu ${nextStep.name}` : 'Flow abgeschlossen',
action: nextStep ? `navigate:${nextStep.url}` : null,
},
{
type: 'ACTION',
label: 'Checkpoint validieren',
action: 'validate:current',
},
{
type: 'HELP',
label: 'Hilfe anzeigen',
action: 'help:show',
},
]
return NextResponse.json({
success: true,
currentStep,
nextStep,
previousStep,
totalSteps: SDK_STEPS.length,
currentIndex: getStepIndex(currentStepId) + 1,
suggestions,
steps: SDK_STEPS,
})
} catch (error) {
console.error('Failed to get flow:', error)
return NextResponse.json(
{ error: 'Failed to get flow' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { action, currentStepId } = body
if (!action || !currentStepId) {
return NextResponse.json(
{ error: 'action and currentStepId are required' },
{ status: 400 }
)
}
let targetStep = null
switch (action) {
case 'next':
targetStep = getNextStep(currentStepId)
break
case 'previous':
targetStep = getPreviousStep(currentStepId)
break
default:
return NextResponse.json(
{ error: 'Invalid action' },
{ status: 400 }
)
}
if (!targetStep) {
return NextResponse.json(
{ error: 'No target step available' },
{ status: 400 }
)
}
return NextResponse.json({
success: true,
targetStep,
redirectUrl: targetStep.url,
})
} catch (error) {
console.error('Failed to navigate flow:', error)
return NextResponse.json(
{ error: 'Failed to navigate flow' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,309 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK Document Generation API
*
* POST /api/sdk/v1/generate - Generate compliance documents
*
* Supported document types:
* - dsfa: Data Protection Impact Assessment
* - tom: Technical and Organizational Measures
* - vvt: Processing Register (Art. 30 GDPR)
* - cookie-banner: Cookie consent banner code
* - audit-report: Audit report
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { documentType, context, options } = body
if (!documentType) {
return NextResponse.json(
{ error: 'documentType is required' },
{ status: 400 }
)
}
// Generate document based on type
let document: unknown = null
let generationTime = Date.now()
switch (documentType) {
case 'dsfa':
document = generateDSFA(context, options)
break
case 'tom':
document = generateTOMs(context, options)
break
case 'vvt':
document = generateVVT(context, options)
break
case 'cookie-banner':
document = generateCookieBanner(context, options)
break
case 'audit-report':
document = generateAuditReport(context, options)
break
default:
return NextResponse.json(
{ error: `Unknown document type: ${documentType}` },
{ status: 400 }
)
}
generationTime = Date.now() - generationTime
return NextResponse.json({
success: true,
documentType,
document,
generatedAt: new Date().toISOString(),
generationTimeMs: generationTime,
})
} catch (error) {
console.error('Failed to generate document:', error)
return NextResponse.json(
{ error: 'Failed to generate document' },
{ status: 500 }
)
}
}
// =============================================================================
// DOCUMENT GENERATORS
// =============================================================================
function generateDSFA(context: unknown, options: unknown) {
return {
id: `dsfa-${Date.now()}`,
status: 'DRAFT',
version: 1,
sections: [
{
id: 'section-1',
title: '1. Systematische Beschreibung der Verarbeitungsvorgänge',
content: 'Die geplante Verarbeitung umfasst...',
status: 'DRAFT',
order: 1,
},
{
id: 'section-2',
title: '2. Bewertung der Notwendigkeit und Verhältnismäßigkeit',
content: 'Die Verarbeitung ist notwendig für...',
status: 'DRAFT',
order: 2,
},
{
id: 'section-3',
title: '3. Bewertung der Risiken für die Rechte und Freiheiten',
content: 'Identifizierte Risiken:\n- Risiko 1\n- Risiko 2',
status: 'DRAFT',
order: 3,
},
{
id: 'section-4',
title: '4. Abhilfemaßnahmen',
content: 'Folgende Maßnahmen werden ergriffen...',
status: 'DRAFT',
order: 4,
},
],
approvals: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
}
function generateTOMs(context: unknown, options: unknown) {
return {
toms: [
{
id: 'tom-1',
category: 'Zutrittskontrolle',
name: 'Physische Zugangskontrollen',
description: 'Maßnahmen zur Verhinderung unbefugten Zutritts zu Datenverarbeitungsanlagen',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-2',
category: 'Zugangskontrolle',
name: 'Authentifizierung',
description: 'Multi-Faktor-Authentifizierung für alle Systeme',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-3',
category: 'Zugriffskontrolle',
name: 'Rollenbasierte Zugriffskontrolle',
description: 'RBAC-System für granulare Berechtigungsvergabe',
type: 'ORGANIZATIONAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-4',
category: 'Weitergabekontrolle',
name: 'Verschlüsselung',
description: 'Ende-zu-Ende-Verschlüsselung für Datenübertragung',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'HIGH',
},
{
id: 'tom-5',
category: 'Eingabekontrolle',
name: 'Audit Logging',
description: 'Protokollierung aller Dateneingaben und -änderungen',
type: 'TECHNICAL',
implementationStatus: 'NOT_IMPLEMENTED',
priority: 'MEDIUM',
},
],
generatedAt: new Date().toISOString(),
}
}
function generateVVT(context: unknown, options: unknown) {
return {
processingActivities: [
{
id: 'pa-1',
name: 'Kundenmanagement',
purpose: 'Verwaltung von Kundenbeziehungen und Aufträgen',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertrag)',
dataCategories: ['Name', 'Kontaktdaten', 'Bestellhistorie'],
dataSubjects: ['Kunden'],
recipients: ['Interne Mitarbeiter', 'Zahlungsdienstleister'],
thirdCountryTransfers: false,
retentionPeriod: '10 Jahre (handelsrechtliche Aufbewahrungspflicht)',
technicalMeasures: ['Verschlüsselung', 'Zugriffskontrolle'],
organizationalMeasures: ['Schulungen', 'Vertraulichkeitsverpflichtung'],
},
],
generatedAt: new Date().toISOString(),
version: '1.0',
}
}
function generateCookieBanner(context: unknown, options: unknown) {
return {
id: `cookie-${Date.now()}`,
style: 'BANNER',
position: 'BOTTOM',
theme: 'LIGHT',
texts: {
title: 'Cookie-Einstellungen',
description: 'Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.',
acceptAll: 'Alle akzeptieren',
rejectAll: 'Alle ablehnen',
settings: 'Einstellungen',
save: 'Speichern',
},
categories: [
{
id: 'necessary',
name: 'Notwendig',
description: 'Diese Cookies sind für die Grundfunktionen erforderlich.',
required: true,
cookies: [],
},
{
id: 'analytics',
name: 'Analyse',
description: 'Diese Cookies helfen uns, die Nutzung zu verstehen.',
required: false,
cookies: [],
},
{
id: 'marketing',
name: 'Marketing',
description: 'Diese Cookies werden für Werbezwecke verwendet.',
required: false,
cookies: [],
},
],
generatedCode: {
html: `<!-- Cookie Banner HTML -->
<div id="cookie-banner" class="cookie-banner">
<div class="cookie-content">
<h3>Cookie-Einstellungen</h3>
<p>Wir verwenden Cookies, um Ihnen die beste Nutzererfahrung zu bieten.</p>
<div class="cookie-actions">
<button onclick="acceptAll()">Alle akzeptieren</button>
<button onclick="rejectAll()">Alle ablehnen</button>
<button onclick="showSettings()">Einstellungen</button>
</div>
</div>
</div>`,
css: `.cookie-banner {
position: fixed;
bottom: 0;
left: 0;
right: 0;
background: white;
box-shadow: 0 -2px 10px rgba(0,0,0,0.1);
padding: 20px;
z-index: 9999;
}
.cookie-content { max-width: 1200px; margin: 0 auto; }
.cookie-actions { margin-top: 15px; display: flex; gap: 10px; }
.cookie-actions button { padding: 10px 20px; border-radius: 5px; cursor: pointer; }`,
js: `function acceptAll() {
setCookie('consent', 'all', 365);
document.getElementById('cookie-banner').style.display = 'none';
}
function rejectAll() {
setCookie('consent', 'necessary', 365);
document.getElementById('cookie-banner').style.display = 'none';
}
function setCookie(name, value, days) {
const expires = new Date(Date.now() + days * 864e5).toUTCString();
document.cookie = name + '=' + value + '; expires=' + expires + '; path=/; SameSite=Lax';
}`,
},
generatedAt: new Date().toISOString(),
}
}
function generateAuditReport(context: unknown, options: unknown) {
return {
id: `audit-${Date.now()}`,
title: 'Compliance Audit Report',
generatedAt: new Date().toISOString(),
summary: {
totalChecks: 50,
passed: 35,
failed: 10,
warnings: 5,
complianceScore: 70,
},
sections: [
{
title: 'Executive Summary',
content: 'Dieser Bericht fasst den aktuellen Compliance-Status zusammen...',
},
{
title: 'Methodik',
content: 'Die Prüfung wurde gemäß ISO 27001 und DSGVO durchgeführt...',
},
{
title: 'Ergebnisse',
content: 'Hauptabweichungen: 3\nNebenabweichungen: 7\nEmpfehlungen: 5',
},
{
title: 'Empfehlungen',
content: '1. Implementierung von MFA\n2. Verbesserung der Dokumentation\n3. Regelmäßige Schulungen',
},
],
}
}

View File

@@ -0,0 +1,137 @@
/**
* Incidents/Breach Management API Proxy - Catch-all route
* Proxies all /api/sdk/v1/incidents/* requests to ai-compliance-sdk backend
* Supports PDF generation for authority notification forms
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/incidents`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const authHeader = request.headers.get('authorization')
if (authHeader) {
headers['Authorization'] = authHeader
}
const tenantHeader = request.headers.get('x-tenant-id')
if (tenantHeader) {
headers['X-Tenant-Id'] = tenantHeader
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body
}
}
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (PDF authority forms, exports)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Incidents API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,74 @@
/**
* Industry Templates API Proxy - Catch-all route
* Proxies all /api/sdk/v1/industry/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/industry`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Industry API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}

View File

@@ -0,0 +1,111 @@
/**
* Multi-Tenant API Proxy - Catch-all route
* Proxies all /api/sdk/v1/multi-tenant/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/multi-tenant`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
// Forward body for POST/PUT/PATCH/DELETE
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
try {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
} catch {
// No body to forward
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Multi-Tenant API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,75 @@
/**
* Reporting API Proxy - Catch-all route
* Proxies all /api/sdk/v1/reporting/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/reporting`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Reporting API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}

View File

@@ -0,0 +1,111 @@
/**
* SSO API Proxy - Catch-all route
* Proxies all /api/sdk/v1/sso/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/sso`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
// Forward body for POST/PUT/PATCH/DELETE
if (['POST', 'PUT', 'PATCH', 'DELETE'].includes(method)) {
try {
const body = await request.text()
if (body) {
fetchOptions.body = body
}
} catch {
// No body to forward
}
}
const response = await fetch(url, fetchOptions)
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('SSO API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,345 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* SDK State Management API
*
* GET /api/sdk/v1/state?tenantId=xxx - Load state for a tenant
* POST /api/sdk/v1/state - Save state for a tenant
* DELETE /api/sdk/v1/state?tenantId=xxx - Clear state for a tenant
*
* Features:
* - Versioning for optimistic locking
* - Last-Modified headers
* - ETag support for caching
* - Prepared for PostgreSQL migration
*/
// =============================================================================
// TYPES
// =============================================================================
interface StoredState {
state: unknown
version: number
userId?: string
createdAt: string
updatedAt: string
}
// =============================================================================
// STORAGE LAYER (Abstract - Easy to swap to PostgreSQL)
// =============================================================================
/**
* In-memory storage for development
* TODO: Replace with PostgreSQL implementation
*
* PostgreSQL Schema:
* CREATE TABLE sdk_states (
* id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
* tenant_id VARCHAR(255) NOT NULL UNIQUE,
* user_id VARCHAR(255),
* state JSONB NOT NULL,
* version INTEGER DEFAULT 1,
* created_at TIMESTAMP DEFAULT NOW(),
* updated_at TIMESTAMP DEFAULT NOW()
* );
*
* CREATE INDEX idx_sdk_states_tenant ON sdk_states(tenant_id);
*/
interface StateStore {
get(tenantId: string): Promise<StoredState | null>
save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState>
delete(tenantId: string): Promise<boolean>
}
class InMemoryStateStore implements StateStore {
private store: Map<string, StoredState> = new Map()
async get(tenantId: string): Promise<StoredState | null> {
return this.store.get(tenantId) || null
}
async save(
tenantId: string,
state: unknown,
userId?: string,
expectedVersion?: number
): Promise<StoredState> {
const existing = this.store.get(tenantId)
// Optimistic locking check
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
const error = new Error('Version conflict') as Error & { status: number }
error.status = 409
throw error
}
const now = new Date().toISOString()
const newVersion = existing ? existing.version + 1 : 1
const stored: StoredState = {
state: {
...(state as object),
lastModified: now,
},
version: newVersion,
userId,
createdAt: existing?.createdAt || now,
updatedAt: now,
}
this.store.set(tenantId, stored)
return stored
}
async delete(tenantId: string): Promise<boolean> {
return this.store.delete(tenantId)
}
}
// Future PostgreSQL implementation would look like:
// class PostgreSQLStateStore implements StateStore {
// private db: Pool
//
// constructor(connectionString: string) {
// this.db = new Pool({ connectionString })
// }
//
// async get(tenantId: string): Promise<StoredState | null> {
// const result = await this.db.query(
// 'SELECT state, version, user_id, created_at, updated_at FROM sdk_states WHERE tenant_id = $1',
// [tenantId]
// )
// if (result.rows.length === 0) return null
// const row = result.rows[0]
// return {
// state: row.state,
// version: row.version,
// userId: row.user_id,
// createdAt: row.created_at,
// updatedAt: row.updated_at,
// }
// }
//
// async save(tenantId: string, state: unknown, userId?: string, expectedVersion?: number): Promise<StoredState> {
// // Use UPSERT with version check
// const result = await this.db.query(`
// INSERT INTO sdk_states (tenant_id, user_id, state, version)
// VALUES ($1, $2, $3, 1)
// ON CONFLICT (tenant_id) DO UPDATE SET
// state = $3,
// user_id = COALESCE($2, sdk_states.user_id),
// version = sdk_states.version + 1,
// updated_at = NOW()
// WHERE ($4::int IS NULL OR sdk_states.version = $4)
// RETURNING version, created_at, updated_at
// `, [tenantId, userId, JSON.stringify(state), expectedVersion])
//
// if (result.rows.length === 0) {
// throw new Error('Version conflict')
// }
//
// return {
// state,
// version: result.rows[0].version,
// userId,
// createdAt: result.rows[0].created_at,
// updatedAt: result.rows[0].updated_at,
// }
// }
//
// async delete(tenantId: string): Promise<boolean> {
// const result = await this.db.query(
// 'DELETE FROM sdk_states WHERE tenant_id = $1',
// [tenantId]
// )
// return result.rowCount > 0
// }
// }
// Use in-memory store for now
const stateStore: StateStore = new InMemoryStateStore()
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function generateETag(version: number, updatedAt: string): string {
return `"${version}-${Buffer.from(updatedAt).toString('base64').slice(0, 8)}"`
}
// =============================================================================
// HANDLERS
// =============================================================================
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
const stored = await stateStore.get(tenantId)
if (!stored) {
return NextResponse.json(
{ success: false, error: 'State not found', tenantId },
{ status: 404 }
)
}
const etag = generateETag(stored.version, stored.updatedAt)
// Check If-None-Match header for caching
const ifNoneMatch = request.headers.get('If-None-Match')
if (ifNoneMatch === etag) {
return new NextResponse(null, { status: 304 })
}
return NextResponse.json(
{
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
},
{
headers: {
'ETag': etag,
'Last-Modified': stored.updatedAt,
'Cache-Control': 'private, no-cache',
},
}
)
} catch (error) {
console.error('Failed to load SDK state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to load state' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state, version } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required' },
{ status: 400 }
)
}
// Check If-Match header for optimistic locking
const ifMatch = request.headers.get('If-Match')
const expectedVersion = ifMatch ? parseInt(ifMatch, 10) : version
const stored = await stateStore.save(tenantId, state, body.userId, expectedVersion)
const etag = generateETag(stored.version, stored.updatedAt)
return NextResponse.json(
{
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
},
{
headers: {
'ETag': etag,
'Last-Modified': stored.updatedAt,
},
}
)
} catch (error) {
const err = error as Error & { status?: number }
// Handle version conflict
if (err.status === 409 || err.message === 'Version conflict') {
return NextResponse.json(
{
success: false,
error: 'Version conflict. State was modified by another request.',
code: 'VERSION_CONFLICT',
},
{ status: 409 }
)
}
console.error('Failed to save SDK state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to save state' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
const deleted = await stateStore.delete(tenantId)
if (!deleted) {
return NextResponse.json(
{ success: false, error: 'State not found', tenantId },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
tenantId,
deletedAt: new Date().toISOString(),
})
} catch (error) {
console.error('Failed to delete SDK state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete state' },
{ status: 500 }
)
}
}
// =============================================================================
// HEALTH CHECK
// =============================================================================
export async function OPTIONS() {
return NextResponse.json({ status: 'ok' }, {
headers: {
'Allow': 'GET, POST, DELETE, OPTIONS',
},
})
}

View File

@@ -0,0 +1,107 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator Controls Evaluation API
*
* POST /api/sdk/v1/tom-generator/controls/evaluate - Evaluate controls for given state
*
* Request body:
* {
* state: TOMGeneratorState
* }
*
* Response:
* {
* evaluations: RulesEngineResult[]
* derivedTOMs: DerivedTOM[]
* summary: {
* total: number
* required: number
* recommended: number
* optional: number
* notApplicable: number
* }
* }
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { state } = body
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required in request body' },
{ status: 400 }
)
}
// Parse dates in state
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})) || [],
documents: [],
derivedTOMs: [],
gapAnalysis: null,
exports: [],
}
// Initialize rules engine and evaluate
const engine = new TOMRulesEngine()
const evaluations = engine.evaluateControls(parsedState)
const derivedTOMs = engine.deriveAllTOMs(parsedState)
// Calculate summary
const summary = {
total: evaluations.length,
required: evaluations.filter((e) => e.applicability === 'REQUIRED').length,
recommended: evaluations.filter((e) => e.applicability === 'RECOMMENDED').length,
optional: evaluations.filter((e) => e.applicability === 'OPTIONAL').length,
notApplicable: evaluations.filter((e) => e.applicability === 'NOT_APPLICABLE').length,
}
// Group by category
const byCategory: Record<string, typeof evaluations> = {}
evaluations.forEach((e) => {
const category = e.controlId.split('-')[1] // Extract category from ID like TOM-AC-01
if (!byCategory[category]) {
byCategory[category] = []
}
byCategory[category].push(e)
})
return NextResponse.json({
success: true,
data: {
evaluations,
derivedTOMs,
summary,
byCategory,
},
})
} catch (error) {
console.error('Failed to evaluate controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to evaluate controls' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,128 @@
import { NextRequest, NextResponse } from 'next/server'
import {
getAllControls,
getControlById,
getControlsByCategory,
searchControls,
getCategories,
} from '@/lib/sdk/tom-generator/controls/loader'
import { ControlCategory } from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator Controls API
*
* GET /api/sdk/v1/tom-generator/controls - List all controls
* GET /api/sdk/v1/tom-generator/controls?id=xxx - Get single control
* GET /api/sdk/v1/tom-generator/controls?category=xxx - Filter by category
* GET /api/sdk/v1/tom-generator/controls?search=xxx - Search controls
* GET /api/sdk/v1/tom-generator/controls?categories=true - Get categories list
*/
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const id = searchParams.get('id')
const category = searchParams.get('category')
const search = searchParams.get('search')
const categoriesOnly = searchParams.get('categories')
const language = (searchParams.get('language') || 'de') as 'de' | 'en'
// Get categories list
if (categoriesOnly === 'true') {
const categories = getCategories()
return NextResponse.json({
success: true,
data: categories,
})
}
// Get single control by ID
if (id) {
const control = getControlById(id)
if (!control) {
return NextResponse.json(
{ success: false, error: `Control not found: ${id}` },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: {
...control,
// Return localized name and description
localizedName: control.name[language],
localizedDescription: control.description[language],
},
})
}
// Filter by category
if (category) {
const controls = getControlsByCategory(category as ControlCategory)
return NextResponse.json({
success: true,
data: controls.map((c) => ({
...c,
localizedName: c.name[language],
localizedDescription: c.description[language],
})),
meta: {
category,
count: controls.length,
},
})
}
// Search controls
if (search) {
const controls = searchControls(search, language)
return NextResponse.json({
success: true,
data: controls.map((c) => ({
...c,
localizedName: c.name[language],
localizedDescription: c.description[language],
})),
meta: {
query: search,
count: controls.length,
},
})
}
// Return all controls
const controls = getAllControls()
const categories = getCategories()
return NextResponse.json({
success: true,
data: controls.map((c) => ({
...c,
localizedName: c.name[language],
localizedDescription: c.description[language],
})),
meta: {
totalControls: controls.length,
categories: categories.length,
language,
},
})
} catch (error) {
console.error('Failed to fetch controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch controls' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,121 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMDocumentAnalyzer } from '@/lib/sdk/tom-generator/ai/document-analyzer'
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
/**
* TOM Generator Evidence Analysis API
*
* POST /api/sdk/v1/tom-generator/evidence/[id]/analyze - Analyze evidence document with AI
*
* Request body:
* {
* tenantId: string
* documentText?: string (if already extracted)
* }
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
const { tenantId, documentText } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
// Get the document
const document = await evidenceStore.getById(tenantId, id)
if (!document) {
return NextResponse.json(
{ success: false, error: `Document not found: ${id}` },
{ status: 404 }
)
}
// Check if already analyzed
if (document.aiAnalysis && document.status === 'ANALYZED') {
return NextResponse.json({
success: true,
data: document.aiAnalysis,
meta: {
alreadyAnalyzed: true,
analyzedAt: document.aiAnalysis.analyzedAt,
},
})
}
// Get document text (in production, this would be extracted from the file)
const text = documentText || `[Document content from ${document.originalName}]`
// Initialize analyzer
const analyzer = new TOMDocumentAnalyzer()
// Analyze the document
const analysisResult = await analyzer.analyzeDocument(
document,
text,
'de'
)
// Check if analysis was successful
if (!analysisResult.success || !analysisResult.analysis) {
return NextResponse.json(
{ success: false, error: analysisResult.error || 'Analysis failed' },
{ status: 500 }
)
}
const analysis = analysisResult.analysis
// Update the document with analysis results
const updatedDocument = await evidenceStore.update(tenantId, id, {
aiAnalysis: analysis,
status: 'ANALYZED',
linkedControlIds: [
...new Set([
...document.linkedControlIds,
...analysis.applicableControls,
]),
],
})
return NextResponse.json({
success: true,
data: {
analysis,
document: updatedDocument,
},
meta: {
documentId: id,
analyzedAt: analysis.analyzedAt,
confidence: analysis.confidence,
applicableControlsCount: analysis.applicableControls.length,
gapsCount: analysis.gaps.length,
},
})
} catch (error) {
console.error('Failed to analyze evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to analyze evidence' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,153 @@
import { NextRequest, NextResponse } from 'next/server'
import { DocumentType } from '@/lib/sdk/tom-generator/types'
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
/**
* TOM Generator Evidence API
*
* GET /api/sdk/v1/tom-generator/evidence?tenantId=xxx - List all evidence documents
* DELETE /api/sdk/v1/tom-generator/evidence?tenantId=xxx&id=xxx - Delete evidence
*/
// =============================================================================
// HANDLERS
// =============================================================================
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
const documentType = searchParams.get('type') as DocumentType | null
const status = searchParams.get('status')
const id = searchParams.get('id')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
// Get single document
if (id) {
const document = await evidenceStore.getById(tenantId, id)
if (!document) {
return NextResponse.json(
{ success: false, error: `Document not found: ${id}` },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
data: document,
})
}
// Filter by type
if (documentType) {
const documents = await evidenceStore.getByType(tenantId, documentType)
return NextResponse.json({
success: true,
data: documents,
meta: {
count: documents.length,
filter: { type: documentType },
},
})
}
// Filter by status
if (status) {
const documents = await evidenceStore.getByStatus(tenantId, status)
return NextResponse.json({
success: true,
data: documents,
meta: {
count: documents.length,
filter: { status },
},
})
}
// Get all documents
const documents = await evidenceStore.getAll(tenantId)
// Group by type for summary
const byType: Record<string, number> = {}
const byStatus: Record<string, number> = {}
documents.forEach((doc) => {
byType[doc.documentType] = (byType[doc.documentType] || 0) + 1
byStatus[doc.status] = (byStatus[doc.status] || 0) + 1
})
return NextResponse.json({
success: true,
data: documents,
meta: {
count: documents.length,
byType,
byStatus,
},
})
} catch (error) {
console.error('Failed to fetch evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch evidence' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
const id = searchParams.get('id')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!id) {
return NextResponse.json(
{ success: false, error: 'id is required' },
{ status: 400 }
)
}
const deleted = await evidenceStore.delete(tenantId, id)
if (!deleted) {
return NextResponse.json(
{ success: false, error: `Document not found: ${id}` },
{ status: 404 }
)
}
return NextResponse.json({
success: true,
id,
deletedAt: new Date().toISOString(),
})
} catch (error) {
console.error('Failed to delete evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete evidence' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, DELETE, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,155 @@
import { NextRequest, NextResponse } from 'next/server'
import { EvidenceDocument, DocumentType } from '@/lib/sdk/tom-generator/types'
import { evidenceStore } from '@/lib/sdk/tom-generator/evidence-store'
import crypto from 'crypto'
/**
* TOM Generator Evidence Upload API
*
* POST /api/sdk/v1/tom-generator/evidence/upload - Upload evidence document
*
* Request: multipart/form-data
* - file: File
* - tenantId: string
* - documentType: DocumentType
* - validFrom?: string (ISO date)
* - validUntil?: string (ISO date)
* - linkedControlIds?: string (comma-separated)
*/
// Document type detection based on filename patterns
function detectDocumentType(filename: string, mimeType: string): DocumentType {
const lower = filename.toLowerCase()
if (lower.includes('avv') || lower.includes('auftragsverarbeitung')) {
return 'AVV'
}
if (lower.includes('dpa') || lower.includes('data processing')) {
return 'DPA'
}
if (lower.includes('sla') || lower.includes('service level')) {
return 'SLA'
}
if (lower.includes('nda') || lower.includes('vertraulichkeit') || lower.includes('geheimhaltung')) {
return 'NDA'
}
if (lower.includes('policy') || lower.includes('richtlinie')) {
return 'POLICY'
}
if (lower.includes('cert') || lower.includes('zertifikat') || lower.includes('iso')) {
return 'CERTIFICATE'
}
if (lower.includes('audit') || lower.includes('prüf') || lower.includes('bericht')) {
return 'AUDIT_REPORT'
}
return 'OTHER'
}
export async function POST(request: NextRequest) {
try {
const formData = await request.formData()
const file = formData.get('file') as File | null
const tenantId = formData.get('tenantId') as string | null
const documentType = formData.get('documentType') as DocumentType | null
const validFrom = formData.get('validFrom') as string | null
const validUntil = formData.get('validUntil') as string | null
const linkedControlIdsStr = formData.get('linkedControlIds') as string | null
const uploadedBy = formData.get('uploadedBy') as string | null
if (!file) {
return NextResponse.json(
{ success: false, error: 'file is required' },
{ status: 400 }
)
}
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
// Read file data
const arrayBuffer = await file.arrayBuffer()
const buffer = Buffer.from(arrayBuffer)
// Generate hash for deduplication
const hash = crypto.createHash('sha256').update(buffer).digest('hex')
// Generate unique filename
const id = crypto.randomUUID()
const ext = file.name.split('.').pop() || 'bin'
const filename = `${id}.${ext}`
// Detect document type if not provided
const detectedType = detectDocumentType(file.name, file.type)
const finalDocumentType = documentType || detectedType
// Parse linked control IDs
const linkedControlIds = linkedControlIdsStr
? linkedControlIdsStr.split(',').map((s) => s.trim()).filter(Boolean)
: []
// Create evidence document
const document: EvidenceDocument = {
id,
filename,
originalName: file.name,
mimeType: file.type,
size: file.size,
uploadedAt: new Date(),
uploadedBy: uploadedBy || 'unknown',
documentType: finalDocumentType,
detectedType,
hash,
validFrom: validFrom ? new Date(validFrom) : null,
validUntil: validUntil ? new Date(validUntil) : null,
linkedControlIds,
aiAnalysis: null,
status: 'PENDING',
}
// Store the document metadata
// Note: In production, the actual file would be stored in MinIO/S3
await evidenceStore.add(tenantId, document)
return NextResponse.json({
success: true,
data: {
id: document.id,
filename: document.filename,
originalName: document.originalName,
mimeType: document.mimeType,
size: document.size,
documentType: document.documentType,
detectedType: document.detectedType,
status: document.status,
uploadedAt: document.uploadedAt.toISOString(),
},
meta: {
hash,
needsAnalysis: true,
analyzeUrl: `/api/sdk/v1/tom-generator/evidence/${id}/analyze`,
},
})
} catch (error) {
console.error('Failed to upload evidence:', error)
return NextResponse.json(
{ success: false, error: 'Failed to upload evidence' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,245 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMGeneratorState } from '@/lib/sdk/tom-generator/types'
import { generateDOCXContent, generateDOCXFilename } from '@/lib/sdk/tom-generator/export/docx'
import { generatePDFContent, generatePDFFilename } from '@/lib/sdk/tom-generator/export/pdf'
import { generateZIPFiles, generateZIPFilename } from '@/lib/sdk/tom-generator/export/zip'
import crypto from 'crypto'
/**
* TOM Generator Export API
*
* POST /api/sdk/v1/tom-generator/export - Generate export
*
* Request body:
* {
* tenantId: string
* format: 'DOCX' | 'PDF' | 'JSON' | 'ZIP'
* language: 'de' | 'en'
* state: TOMGeneratorState
* options?: {
* includeEvidence?: boolean
* includeGapAnalysis?: boolean
* companyLogo?: string (base64)
* }
* }
*/
// In-memory export store for tracking exports
interface StoredExport {
id: string
tenantId: string
format: string
filename: string
content: string // Base64 encoded content
generatedAt: Date
size: number
}
const exportStore: Map<string, StoredExport> = new Map()
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, format, language = 'de', state, options = {} } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!format) {
return NextResponse.json(
{ success: false, error: 'format is required (DOCX, PDF, JSON, ZIP)' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required' },
{ status: 400 }
)
}
// Parse dates in state
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})) || [],
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...doc,
uploadedAt: new Date(doc.uploadedAt),
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
aiAnalysis: doc.aiAnalysis ? {
...doc.aiAnalysis,
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
} : null,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
const exportId = crypto.randomUUID()
let content: string
let filename: string
let mimeType: string
switch (format.toUpperCase()) {
case 'DOCX': {
// Generate DOCX structure (actual binary conversion would require docx library)
const docxContent = generateDOCXContent(parsedState, { language: language as 'de' | 'en', ...options })
content = Buffer.from(JSON.stringify(docxContent, null, 2)).toString('base64')
filename = generateDOCXFilename(parsedState, language as 'de' | 'en')
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
break
}
case 'PDF': {
// Generate PDF structure (actual binary conversion would require pdf library)
const pdfContent = generatePDFContent(parsedState, { language: language as 'de' | 'en', ...options })
content = Buffer.from(JSON.stringify(pdfContent, null, 2)).toString('base64')
filename = generatePDFFilename(parsedState, language as 'de' | 'en')
mimeType = 'application/pdf'
break
}
case 'JSON':
content = Buffer.from(JSON.stringify(parsedState, null, 2)).toString('base64')
filename = `tom-export-${tenantId}-${new Date().toISOString().split('T')[0]}.json`
mimeType = 'application/json'
break
case 'ZIP': {
const files = generateZIPFiles(parsedState, { language: language as 'de' | 'en', ...options })
// For now, return the files metadata (actual ZIP generation would require a library)
content = Buffer.from(JSON.stringify(files, null, 2)).toString('base64')
filename = generateZIPFilename(parsedState, language as 'de' | 'en')
mimeType = 'application/zip'
break
}
default:
return NextResponse.json(
{ success: false, error: `Unsupported format: ${format}` },
{ status: 400 }
)
}
// Store the export
const storedExport: StoredExport = {
id: exportId,
tenantId,
format: format.toUpperCase(),
filename,
content,
generatedAt: new Date(),
size: Buffer.from(content, 'base64').length,
}
exportStore.set(exportId, storedExport)
return NextResponse.json({
success: true,
data: {
exportId,
filename,
format: format.toUpperCase(),
mimeType,
size: storedExport.size,
generatedAt: storedExport.generatedAt.toISOString(),
downloadUrl: `/api/sdk/v1/tom-generator/export?exportId=${exportId}`,
},
})
} catch (error) {
console.error('Failed to generate export:', error)
return NextResponse.json(
{ success: false, error: 'Failed to generate export' },
{ status: 500 }
)
}
}
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const exportId = searchParams.get('exportId')
if (!exportId) {
return NextResponse.json(
{ success: false, error: 'exportId is required' },
{ status: 400 }
)
}
const storedExport = exportStore.get(exportId)
if (!storedExport) {
return NextResponse.json(
{ success: false, error: `Export not found: ${exportId}` },
{ status: 404 }
)
}
// Return the file as download
const buffer = Buffer.from(storedExport.content, 'base64')
let mimeType: string
switch (storedExport.format) {
case 'DOCX':
mimeType = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
break
case 'PDF':
mimeType = 'application/pdf'
break
case 'JSON':
mimeType = 'application/json'
break
case 'ZIP':
mimeType = 'application/zip'
break
default:
mimeType = 'application/octet-stream'
}
return new NextResponse(buffer, {
headers: {
'Content-Type': mimeType,
'Content-Disposition': `attachment; filename="${storedExport.filename}"`,
'Content-Length': buffer.length.toString(),
},
})
} catch (error) {
console.error('Failed to download export:', error)
return NextResponse.json(
{ success: false, error: 'Failed to download export' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,205 @@
import { NextRequest, NextResponse } from 'next/server'
import { TOMRulesEngine } from '@/lib/sdk/tom-generator/rules-engine'
import { TOMGeneratorState, GapAnalysisResult } from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator Gap Analysis API
*
* POST /api/sdk/v1/tom-generator/gap-analysis - Perform gap analysis
*
* Request body:
* {
* tenantId: string
* state: TOMGeneratorState
* }
*
* Response:
* {
* gapAnalysis: GapAnalysisResult
* }
*/
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required in request body' },
{ status: 400 }
)
}
// Parse dates in state
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps?.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})) || [],
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...doc,
uploadedAt: new Date(doc.uploadedAt),
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
aiAnalysis: doc.aiAnalysis ? {
...doc.aiAnalysis,
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
} : null,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
// Initialize rules engine
const engine = new TOMRulesEngine()
// Perform gap analysis using derived TOMs and documents from state
const gapAnalysis = engine.performGapAnalysis(
parsedState.derivedTOMs,
parsedState.documents
)
// Calculate detailed metrics
const metrics = calculateGapMetrics(gapAnalysis)
return NextResponse.json({
success: true,
data: {
gapAnalysis,
metrics,
generatedAt: gapAnalysis.generatedAt.toISOString(),
},
})
} catch (error) {
console.error('Failed to perform gap analysis:', error)
return NextResponse.json(
{ success: false, error: 'Failed to perform gap analysis' },
{ status: 500 }
)
}
}
function calculateGapMetrics(gapAnalysis: GapAnalysisResult) {
const totalGaps = gapAnalysis.missingControls.length +
gapAnalysis.partialControls.length +
gapAnalysis.missingEvidence.length
const criticalGaps = gapAnalysis.missingControls.filter(
(c) => c.priority === 'CRITICAL' || c.priority === 'HIGH'
).length
const mediumGaps = gapAnalysis.missingControls.filter(
(c) => c.priority === 'MEDIUM'
).length
const lowGaps = gapAnalysis.missingControls.filter(
(c) => c.priority === 'LOW'
).length
// Group missing controls by category
const gapsByCategory: Record<string, number> = {}
gapAnalysis.missingControls.forEach((control) => {
const category = control.controlId.split('-')[1] || 'OTHER'
gapsByCategory[category] = (gapsByCategory[category] || 0) + 1
})
// Calculate compliance readiness
const maxScore = 100
const deductionPerCritical = 10
const deductionPerMedium = 5
const deductionPerLow = 2
const deductionPerPartial = 3
const deductionPerMissingEvidence = 1
const deductions =
criticalGaps * deductionPerCritical +
mediumGaps * deductionPerMedium +
lowGaps * deductionPerLow +
gapAnalysis.partialControls.length * deductionPerPartial +
gapAnalysis.missingEvidence.length * deductionPerMissingEvidence
const complianceReadiness = Math.max(0, Math.min(100, maxScore - deductions))
// Prioritized action items
const prioritizedActions = [
...gapAnalysis.missingControls
.filter((c) => c.priority === 'CRITICAL')
.map((c) => ({
type: 'MISSING_CONTROL',
priority: 'CRITICAL',
controlId: c.controlId,
reason: c.reason,
action: `Implement control ${c.controlId}`,
})),
...gapAnalysis.missingControls
.filter((c) => c.priority === 'HIGH')
.map((c) => ({
type: 'MISSING_CONTROL',
priority: 'HIGH',
controlId: c.controlId,
reason: c.reason,
action: `Implement control ${c.controlId}`,
})),
...gapAnalysis.partialControls.map((c) => ({
type: 'PARTIAL_CONTROL',
priority: 'MEDIUM',
controlId: c.controlId,
missingAspects: c.missingAspects,
action: `Complete implementation of ${c.controlId}`,
})),
...gapAnalysis.missingEvidence.map((e) => ({
type: 'MISSING_EVIDENCE',
priority: 'LOW',
controlId: e.controlId,
requiredEvidence: e.requiredEvidence,
action: `Upload evidence for ${e.controlId}`,
})),
]
return {
totalGaps,
criticalGaps,
mediumGaps,
lowGaps,
partialControls: gapAnalysis.partialControls.length,
missingEvidence: gapAnalysis.missingEvidence.length,
gapsByCategory,
complianceReadiness,
overallScore: gapAnalysis.overallScore,
prioritizedActionsCount: prioritizedActions.length,
prioritizedActions: prioritizedActions.slice(0, 10), // Top 10 actions
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'POST, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,250 @@
import { NextRequest, NextResponse } from 'next/server'
import {
TOMGeneratorState,
createEmptyTOMGeneratorState,
} from '@/lib/sdk/tom-generator/types'
/**
* TOM Generator State API
*
* GET /api/sdk/v1/tom-generator/state?tenantId=xxx - Load TOM generator state
* POST /api/sdk/v1/tom-generator/state - Save TOM generator state
* DELETE /api/sdk/v1/tom-generator/state?tenantId=xxx - Clear state
*/
// =============================================================================
// STORAGE (In-Memory for development)
// =============================================================================
interface StoredTOMState {
state: TOMGeneratorState
version: number
createdAt: string
updatedAt: string
}
class InMemoryTOMStateStore {
private store: Map<string, StoredTOMState> = new Map()
async get(tenantId: string): Promise<StoredTOMState | null> {
return this.store.get(tenantId) || null
}
async save(tenantId: string, state: TOMGeneratorState, expectedVersion?: number): Promise<StoredTOMState> {
const existing = this.store.get(tenantId)
if (expectedVersion !== undefined && existing && existing.version !== expectedVersion) {
const error = new Error('Version conflict') as Error & { status: number }
error.status = 409
throw error
}
const now = new Date().toISOString()
const newVersion = existing ? existing.version + 1 : 1
const stored: StoredTOMState = {
state: {
...state,
updatedAt: new Date(now),
},
version: newVersion,
createdAt: existing?.createdAt || now,
updatedAt: now,
}
this.store.set(tenantId, stored)
return stored
}
async delete(tenantId: string): Promise<boolean> {
return this.store.delete(tenantId)
}
async list(): Promise<{ tenantId: string; updatedAt: string }[]> {
const result: { tenantId: string; updatedAt: string }[] = []
this.store.forEach((value, key) => {
result.push({ tenantId: key, updatedAt: value.updatedAt })
})
return result
}
}
const stateStore = new InMemoryTOMStateStore()
// =============================================================================
// HANDLERS
// =============================================================================
export async function GET(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
// List all states if no tenantId provided
if (!tenantId) {
const states = await stateStore.list()
return NextResponse.json({
success: true,
data: states,
})
}
const stored = await stateStore.get(tenantId)
if (!stored) {
// Return empty state for new tenants
const emptyState = createEmptyTOMGeneratorState(tenantId)
return NextResponse.json({
success: true,
data: {
tenantId,
state: emptyState,
version: 0,
isNew: true,
},
})
}
return NextResponse.json({
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
})
} catch (error) {
console.error('Failed to load TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to load state' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const { tenantId, state, version } = body
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
if (!state) {
return NextResponse.json(
{ success: false, error: 'state is required' },
{ status: 400 }
)
}
// Deserialize dates
const parsedState: TOMGeneratorState = {
...state,
createdAt: new Date(state.createdAt),
updatedAt: new Date(state.updatedAt),
steps: state.steps.map((step: { id: string; completed: boolean; data: unknown; validatedAt: string | null }) => ({
...step,
validatedAt: step.validatedAt ? new Date(step.validatedAt) : null,
})),
documents: state.documents?.map((doc: { uploadedAt: string; validFrom?: string; validUntil?: string; aiAnalysis?: { analyzedAt: string } }) => ({
...doc,
uploadedAt: new Date(doc.uploadedAt),
validFrom: doc.validFrom ? new Date(doc.validFrom) : null,
validUntil: doc.validUntil ? new Date(doc.validUntil) : null,
aiAnalysis: doc.aiAnalysis ? {
...doc.aiAnalysis,
analyzedAt: new Date(doc.aiAnalysis.analyzedAt),
} : null,
})) || [],
derivedTOMs: state.derivedTOMs?.map((tom: { implementationDate?: string; reviewDate?: string }) => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
})) || [],
gapAnalysis: state.gapAnalysis ? {
...state.gapAnalysis,
generatedAt: new Date(state.gapAnalysis.generatedAt),
} : null,
exports: state.exports?.map((exp: { generatedAt: string }) => ({
...exp,
generatedAt: new Date(exp.generatedAt),
})) || [],
}
const stored = await stateStore.save(tenantId, parsedState, version)
return NextResponse.json({
success: true,
data: {
tenantId,
state: stored.state,
version: stored.version,
lastModified: stored.updatedAt,
},
})
} catch (error) {
const err = error as Error & { status?: number }
if (err.status === 409 || err.message === 'Version conflict') {
return NextResponse.json(
{
success: false,
error: 'Version conflict. State was modified by another request.',
code: 'VERSION_CONFLICT',
},
{ status: 409 }
)
}
console.error('Failed to save TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to save state' },
{ status: 500 }
)
}
}
export async function DELETE(request: NextRequest) {
try {
const { searchParams } = new URL(request.url)
const tenantId = searchParams.get('tenantId')
if (!tenantId) {
return NextResponse.json(
{ success: false, error: 'tenantId is required' },
{ status: 400 }
)
}
const deleted = await stateStore.delete(tenantId)
return NextResponse.json({
success: true,
tenantId,
deleted,
deletedAt: new Date().toISOString(),
})
} catch (error) {
console.error('Failed to delete TOM generator state:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete state' },
{ status: 500 }
)
}
}
export async function OPTIONS() {
return NextResponse.json(
{ status: 'ok' },
{
headers: {
Allow: 'GET, POST, DELETE, OPTIONS',
},
}
)
}

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Forward the request to the SDK backend
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/assess`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward tenant ID if present
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
console.error('SDK backend error:', errorText)
return NextResponse.json(
{ error: 'SDK backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Failed to call SDK backend:', error)
return NextResponse.json(
{ error: 'Failed to connect to SDK backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,40 @@
import { NextRequest, NextResponse } from 'next/server'
const SDK_BASE_URL = process.env.SDK_BASE_URL || 'http://localhost:8085'
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Forward the request to the SDK backend
const response = await fetch(`${SDK_BASE_URL}/sdk/v1/ucca/obligations/export/direct`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
// Forward tenant ID if present
...(request.headers.get('X-Tenant-ID') && {
'X-Tenant-ID': request.headers.get('X-Tenant-ID') as string,
}),
},
body: JSON.stringify(body),
})
if (!response.ok) {
const errorText = await response.text()
console.error('SDK backend error:', errorText)
return NextResponse.json(
{ error: 'SDK backend error', details: errorText },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Failed to call SDK backend:', error)
return NextResponse.json(
{ error: 'Failed to connect to SDK backend' },
{ status: 503 }
)
}
}

View File

@@ -0,0 +1,197 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import {
Finding,
CONTRACT_REVIEW_SYSTEM_PROMPT,
} from '@/lib/sdk/vendor-compliance'
/**
* POST /api/sdk/v1/vendor-compliance/contracts/[id]/review
*
* Starts the LLM-based contract review process
*/
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: contractId } = await params
// In production:
// 1. Fetch contract from database
// 2. Extract text from PDF/DOCX using embedding-service
// 3. Send to LLM for analysis
// 4. Store findings in database
// 5. Update contract with compliance score
// For demo, return mock analysis results
const mockFindings: Finding[] = [
{
id: uuidv4(),
tenantId: 'default',
contractId,
vendorId: 'mock-vendor',
type: 'OK',
category: 'AVV_CONTENT',
severity: 'LOW',
title: {
de: 'Weisungsgebundenheit vorhanden',
en: 'Instruction binding present',
},
description: {
de: 'Der Vertrag enthält eine angemessene Regelung zur Weisungsgebundenheit des Auftragsverarbeiters.',
en: 'The contract contains an appropriate provision for processor instruction binding.',
},
citations: [
{
documentId: contractId,
page: 2,
startChar: 150,
endChar: 350,
quotedText: 'Der Auftragnehmer verarbeitet personenbezogene Daten ausschließlich auf dokumentierte Weisung des Auftraggebers.',
quoteHash: 'abc123',
},
],
affectedRequirement: 'Art. 28 Abs. 3 lit. a DSGVO',
triggeredControls: ['VND-CON-01'],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: uuidv4(),
tenantId: 'default',
contractId,
vendorId: 'mock-vendor',
type: 'GAP',
category: 'INCIDENT',
severity: 'HIGH',
title: {
de: 'Meldefrist für Datenpannen zu lang',
en: 'Data breach notification deadline too long',
},
description: {
de: 'Die vereinbarte Meldefrist von 72 Stunden ist zu lang, um die eigene Meldepflicht gegenüber der Aufsichtsbehörde fristgerecht erfüllen zu können.',
en: 'The agreed notification deadline of 72 hours is too long to meet own notification obligations to the supervisory authority in time.',
},
recommendation: {
de: 'Verhandeln Sie eine kürzere Meldefrist von maximal 24-48 Stunden.',
en: 'Negotiate a shorter notification deadline of maximum 24-48 hours.',
},
citations: [
{
documentId: contractId,
page: 5,
startChar: 820,
endChar: 950,
quotedText: 'Der Auftragnehmer wird den Auftraggeber innerhalb von 72 Stunden über eine Verletzung des Schutzes personenbezogener Daten informieren.',
quoteHash: 'def456',
},
],
affectedRequirement: 'Art. 33 Abs. 2 DSGVO',
triggeredControls: ['VND-INC-01'],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
},
{
id: uuidv4(),
tenantId: 'default',
contractId,
vendorId: 'mock-vendor',
type: 'RISK',
category: 'TRANSFER',
severity: 'MEDIUM',
title: {
de: 'Drittlandtransfer USA ohne TIA',
en: 'Third country transfer to USA without TIA',
},
description: {
de: 'Der Vertrag erlaubt Datenverarbeitung in den USA. Es liegt jedoch kein Transfer Impact Assessment (TIA) vor.',
en: 'The contract allows data processing in the USA. However, no Transfer Impact Assessment (TIA) is available.',
},
recommendation: {
de: 'Führen Sie ein TIA durch und dokumentieren Sie zusätzliche Schutzmaßnahmen.',
en: 'Conduct a TIA and document supplementary measures.',
},
citations: [
{
documentId: contractId,
page: 8,
startChar: 1200,
endChar: 1350,
quotedText: 'Die Verarbeitung kann auch in Rechenzentren in den Vereinigten Staaten von Amerika erfolgen.',
quoteHash: 'ghi789',
},
],
affectedRequirement: 'Art. 44-49 DSGVO, Schrems II',
triggeredControls: ['VND-TRF-01', 'VND-TRF-03'],
status: 'OPEN',
createdAt: new Date(),
updatedAt: new Date(),
},
]
// Calculate compliance score based on findings
const okFindings = mockFindings.filter((f) => f.type === 'OK').length
const totalChecks = mockFindings.length + 5 // Assume 5 additional checks passed
const complianceScore = Math.round((okFindings / totalChecks) * 100 + 60) // Base score + passed checks
return NextResponse.json({
success: true,
data: {
contractId,
findings: mockFindings,
complianceScore: Math.min(100, complianceScore),
reviewCompletedAt: new Date().toISOString(),
topRisks: [
{ de: 'Meldefrist für Datenpannen zu lang', en: 'Data breach notification deadline too long' },
{ de: 'Fehlende TIA für USA-Transfer', en: 'Missing TIA for USA transfer' },
],
requiredActions: [
{ de: 'Meldefrist auf 24-48h verkürzen', en: 'Reduce notification deadline to 24-48h' },
{ de: 'TIA für USA-Transfer durchführen', en: 'Conduct TIA for USA transfer' },
],
},
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error reviewing contract:', error)
return NextResponse.json(
{ success: false, error: 'Failed to review contract' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/vendor-compliance/contracts/[id]/review
*
* Get existing review results
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id: contractId } = await params
// In production, fetch from database
return NextResponse.json({
success: true,
data: {
contractId,
findings: [],
complianceScore: null,
reviewStatus: 'PENDING',
},
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching review:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch review' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,88 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { ContractDocument } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const contracts: Map<string, ContractDocument> = new Map()
export async function GET(request: NextRequest) {
try {
const contractList = Array.from(contracts.values())
return NextResponse.json({
success: true,
data: contractList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching contracts:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch contracts' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
// Handle multipart form data for file upload
const formData = await request.formData()
const file = formData.get('file') as File | null
const vendorId = formData.get('vendorId') as string
const metadataStr = formData.get('metadata') as string
if (!file || !vendorId) {
return NextResponse.json(
{ success: false, error: 'File and vendorId are required' },
{ status: 400 }
)
}
const metadata = metadataStr ? JSON.parse(metadataStr) : {}
const id = uuidv4()
// In production, upload file to storage (MinIO, S3, etc.)
const storagePath = `contracts/${id}/${file.name}`
const contract: ContractDocument = {
id,
tenantId: 'default',
vendorId,
fileName: `${id}-${file.name}`,
originalName: file.name,
mimeType: file.type,
fileSize: file.size,
storagePath,
documentType: metadata.documentType || 'OTHER',
version: metadata.version || '1.0',
previousVersionId: metadata.previousVersionId,
parties: metadata.parties,
effectiveDate: metadata.effectiveDate ? new Date(metadata.effectiveDate) : undefined,
expirationDate: metadata.expirationDate ? new Date(metadata.expirationDate) : undefined,
autoRenewal: metadata.autoRenewal,
renewalNoticePeriod: metadata.renewalNoticePeriod,
terminationNoticePeriod: metadata.terminationNoticePeriod,
reviewStatus: 'PENDING',
status: 'DRAFT',
createdAt: new Date(),
updatedAt: new Date(),
}
contracts.set(id, contract)
return NextResponse.json(
{
success: true,
data: contract,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error uploading contract:', error)
return NextResponse.json(
{ success: false, error: 'Failed to upload contract' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,28 @@
import { NextRequest, NextResponse } from 'next/server'
import { CONTROLS_LIBRARY } from '@/lib/sdk/vendor-compliance'
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const domain = searchParams.get('domain')
let controls = [...CONTROLS_LIBRARY]
// Filter by domain if provided
if (domain) {
controls = controls.filter((c) => c.domain === domain)
}
return NextResponse.json({
success: true,
data: controls,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching controls:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch controls' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,75 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* GET /api/sdk/v1/vendor-compliance/export/[reportId]/download
*
* Download a generated report file.
* In production, this would redirect to a signed MinIO/S3 URL or stream the file.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Implement actual file download
// This would typically:
// 1. Verify report exists and user has access
// 2. Generate signed URL for MinIO/S3
// 3. Redirect to signed URL or stream file
// For now, return a placeholder PDF
const placeholderContent = `
%PDF-1.4
1 0 obj
<< /Type /Catalog /Pages 2 0 R >>
endobj
2 0 obj
<< /Type /Pages /Kids [3 0 R] /Count 1 >>
endobj
3 0 obj
<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] /Contents 4 0 R /Resources << /Font << /F1 5 0 R >> >> >>
endobj
4 0 obj
<< /Length 200 >>
stream
BT
/F1 24 Tf
100 700 Td
(Vendor Compliance Report) Tj
/F1 12 Tf
100 650 Td
(Report ID: ${reportId}) Tj
100 620 Td
(Generated: ${new Date().toISOString()}) Tj
100 580 Td
(This is a placeholder. Implement actual report generation.) Tj
ET
endstream
endobj
5 0 obj
<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>
endobj
xref
0 6
0000000000 65535 f
0000000009 00000 n
0000000058 00000 n
0000000115 00000 n
0000000266 00000 n
0000000519 00000 n
trailer
<< /Size 6 /Root 1 0 R >>
startxref
598
%%EOF
`.trim()
// Return as PDF
return new NextResponse(placeholderContent, {
headers: {
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="Report_${reportId.slice(0, 8)}.pdf"`,
},
})
}

View File

@@ -0,0 +1,44 @@
import { NextRequest, NextResponse } from 'next/server'
/**
* GET /api/sdk/v1/vendor-compliance/export/[reportId]
*
* Get report metadata by ID.
*/
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Fetch report metadata from database
// For now, return mock data
return NextResponse.json({
id: reportId,
status: 'completed',
filename: `Report_${reportId.slice(0, 8)}.pdf`,
generatedAt: new Date().toISOString(),
expiresAt: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(), // 24h
})
}
/**
* DELETE /api/sdk/v1/vendor-compliance/export/[reportId]
*
* Delete a generated report.
*/
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ reportId: string }> }
) {
const { reportId } = await params
// TODO: Delete report from storage and database
console.log('Deleting report:', reportId)
return NextResponse.json({
success: true,
deletedId: reportId,
})
}

View File

@@ -0,0 +1,118 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
/**
* POST /api/sdk/v1/vendor-compliance/export
*
* Generate and export reports in various formats.
* Currently returns mock data - integrate with actual report generation service.
*/
interface ExportConfig {
reportType: 'VVT_EXPORT' | 'VENDOR_AUDIT' | 'ROPA' | 'MANAGEMENT_SUMMARY' | 'DPIA_INPUT'
format: 'PDF' | 'DOCX' | 'XLSX' | 'JSON'
scope: {
vendorIds: string[]
processingActivityIds: string[]
includeFindings: boolean
includeControls: boolean
includeRiskAssessment: boolean
dateRange?: {
from: string
to: string
}
}
}
const REPORT_TYPE_NAMES: Record<ExportConfig['reportType'], string> = {
VVT_EXPORT: 'Verarbeitungsverzeichnis',
VENDOR_AUDIT: 'Vendor-Audit-Pack',
ROPA: 'RoPA',
MANAGEMENT_SUMMARY: 'Management-Summary',
DPIA_INPUT: 'DSFA-Input',
}
const FORMAT_EXTENSIONS: Record<ExportConfig['format'], string> = {
PDF: 'pdf',
DOCX: 'docx',
XLSX: 'xlsx',
JSON: 'json',
}
export async function POST(request: NextRequest) {
try {
const config = (await request.json()) as ExportConfig
// Validate request
if (!config.reportType || !config.format) {
return NextResponse.json(
{ error: 'reportType and format are required' },
{ status: 400 }
)
}
// Generate report ID and filename
const reportId = uuidv4()
const timestamp = new Date().toISOString().slice(0, 10).replace(/-/g, '')
const filename = `${REPORT_TYPE_NAMES[config.reportType]}_${timestamp}.${FORMAT_EXTENSIONS[config.format]}`
// TODO: Implement actual report generation
// This would typically:
// 1. Fetch data from database based on scope
// 2. Generate report using template engine (e.g., docx-templates, pdfkit)
// 3. Store in MinIO/S3
// 4. Return download URL
// Mock implementation - simulate processing time
await new Promise((resolve) => setTimeout(resolve, 500))
// In production, this would be a signed URL to MinIO/S3
const downloadUrl = `/api/sdk/v1/vendor-compliance/export/${reportId}/download`
// Log export for audit trail
console.log('Export generated:', {
reportId,
reportType: config.reportType,
format: config.format,
scope: config.scope,
filename,
generatedAt: new Date().toISOString(),
})
return NextResponse.json({
id: reportId,
reportType: config.reportType,
format: config.format,
filename,
downloadUrl,
generatedAt: new Date().toISOString(),
scope: {
vendorCount: config.scope.vendorIds?.length || 0,
activityCount: config.scope.processingActivityIds?.length || 0,
includesFindings: config.scope.includeFindings,
includesControls: config.scope.includeControls,
includesRiskAssessment: config.scope.includeRiskAssessment,
},
})
} catch (error) {
console.error('Export error:', error)
return NextResponse.json(
{ error: 'Failed to generate export' },
{ status: 500 }
)
}
}
/**
* GET /api/sdk/v1/vendor-compliance/export
*
* List recent exports for the current tenant.
*/
export async function GET() {
// TODO: Implement fetching recent exports from database
// For now, return empty list
return NextResponse.json({
exports: [],
totalCount: 0,
})
}

View File

@@ -0,0 +1,43 @@
import { NextRequest, NextResponse } from 'next/server'
import { Finding } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const findings: Map<string, Finding> = new Map()
export async function GET(request: NextRequest) {
try {
const searchParams = request.nextUrl.searchParams
const vendorId = searchParams.get('vendorId')
const contractId = searchParams.get('contractId')
const status = searchParams.get('status')
let findingsList = Array.from(findings.values())
// Filter by vendor
if (vendorId) {
findingsList = findingsList.filter((f) => f.vendorId === vendorId)
}
// Filter by contract
if (contractId) {
findingsList = findingsList.filter((f) => f.contractId === contractId)
}
// Filter by status
if (status) {
findingsList = findingsList.filter((f) => f.status === status)
}
return NextResponse.json({
success: true,
data: findingsList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching findings:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch findings' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,70 @@
import { NextRequest, NextResponse } from 'next/server'
// This would reference the same storage as the main route
// In production, this would be database calls
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// In production, fetch from database
return NextResponse.json({
success: true,
data: null, // Would return the activity
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch processing activity' },
{ status: 500 }
)
}
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
const body = await request.json()
// In production, update in database
return NextResponse.json({
success: true,
data: { id, ...body, updatedAt: new Date() },
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error updating processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to update processing activity' },
{ status: 500 }
)
}
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ id: string }> }
) {
try {
const { id } = await params
// In production, delete from database
return NextResponse.json({
success: true,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error deleting processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to delete processing activity' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,84 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { ProcessingActivity, generateVVTId } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
// In production, this would be replaced with database calls
const processingActivities: Map<string, ProcessingActivity> = new Map()
export async function GET(request: NextRequest) {
try {
const activities = Array.from(processingActivities.values())
return NextResponse.json({
success: true,
data: activities,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching processing activities:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch processing activities' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
// Generate IDs
const id = uuidv4()
const existingIds = Array.from(processingActivities.values()).map((a) => a.vvtId)
const vvtId = body.vvtId || generateVVTId(existingIds)
const activity: ProcessingActivity = {
id,
tenantId: 'default', // Would come from auth context
vvtId,
name: body.name,
responsible: body.responsible,
dpoContact: body.dpoContact,
purposes: body.purposes || [],
dataSubjectCategories: body.dataSubjectCategories || [],
personalDataCategories: body.personalDataCategories || [],
recipientCategories: body.recipientCategories || [],
thirdCountryTransfers: body.thirdCountryTransfers || [],
retentionPeriod: body.retentionPeriod || { description: { de: '', en: '' } },
technicalMeasures: body.technicalMeasures || [],
legalBasis: body.legalBasis || [],
dataSources: body.dataSources || [],
systems: body.systems || [],
dataFlows: body.dataFlows || [],
protectionLevel: body.protectionLevel || 'MEDIUM',
dpiaRequired: body.dpiaRequired || false,
dpiaJustification: body.dpiaJustification,
subProcessors: body.subProcessors || [],
legalRetentionBasis: body.legalRetentionBasis,
status: body.status || 'DRAFT',
owner: body.owner || '',
lastReviewDate: body.lastReviewDate,
nextReviewDate: body.nextReviewDate,
createdAt: new Date(),
updatedAt: new Date(),
}
processingActivities.set(id, activity)
return NextResponse.json(
{
success: true,
data: activity,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error creating processing activity:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create processing activity' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,82 @@
import { NextRequest, NextResponse } from 'next/server'
import { v4 as uuidv4 } from 'uuid'
import { Vendor } from '@/lib/sdk/vendor-compliance'
// In-memory storage for demo purposes
const vendors: Map<string, Vendor> = new Map()
export async function GET(request: NextRequest) {
try {
const vendorList = Array.from(vendors.values())
return NextResponse.json({
success: true,
data: vendorList,
timestamp: new Date().toISOString(),
})
} catch (error) {
console.error('Error fetching vendors:', error)
return NextResponse.json(
{ success: false, error: 'Failed to fetch vendors' },
{ status: 500 }
)
}
}
export async function POST(request: NextRequest) {
try {
const body = await request.json()
const id = uuidv4()
const vendor: Vendor = {
id,
tenantId: 'default',
name: body.name,
legalForm: body.legalForm,
country: body.country,
address: body.address,
website: body.website,
role: body.role,
serviceDescription: body.serviceDescription,
serviceCategory: body.serviceCategory,
dataAccessLevel: body.dataAccessLevel || 'NONE',
processingLocations: body.processingLocations || [],
transferMechanisms: body.transferMechanisms || [],
certifications: body.certifications || [],
primaryContact: body.primaryContact,
dpoContact: body.dpoContact,
securityContact: body.securityContact,
contractTypes: body.contractTypes || [],
contracts: body.contracts || [],
inherentRiskScore: body.inherentRiskScore || 50,
residualRiskScore: body.residualRiskScore || 50,
manualRiskAdjustment: body.manualRiskAdjustment,
riskJustification: body.riskJustification,
reviewFrequency: body.reviewFrequency || 'ANNUAL',
lastReviewDate: body.lastReviewDate,
nextReviewDate: body.nextReviewDate,
status: body.status || 'ACTIVE',
processingActivityIds: body.processingActivityIds || [],
notes: body.notes,
createdAt: new Date(),
updatedAt: new Date(),
}
vendors.set(id, vendor)
return NextResponse.json(
{
success: true,
data: vendor,
timestamp: new Date().toISOString(),
},
{ status: 201 }
)
} catch (error) {
console.error('Error creating vendor:', error)
return NextResponse.json(
{ success: false, error: 'Failed to create vendor' },
{ status: 500 }
)
}
}

View File

@@ -0,0 +1,135 @@
/**
* Vendor Compliance API Proxy - Catch-all route
* Proxies all /api/sdk/v1/vendors/* requests to ai-compliance-sdk backend
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/vendors`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {
'Content-Type': 'application/json',
}
// Forward all relevant headers
const headerNames = ['authorization', 'x-tenant-id', 'x-user-id', 'x-namespace-id', 'x-tenant-slug']
for (const name of headerNames) {
const value = request.headers.get(name)
if (value) {
headers[name] = value
}
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(30000),
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
const contentType = request.headers.get('content-type')
if (contentType?.includes('application/json')) {
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body - continue without
}
}
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (e.g., PDF exports)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Vendor Compliance API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,147 @@
/**
* Whistleblower API Proxy - Catch-all route
* Proxies all /api/sdk/v1/whistleblower/* requests to ai-compliance-sdk backend
* Supports multipart/form-data for file uploads
*/
import { NextRequest, NextResponse } from 'next/server'
const SDK_BACKEND_URL = process.env.SDK_API_URL || 'http://ai-compliance-sdk:8090'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${SDK_BACKEND_URL}/sdk/v1/whistleblower`
const url = pathStr
? `${basePath}/${pathStr}${searchParams ? `?${searchParams}` : ''}`
: `${basePath}${searchParams ? `?${searchParams}` : ''}`
try {
const headers: HeadersInit = {}
const contentType = request.headers.get('content-type')
// Forward auth headers
const authHeader = request.headers.get('authorization')
if (authHeader) {
headers['Authorization'] = authHeader
}
const tenantHeader = request.headers.get('x-tenant-id')
if (tenantHeader) {
headers['X-Tenant-Id'] = tenantHeader
}
const fetchOptions: RequestInit = {
method,
headers,
signal: AbortSignal.timeout(60000), // 60s for file uploads
}
if (['POST', 'PUT', 'PATCH'].includes(method)) {
if (contentType?.includes('multipart/form-data')) {
// Forward multipart form data (file uploads)
const formData = await request.formData()
fetchOptions.body = formData
// Don't set Content-Type - let fetch set it with boundary
} else if (contentType?.includes('application/json')) {
headers['Content-Type'] = 'application/json'
try {
const text = await request.text()
if (text && text.trim()) {
fetchOptions.body = text
}
} catch {
// Empty or invalid body
}
} else {
headers['Content-Type'] = 'application/json'
}
} else {
headers['Content-Type'] = 'application/json'
}
const response = await fetch(url, fetchOptions)
// Handle non-JSON responses (e.g., PDF exports, file downloads)
const responseContentType = response.headers.get('content-type')
if (responseContentType?.includes('application/pdf') ||
responseContentType?.includes('application/octet-stream') ||
responseContentType?.includes('image/')) {
const blob = await response.blob()
return new NextResponse(blob, {
status: response.status,
headers: {
'Content-Type': responseContentType,
'Content-Disposition': response.headers.get('content-disposition') || '',
},
})
}
if (!response.ok) {
const errorText = await response.text()
let errorJson
try {
errorJson = JSON.parse(errorText)
} catch {
errorJson = { error: errorText }
}
return NextResponse.json(
{ error: `Backend Error: ${response.status}`, ...errorJson },
{ status: response.status }
)
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Whistleblower API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum SDK Backend fehlgeschlagen' },
{ status: 503 }
)
}
}
export async function GET(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'GET')
}
export async function POST(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'POST')
}
export async function PUT(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PUT')
}
export async function PATCH(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'PATCH')
}
export async function DELETE(
request: NextRequest,
{ params }: { params: Promise<{ path?: string[] }> }
) {
const { path } = await params
return proxyRequest(request, path, 'DELETE')
}

View File

@@ -0,0 +1,456 @@
# Wie funktioniert das Klausur-Namespace-System?
Eine umfassende Erklaerung des BYOEH-Systems -- von der Anonymisierung bis zur sicheren KI-Korrektur.
---
## 1. Was ist das Namespace-System?
Das **BYOEH-System** (Bring Your Own Expectation Horizon) ist eine Datenschutz-Architektur, die es Lehrern ermoeglicht, Klausuren **anonym und verschluesselt** von einer KI korrigieren zu lassen -- ohne dass jemals der Name eines Schuelers den Rechner des Lehrers verlaesst.
> *"Die Klausuren gehen anonym in die Cloud, werden dort von KI korrigiert, und kommen korrigiert zurueck. Nur der Lehrer kann die Ergebnisse wieder den Schuelern zuordnen -- denn nur seine Hardware hat den Schluessel dafuer."*
Das System loest ein grundlegendes Problem: Klausurkorrektur mit KI-Unterstuetzung **ohne Datenschutzrisiko**. Die Loesung besteht aus vier Bausteinen:
1. **Pseudonymisierung:** Namen werden durch zufaellige Codes ersetzt. Niemand ausser dem Lehrer kennt die Zuordnung.
2. **Verschluesselung:** Alles wird *im Browser des Lehrers* verschluesselt, bevor es den Rechner verlaesst. Der Server sieht nur unlesbaren Datensalat.
3. **Namespace-Isolation:** Jeder Lehrer hat einen eigenen, abgeschotteten Bereich (Namespace). Kein Lehrer kann auf die Daten eines anderen zugreifen.
4. **KI-Korrektur:** Die KI arbeitet mit den verschluesselten Daten und dem Erwartungshorizont (EH) des Lehrers. Korrekturvorschlaege gehen zurueck an den Lehrer.
!!! info "Kern-Designprinzip: Operator Blindness"
**Breakpilot kann die Klausuren nicht lesen.** Der Server sieht nur verschluesselte Daten und einen Schluessel-Hash (nicht den Schluessel selbst). Die Passphrase zum Entschluesseln existiert *nur* im Browser des Lehrers und wird niemals uebertragen. Selbst ein Angriff auf den Server wuerde keine Klausurtexte preisgeben.
---
## 2. Der komplette Ablauf im Ueberblick
Der Prozess laesst sich in sieben Schritte unterteilen. Stellen Sie sich vor, der Lehrer sitzt an seinem Rechner und hat einen Stapel gescannter Klausuren:
```text title="Der komplette Workflow: Von der Klausur zur KI-Korrektur"
SCHRITT 1: KLAUSUREN SCANNEN & HOCHLADEN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lehrer scannt Klausuren ein (PDF oder Bild)
→ System erkennt automatisch den Kopfbereich mit Namen
→ Kopfbereich wird permanent entfernt (Header-Redaction)
→ Jede Klausur erhaelt einen zufaelligen Code (doc_token)
SCHRITT 2: VERSCHLUESSELUNG IM BROWSER
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lehrer gibt eine Passphrase ein (z.B. "MeinGeheimesPasswort2025!")
→ Browser leitet daraus einen 256-Bit-Schluessel ab (PBKDF2)
→ Klausur wird mit AES-256-GCM verschluesselt
→ Nur der Hash des Schluessels wird an den Server gesendet
→ Passphrase und Schluessel verlassen NIEMALS den Browser
SCHRITT 3: IDENTITAETS-MAP SICHERN
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Die Zuordnung "doc_token → Schuelername" wird verschluesselt:
→ Tabelle: "a7f3c2d1... = Max Mustermann, b9e4a1f8... = Anna Schmidt"
→ Diese Tabelle wird mit dem gleichen Schluessel verschluesselt
→ Ohne Passphrase kann niemand die Zuordnung wiederherstellen
SCHRITT 4: UPLOAD IN DEN NAMESPACE
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Die verschluesselten Dateien gehen in den persoenlichen Namespace:
→ Jeder Lehrer hat eine eigene tenant_id
→ Daten werden in MinIO (verschluesselt) + Qdrant (Vektoren) gespeichert
→ Server sieht: verschluesselter Blob + Schluessel-Hash + Salt
SCHRITT 5: KI-KORREKTUR
━━━━━━━━━━━━━━━━━━━━━━━
Der Lehrer startet die KI-Korrektur:
→ RAG-System durchsucht den Erwartungshorizont (EH)
→ KI generiert Korrekturvorschlaege pro Kriterium
→ Vorschlaege basieren auf dem EH, nicht auf Halluzinationen
SCHRITT 6: ERGEBNISSE ZURUECK
━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Korrekturvorschlaege gehen an den Lehrer:
→ Lehrer gibt Passphrase ein
→ Browser entschluesselt die Ergebnisse
→ Lehrer sieht: Vorschlaege pro Kriterium + Gesamtnote
SCHRITT 7: ZUORDNUNG & FINALISIERUNG
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Lehrer ordnet Ergebnisse den Schuelern zu:
→ Identitaets-Map wird entschluesselt
→ doc_token wird wieder dem echten Namen zugeordnet
→ Lehrer ueberprueft/korrigiert KI-Vorschlaege
→ Fertige Korrektur + Gutachten koennen exportiert werden
```
---
## 3. Pseudonymisierung: Wie Namen verschwinden
Pseudonymisierung bedeutet: personenbezogene Daten werden durch **zufaellige Codes** ersetzt, sodass ohne Zusatzinformation kein Rueckschluss auf die Person moeglich ist. Im BYOEH-System passiert das auf zwei Ebenen:
### 3.1 Der doc_token: Ein zufaelliger Ausweis
Jede Klausur erhaelt einen **doc_token** -- einen 128-Bit-Zufallscode im UUID4-Format (z.B. `a7f3c2d1-4e9b-4a5f-8c7d-6b2e1f0a9d3c`). Dieser Code:
- Ist **kryptographisch zufaellig** -- es gibt keinen Zusammenhang zwischen Token und Schueler
- Kann **nicht zurueckgerechnet** werden -- auch mit Kenntnis des Algorithmus ist kein Rueckschluss moeglich
- Wird **auf der Klausur aufgedruckt** (als QR-Code), damit die physische Klausur spaeter wieder zugeordnet werden kann
### 3.2 Header-Redaction: Der Name wird entfernt
Bevor eine Klausur verarbeitet wird, entfernt das System den **Kopfbereich** der gescannten Seite -- dort, wo typischerweise Name, Klasse und Datum stehen. Diese Entfernung ist **permanent**: Die Originaldaten werden nicht gespeichert.
| Methode | Wie es funktioniert | Wann verwendet |
|---------|---------------------|----------------|
| **Einfache Redaction** | Obere ~2,5 cm der Seite werden weiss ueberschrieben | Standard bei allen Uploads |
| **Smarte Redaction** | OpenCV erkennt Textbereiche und entfernt gezielt den Kopf, verschont aber QR-Codes | Wenn QR-Codes auf der Klausur sind |
### 3.3 Die Identitaets-Map: Nur der Lehrer kennt die Zuordnung
Die Zuordnung *doc_token → Schuelername* wird als **verschluesselte Tabelle** gespeichert:
```text title="Datenbank: ExamSession (vereinfacht)"
ExamSession
├── teacher_id = "lehrer-uuid-123" ← Pflichtfeld (Isolation)
├── encrypted_identity_map = [verschluesselte Bytes] ← Nur mit Passphrase lesbar
├── identity_map_iv = "a3f2c1..." ← Initialisierungsvektor (fuer AES)
└── PseudonymizedDocument (pro Klausur)
├── doc_token = "a7f3c2d1-..." ← Zufaelliger Code (Primary Key)
├── exam_session_id = [Referenz]
└── (Kein Name, keine Klasse, kein persoenliches Datum)
```
!!! success "DSGVO Art. 4 Nr. 5 konform"
Die Pseudonymisierung erfuellt die Definition aus der DSGVO: Die personenbezogenen Daten (Schuelernamen) koennen **ohne Hinzuziehung zusaetzlicher Informationen** (der verschluesselten Identitaets-Map + der Passphrase des Lehrers) nicht mehr einer bestimmten Person zugeordnet werden.
---
## 4. Verschluesselung: Wie Daten geschuetzt werden
Die Verschluesselung ist das Herzstueck des Datenschutzes. Sie findet **vollstaendig im Browser** statt -- der Server bekommt nur verschluesselte Daten zu sehen.
### 4.1 Der Verschluesselungsvorgang
Wenn der Lehrer eine Klausur oder einen Erwartungshorizont hochlaedt, passiert im Browser folgendes:
```text title="Client-seitige Verschluesselung (im Browser)"
┌─────────────────────────────────────────────────────────────────┐
│ Browser des Lehrers │
├─────────────────────────────────────────────────────────────────┤
│ │
│ 1. Lehrer gibt Passphrase ein (z.B. "MeinGeheimesPasswort!") │
│ │ ↑ │
│ │ │ Passphrase bleibt hier -- wird NIE gesendet │
│ ▼ │
│ 2. Schluessel-Ableitung: │
│ PBKDF2-SHA256(Passphrase, zufaelliger Salt, 100.000 Runden) │
│ │ │
│ │ → Ergebnis: 256-Bit-Schluessel (32 Bytes) │
│ │ → Selbst bei Kenntnis des Salts sind 100.000 Runden │
│ │ noetig, um den Schluessel zu erraten │
│ ▼ │
│ 3. Verschluesselung: │
│ AES-256-GCM(Schluessel, zufaelliger IV, Datei-Inhalt) │
│ │ │
│ │ → AES-256: Militaerstandard, 2^256 moegliche Schluessel │
│ │ → GCM: Garantiert Integritaet (Manipulation erkennbar) │
│ ▼ │
│ 4. Schluessel-Hash: │
│ SHA-256(abgeleiteter Schluessel) → Hash fuer Verifikation │
│ │ │
│ │ → Der Server speichert nur diesen Hash │
│ │ → Damit kann geprueft werden ob die Passphrase stimmt │
│ │ → Vom Hash kann der Schluessel NICHT zurueckgerechnet │
│ │ werden │
│ ▼ │
│ 5. Upload: Nur diese Daten gehen an den Server: │
│ • Verschluesselter Blob (unlesbar ohne Schluessel) │
│ • Salt (zufaellige Bytes, harmlos) │
│ • IV (Initialisierungsvektor, harmlos) │
│ • Schluessel-Hash (zur Verifikation, nicht umkehrbar) │
│ │
│ Was NICHT an den Server geht: │
│ ✗ Passphrase │
│ ✗ Abgeleiteter Schluessel │
│ ✗ Unverschluesselter Klartext │
└─────────────────────────────────────────────────────────────────┘
```
### 4.2 Warum ist das sicher?
| Angriffsszenario | Was der Angreifer sieht | Ergebnis |
|------------------|-------------------------|----------|
| **Server wird gehackt** | Verschluesselte Blobs + Hashes | Keine lesbaren Klausuren |
| **Datenbank wird geleakt** | encrypted_identity_map (verschluesselt) | Keine Schuelernamen |
| **Netzwerkverkehr abgefangen** | Verschluesselte Daten (HTTPS + AES) | Doppelt verschluesselt |
| **Betreiber will mitlesen** | Verschluesselte Blobs, kein Schluessel | Operator Blindness |
| **Anderer Lehrer versucht Zugriff** | Nichts (Tenant-Isolation) | Namespace blockiert |
---
## 5. Namespace-Isolation: Jeder Lehrer hat seinen eigenen Bereich
Ein **Namespace** (auch "Tenant" genannt) ist ein abgeschotteter Bereich im System. Man kann es sich wie **separate Schliessfaecher** in einer Bank vorstellen: Jeder Lehrer hat sein eigenes Fach, und kein Schluessel passt in ein anderes Fach.
### 5.1 Wie die Isolation funktioniert
Jeder Lehrer erhaelt beim ersten Login eine eindeutige `tenant_id`. Diese ID wird bei **jeder einzelnen Datenbankabfrage** als Pflichtfilter mitgefuehrt:
```text title="Tenant-Isolation in der Vektordatenbank (Qdrant)"
Lehrer A (tenant_id: "school-A-lehrer-1")
├── Klausur 1 (verschluesselt)
├── Klausur 2 (verschluesselt)
└── Erwartungshorizont Deutsch LK 2025
Lehrer B (tenant_id: "school-B-lehrer-2")
├── Klausur 1 (verschluesselt)
└── Erwartungshorizont Mathe GK 2025
Suchanfrage von Lehrer A:
"Wie soll die Einleitung strukturiert sein?"
→ Suche NUR in tenant_id = "school-A-lehrer-1"
→ Lehrer B's Daten sind UNSICHTBAR
Es gibt KEINE Abfrage ohne tenant_id-Filter.
```
### 5.2 Drei Ebenen der Isolation
| Ebene | System | Isolation |
|-------|--------|-----------|
| **Dateisystem** | MinIO (S3-Storage) | Eigener Ordner pro Lehrer: `/tenant-id/eh-id/encrypted.bin` |
| **Vektordatenbank** | Qdrant | Pflichtfilter `tenant_id` bei jeder Suche |
| **Metadaten-DB** | PostgreSQL | Jede Tabelle hat `teacher_id` als Pflichtfeld |
!!! warning "Kein Training mit Lehrerdaten"
Auf allen Vektoren in Qdrant ist das Flag `training_allowed: false` gesetzt. Das bedeutet: Die Inhalte der Lehrer werden **ausschliesslich fuer RAG-Suchen** (Abruf relevanter Textpassagen) verwendet und **niemals zum Trainieren** eines KI-Modells eingesetzt.
---
## 6. Der Erwartungshorizont: Die Grundlage fuer KI-Korrektur
Ein **Erwartungshorizont** (EH) ist das Dokument, das beschreibt, was in einer Klausur erwartet wird: welche Inhalte in welcher Qualitaet vorkommen sollen. Im BYOEH-System laedt der Lehrer seinen eigenen EH hoch, und die KI nutzt ihn als Referenz fuer Korrekturvorschlaege.
### 6.1 Upload-Wizard (5 Schritte)
| Schritt | Was passiert | Warum |
|---------|--------------|-------|
| **1. Datei waehlen** | PDF per Drag & Drop hochladen | Der EH als digitales Dokument |
| **2. Metadaten** | Titel, Fach, Niveau (eA/gA), Jahr | Fuer Filterung und Organisation |
| **3. Rechtebestaetigung** | Checkbox: "Ich bin berechtigt" | Rechtliche Absicherung (Urheberrecht) |
| **4. Verschluesselung** | Passphrase eingeben (2x bestaetigen) | Schluessel fuer Ende-zu-Ende-Verschluesselung |
| **5. Zusammenfassung** | Pruefen und bestaetigen | Letzte Kontrolle vor dem Upload |
### 6.2 RAG-Pipeline: Wie der EH fuer die KI nutzbar wird
Nach dem Upload wird der EH fuer die KI-Suche vorbereitet. Dieser Vorgang heisst **Indexierung** und funktioniert wie das Erstellen eines Stichwortverzeichnisses fuer ein Buch:
```text title="Indexierung: Vom PDF zum durchsuchbaren EH"
Erwartungshorizont (verschluesselt auf Server)
|
v
┌────────────────────────────────┐
│ 1. Passphrase-Verifikation │ ← Lehrer gibt Passphrase ein
│ Hash pruefen │ Server vergleicht mit gespeichertem Hash
└──────────┬─────────────────────┘
|
v
┌────────────────────────────────┐
│ 2. Entschluesselung │ ← Temporaer im Arbeitsspeicher
│ AES-256-GCM Decrypt │ (wird nach Verarbeitung geloescht)
└──────────┬─────────────────────┘
|
v
┌────────────────────────────────┐
│ 3. Text-Extraktion │ ← PDF → Klartext
│ Tabellen, Listen erkennen │
└──────────┬─────────────────────┘
|
v
┌────────────────────────────────┐
│ 4. Chunking │ ← Text in 1.000-Zeichen-Abschnitte zerlegen
│ Ueberlappung: 200 Zeichen │ (mit Ueberlappung fuer Kontext)
└──────────┬─────────────────────┘
|
v
┌────────────────────────────────┐
│ 5. Embedding │ ← Jeder Abschnitt wird in einen
│ Text → 1.536 Zahlen │ Bedeutungsvektor umgewandelt
└──────────┬─────────────────────┘
|
v
┌────────────────────────────────┐
│ 6. Re-Encryption │ ← Jeder Chunk wird ERNEUT verschluesselt
│ AES-256-GCM pro Chunk │ bevor er gespeichert wird
└──────────┬─────────────────────┘
|
v
┌────────────────────────────────┐
│ 7. Qdrant-Indexierung │ ← Vektor + verschluesselter Chunk
│ tenant_id: "lehrer-123" │ werden mit Tenant-Filter gespeichert
│ training_allowed: false │
└────────────────────────────────┘
```
### 6.3 Wie die KI den EH nutzt (RAG-Query)
Wenn der Lehrer bei der Korrektur einen Vorschlag anfordert, passiert folgendes:
1. **Frage formulieren:** Das System erstellt eine Suchanfrage aus dem Klausurtext und dem aktuellen Bewertungskriterium.
2. **Semantische Suche:** Die Anfrage wird in einen Vektor umgewandelt und gegen die EH-Vektoren in Qdrant gesucht -- *nur im Namespace des Lehrers*.
3. **Entschluesselung:** Die gefundenen Chunks werden mit der Passphrase des Lehrers entschluesselt.
4. **KI-Antwort:** Die entschluesselten EH-Passagen werden als Kontext an die KI uebergeben, die daraus einen Korrekturvorschlag generiert.
---
## 7. Key Sharing: Zweitkorrektur ermoeglichen
Bei Abiturklausuren muss eine **Zweitkorrektur** durch einen anderen Lehrer erfolgen. Das Key-Sharing-System ermoeglicht es dem Erstpruefer, seinen Erwartungshorizont sicher mit dem Zweitpruefer zu teilen -- ohne die Verschluesselung aufzugeben.
### 7.1 Einladungs-Workflow
```text title="Key Sharing: Sicheres Teilen zwischen Pruefern"
Erstpruefer Server Zweitpruefer
│ │ │
│ 1. Einladung senden │ │
│ (E-Mail + Rolle + Klausur) │ │
│─────────────────────────────────▶ │
│ │ │
│ │ 2. Einladung erstellt │
│ │ (14 Tage gueltig) │
│ │ │
│ │ 3. Benachrichtigung ──────▶│
│ │ │
│ │ 4. Annehmen
│ │◀─────────────────────────────│
│ │ │
│ │ 5. Key-Share erstellt │
│ │ │
│ │ 6. Zweitpruefer kann ──────▶│
│ │ RAG-Queries ausfuehren │
│ │ │
│ 7. Zugriff widerrufen │ │
│ (jederzeit moeglich) │ │
│─────────────────────────────────▶ │
```
### 7.2 Rollen beim Key-Sharing
| Rolle | Wer | Rechte |
|-------|-----|--------|
| **Erstpruefer (EK)** | Kurslehrer | Vollzugriff, kann teilen & widerrufen |
| **Zweitpruefer (ZK)** | Anderer Fachlehrer | Nur Lesen, RAG-Queries, eigene Annotations |
| **Drittpruefer (DK)** | Bei Differenz ≥ 4 Punkte | Nur Lesen, RAG-Queries |
| **Fachvorsitz** | Fachbereichsleitung | Nur Lesen (Aufsichtsfunktion) |
---
## 8. KI-gestuetzte Bewertung: Wie die Korrektur funktioniert
Die KI bewertet jede Klausur anhand von **fuenf Kriterien**, die zusammen 100% ergeben:
| Kriterium | Gewichtung | Was geprueft wird |
|-----------|------------|-------------------|
| Rechtschreibung | 15% | Orthographie, Zeichensetzung |
| Grammatik | 15% | Satzbau, Kongruenz, Tempus |
| **Inhalt** | **40%** | Bezug zum EH, Vollstaendigkeit, Argumentation |
| Struktur | 15% | Gliederung, Einleitung/Schluss, roter Faden |
| Stil | 15% | Ausdruck, Wortwahl, Fachsprache |
Die Bewertung folgt dem **15-Punkte-System** (0-15 Notenpunkte) der gymnasialen Oberstufe.
!!! info "KI schlaegt vor, Lehrer entscheidet"
Alle KI-Bewertungen sind **Vorschlaege**. Der Lehrer hat bei jedem Kriterium die volle Kontrolle: Er kann den Vorschlag annehmen, aendern oder komplett ueberschreiben. Die finale Note setzt immer der Lehrer.
---
## 9. Audit-Trail: Alles wird protokolliert
Jede Aktion im System wird revisionssicher im **Audit-Log** gespeichert. Das ist wichtig fuer die Nachvollziehbarkeit und fuer den Fall, dass Schueler oder Eltern eine Korrektur anfechten.
| Aktion | Was protokolliert wird |
|--------|------------------------|
| `upload` | EH hochgeladen (Dateigroesse, Metadaten, Zeitstempel) |
| `index` | EH fuer RAG indexiert (Anzahl Chunks, Dauer) |
| `rag_query` | RAG-Suchanfrage ausgefuehrt (Query-Hash, Anzahl Ergebnisse, Score) |
| `share` | EH mit anderem Pruefer geteilt (Empfaenger, Rolle) |
| `revoke_share` | Zugriff widerrufen (wer, wann) |
| `link_klausur` | EH mit Klausur verknuepft |
| `delete` | EH geloescht (Soft Delete, bleibt in Logs) |
---
## 10. API-Endpunkte (Technische Referenz)
Alle Endpunkte laufen ueber den **klausur-service** auf Port 8086.
### 10.1 Erwartungshorizont-Verwaltung
| Methode | Endpunkt | Beschreibung |
|---------|----------|--------------|
| `POST` | `/api/v1/eh/upload` | Verschluesselten EH hochladen |
| `GET` | `/api/v1/eh` | Eigene EHs auflisten |
| `GET` | `/api/v1/eh/{id}` | Einzelnen EH abrufen |
| `DELETE` | `/api/v1/eh/{id}` | EH loeschen (Soft Delete) |
| `POST` | `/api/v1/eh/{id}/index` | EH fuer RAG indexieren |
| `POST` | `/api/v1/eh/rag-query` | RAG-Suchanfrage ausfuehren |
### 10.2 Key Sharing
| Methode | Endpunkt | Beschreibung |
|---------|----------|--------------|
| `POST` | `/api/v1/eh/{id}/share` | EH mit Pruefer teilen |
| `GET` | `/api/v1/eh/{id}/shares` | Geteilte Zugriffe auflisten |
| `DELETE` | `/api/v1/eh/{id}/shares/{shareId}` | Zugriff widerrufen |
| `GET` | `/api/v1/eh/shared-with-me` | Mit mir geteilte EHs |
### 10.3 Klausur-Integration
| Methode | Endpunkt | Beschreibung |
|---------|----------|--------------|
| `POST` | `/api/v1/eh/{id}/link-klausur` | EH mit Klausur verknuepfen |
| `DELETE` | `/api/v1/eh/{id}/link-klausur/{klausurId}` | Verknuepfung loesen |
| `GET` | `/api/v1/klausuren/{id}/linked-eh` | Verknuepften EH abrufen |
---
## 11. Dateistruktur im Code
```text title="Relevante Dateien im Repository"
klausur-service/
├── backend/
│ ├── main.py # API-Endpunkte + Datenmodelle
│ ├── qdrant_service.py # Vektordatenbank-Operationen (Tenant-Filter)
│ └── eh_pipeline.py # Chunking, Embedding, Verschluesselung
├── frontend/
│ └── src/
│ ├── components/
│ │ └── EHUploadWizard.tsx # 5-Schritt-Upload-Wizard
│ └── services/
│ ├── api.ts # API-Client
│ └── encryption.ts # Client-seitige Kryptographie
└── docs/
├── BYOEH-Architecture.md # Technische Architektur
└── BYOEH-Developer-Guide.md # Entwickler-Handbuch
backend/klausur/
├── db_models.py # ExamSession, PseudonymizedDocument
└── services/
└── pseudonymizer.py # QR-Codes, Header-Redaction, doc_tokens
```
---
## 12. Zusammenfassung: Die Sicherheitsgarantien
| Garantie | Wie umgesetzt | Regelwerk |
|----------|---------------|-----------|
| **Kein Name verlaesst den Rechner** | Header-Redaction + verschluesselte Identity-Map | DSGVO Art. 4 Nr. 5 |
| **Betreiber kann nicht mitlesen** | Client-seitige AES-256-GCM Verschluesselung | DSGVO Art. 32 |
| **Kein Zugriff durch andere Lehrer** | Tenant-Isolation (Namespace) auf allen 3 Ebenen | DSGVO Art. 25 |
| **Kein KI-Training mit Schuelerdaten** | `training_allowed: false` auf allen Vektoren | AI Act Art. 10 |
| **Alles nachvollziehbar** | Vollstaendiger Audit-Trail aller Aktionen | DSGVO Art. 5 Abs. 2 |
| **Lehrer behaelt volle Kontrolle** | KI-Vorschlaege, keine KI-Entscheidungen + jederzeitiger Widerruf | DSGVO Art. 22 |
!!! success "Das Wichtigste in einem Satz"
Das BYOEH-System ermoeglicht KI-gestuetzte Klausurkorrektur, bei der **kein Schuelername den Rechner des Lehrers verlaesst**, alle Daten **Ende-zu-Ende verschluesselt** sind, jeder Lehrer seinen **eigenen abgeschotteten Namespace** hat, und die KI **nur Vorschlaege macht** -- die finale Bewertung trifft immer der Lehrer.

View File

@@ -61,6 +61,7 @@ nav:
- Architektur: services/ki-daten-pipeline/architecture.md
- Klausur-Service:
- Uebersicht: services/klausur-service/index.md
- BYOEH Systemerklaerung: services/klausur-service/byoeh-system-erklaerung.md
- BYOEH Architektur: services/klausur-service/BYOEH-Architecture.md
- BYOEH Developer Guide: services/klausur-service/BYOEH-Developer-Guide.md
- NiBiS Pipeline: services/klausur-service/NiBiS-Ingestion-Pipeline.md

View File

@@ -0,0 +1,741 @@
import { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = {
title: 'EU AI Act Ueberblick - Was Unternehmen wissen muessen | BreakPilot Comply',
description: 'Der EU AI Act reguliert KI-Systeme nach Risikostufen. Erfahren Sie, welche Pflichten fuer Ihr Unternehmen gelten.',
}
export default function AIActUeberblickPage() {
return (
<div className="min-h-screen bg-white">
{/* Hero Section */}
<section className="pt-12 pb-12 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-primary-50 to-white">
<div className="max-w-4xl mx-auto text-center">
<div className="inline-flex items-center gap-2 px-4 py-2 bg-primary-100 text-primary-700 rounded-full text-sm font-medium mb-6">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
EU AI Act - Verordnung (EU) 2024/1689
</div>
<h1 className="text-4xl sm:text-5xl font-bold text-slate-900 tracking-tight">
EU AI Act: Der umfassende Ueberblick fuer Unternehmen
</h1>
<p className="mt-6 text-xl text-slate-600 max-w-2xl mx-auto">
Die weltweit erste umfassende KI-Regulierung ist in Kraft. Erfahren Sie, welche Pflichten fuer Ihr Unternehmen gelten und wie Sie sich vorbereiten koennen.
</p>
<div className="mt-6 flex items-center justify-center gap-4 text-sm text-slate-500">
<span>Aktualisiert: Februar 2025</span>
<span className="w-1 h-1 bg-slate-300 rounded-full" />
<span>Lesezeit: ca. 12 Minuten</span>
</div>
</div>
</section>
{/* Article Content */}
<article className="py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
{/* Table of Contents */}
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6 mb-12">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Inhaltsverzeichnis</h2>
<nav className="space-y-2 text-sm">
<a href="#was-ist-der-ai-act" className="block text-primary-600 hover:text-primary-700 transition-colors">1. Was ist der EU AI Act?</a>
<a href="#risikokategorien" className="block text-primary-600 hover:text-primary-700 transition-colors">2. Die vier Risikokategorien</a>
<a href="#zeitplan" className="block text-primary-600 hover:text-primary-700 transition-colors">3. Zeitplan der Anwendung</a>
<a href="#pflichten-anbieter" className="block text-primary-600 hover:text-primary-700 transition-colors">4. Pflichten fuer Anbieter von Hochrisiko-KI</a>
<a href="#pflichten-nutzer" className="block text-primary-600 hover:text-primary-700 transition-colors">5. Pflichten fuer Nutzer von KI-Systemen</a>
<a href="#strafen" className="block text-primary-600 hover:text-primary-700 transition-colors">6. Strafen bei Verstoss</a>
<a href="#checkliste" className="block text-primary-600 hover:text-primary-700 transition-colors">7. Checkliste: Ist Ihr Unternehmen betroffen?</a>
<a href="#breakpilot-comply" className="block text-primary-600 hover:text-primary-700 transition-colors">8. Wie BreakPilot Comply hilft</a>
</nav>
</div>
{/* Section 1: Was ist der EU AI Act? */}
<section id="was-ist-der-ai-act">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
1. Was ist der EU AI Act?
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Der EU AI Act (Verordnung (EU) 2024/1689) ist die weltweit erste umfassende Regulierung fuer Kuenstliche Intelligenz. Am 1. August 2024 ist die Verordnung offiziell in Kraft getreten und wird stufenweise bis 2027 vollstaendig anwendbar. Ziel der Verordnung ist es, einen einheitlichen Rechtsrahmen fuer die Entwicklung, den Vertrieb und den Einsatz von KI-Systemen innerhalb der Europaeischen Union zu schaffen.
</p>
<p className="text-gray-600 leading-relaxed mb-4">
Der regulatorische Ansatz folgt dem Prinzip der Risikobasierung: Je hoeher das Risiko eines KI-Systems fuer Grundrechte, Sicherheit oder demokratische Prozesse, desto strenger sind die Anforderungen. Damit unterscheidet sich der EU AI Act grundlegend von sektoralen Regulierungsansaetzen und schafft stattdessen einen horizontalen, technologieneutralen Rahmen, der fuer alle Branchen gilt.
</p>
<p className="text-gray-600 leading-relaxed mb-4">
Fuer Unternehmen bedeutet dies: Wer KI-Systeme entwickelt, vertreibt oder einsetzt, muss pruefen, in welche Risikokategorie die eigenen Systeme fallen, und die entsprechenden Pflichten erfuellen. Die Verordnung gilt nicht nur fuer europaeische Unternehmen, sondern fuer jeden Anbieter, dessen KI-Systeme auf dem EU-Markt eingesetzt werden -- unabhaengig vom Firmensitz.
</p>
<div className="bg-primary-50 border border-primary-200 rounded-xl p-6 my-8">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-primary-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="font-semibold text-primary-900 mb-1">Gut zu wissen</p>
<p className="text-primary-800 text-sm">
Der EU AI Act definiert ein &quot;KI-System&quot; als ein maschinengestuetztes System, das mit unterschiedlichem Grad an Autonomie operiert, sich nach der Bereitstellung anpassen kann und aus den erhaltenen Eingaben Ergebnisse wie Vorhersagen, Inhalte, Empfehlungen oder Entscheidungen ableitet, die physische oder virtuelle Umgebungen beeinflussen koennen. Diese Definition ist bewusst breit gefasst und umfasst sowohl klassisches Machine Learning als auch generative KI und Large Language Models.
</p>
</div>
</div>
</div>
</section>
{/* Section 2: Die vier Risikokategorien */}
<section id="risikokategorien">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
2. Die vier Risikokategorien
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Das Herzstrueck des EU AI Act ist das vierstufige Risikoklassifizierungssystem. Jedes KI-System wird anhand seines Anwendungszwecks und der potenziellen Auswirkungen einer der vier Kategorien zugeordnet. Die Einstufung bestimmt, welche regulatorischen Anforderungen erfuellt werden muessen.
</p>
<div className="space-y-4 mt-6">
{/* Unakzeptables Risiko */}
<div className="border-l-4 border-red-500 bg-red-50 rounded-r-xl p-6">
<div className="flex items-center gap-3 mb-2">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-red-100 text-red-700 font-bold text-sm">!</span>
<h3 className="text-xl font-semibold text-gray-800">Unakzeptables Risiko -- Verboten</h3>
</div>
<p className="text-gray-600 text-sm mb-3">
KI-Systeme in dieser Kategorie werden als direkte Bedrohung fuer Grundrechte und demokratische Werte angesehen und sind in der EU vollstaendig verboten. Es gibt keinen Compliance-Pfad -- diese Systeme duerfen weder entwickelt noch eingesetzt werden.
</p>
<div className="mt-3">
<p className="text-sm font-medium text-red-800 mb-2">Beispiele verbotener Praktiken:</p>
<ul className="text-sm text-gray-600 space-y-1">
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">&#x2716;</span>
<span>Social Scoring durch Behoerden (Bewertung des Sozialverhaltens von Buergern)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">&#x2716;</span>
<span>Biometrische Echtzeit-Fernidentifikation im oeffentlichen Raum (mit engen Ausnahmen fuer Strafverfolgung)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">&#x2716;</span>
<span>Emotionserkennung am Arbeitsplatz und in Bildungseinrichtungen</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">&#x2716;</span>
<span>Manipulative KI-Techniken, die Schwaechen von Personen ausnutzen (Alter, Behinderung, wirtschaftliche Lage)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-red-500 mt-1">&#x2716;</span>
<span>Ungezielte Gesichtsbild-Datenbanken durch Scraping von Internet- oder Ueberwachungsmaterial</span>
</li>
</ul>
</div>
</div>
{/* Hochrisiko */}
<div className="border-l-4 border-orange-500 bg-orange-50 rounded-r-xl p-6">
<div className="flex items-center gap-3 mb-2">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-orange-100 text-orange-700 font-bold text-sm">H</span>
<h3 className="text-xl font-semibold text-gray-800">Hochrisiko -- Strenge Auflagen</h3>
</div>
<p className="text-gray-600 text-sm mb-3">
Hochrisiko-KI-Systeme sind erlaubt, unterliegen aber umfangreichen regulatorischen Anforderungen. Diese Kategorie ist fuer die meisten Unternehmen die relevanteste, da sie zahlreiche gaengige Anwendungsgebiete umfasst. Hochrisiko-Systeme sind in Anhang III der Verordnung aufgelistet und betreffen unter anderem:
</p>
<div className="mt-3 grid sm:grid-cols-2 gap-3">
<div className="bg-white rounded-lg p-3 border border-orange-200">
<p className="text-sm font-medium text-gray-900">Bildung & Ausbildung</p>
<p className="text-xs text-gray-500 mt-1">Zugang zu Bildungseinrichtungen, Pruefungsbewertung, Leistungsbeurteilung</p>
</div>
<div className="bg-white rounded-lg p-3 border border-orange-200">
<p className="text-sm font-medium text-gray-900">Personalwesen</p>
<p className="text-xs text-gray-500 mt-1">Bewerberauswahl, Leistungsbewertung, Befoerderungsentscheidungen</p>
</div>
<div className="bg-white rounded-lg p-3 border border-orange-200">
<p className="text-sm font-medium text-gray-900">Kritische Infrastruktur</p>
<p className="text-xs text-gray-500 mt-1">Steuerung von Wasser, Gas, Strom, Verkehr</p>
</div>
<div className="bg-white rounded-lg p-3 border border-orange-200">
<p className="text-sm font-medium text-gray-900">Kreditwuerdigkeit & Versicherung</p>
<p className="text-xs text-gray-500 mt-1">Bonitaetsbewertung, Risikoeinstufung bei Lebens- und Krankenversicherung</p>
</div>
<div className="bg-white rounded-lg p-3 border border-orange-200">
<p className="text-sm font-medium text-gray-900">Strafverfolgung</p>
<p className="text-xs text-gray-500 mt-1">Risikobewertung, Luegenerkennung, Beweismittelbewertung</p>
</div>
<div className="bg-white rounded-lg p-3 border border-orange-200">
<p className="text-sm font-medium text-gray-900">Migration & Grenzschutz</p>
<p className="text-xs text-gray-500 mt-1">Asylantragsbewertung, Risikoeinstufung bei der Einreise</p>
</div>
</div>
</div>
{/* Begrenztes Risiko */}
<div className="border-l-4 border-yellow-500 bg-yellow-50 rounded-r-xl p-6">
<div className="flex items-center gap-3 mb-2">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-yellow-100 text-yellow-700 font-bold text-sm">B</span>
<h3 className="text-xl font-semibold text-gray-800">Begrenztes Risiko -- Transparenzpflichten</h3>
</div>
<p className="text-gray-600 text-sm mb-3">
Fuer KI-Systeme mit begrenztem Risiko gelten primaer Transparenz- und Kennzeichnungspflichten. Nutzer muessen informiert werden, dass sie mit einem KI-System interagieren oder dass Inhalte KI-generiert sind. Diese Anforderungen sind deutlich weniger umfangreich als bei Hochrisiko-Systemen.
</p>
<div className="mt-3">
<p className="text-sm font-medium text-yellow-800 mb-2">Beispiele:</p>
<ul className="text-sm text-gray-600 space-y-1">
<li className="flex items-start gap-2">
<span className="text-yellow-600 mt-1">&#x25B6;</span>
<span>Chatbots und virtuelle Assistenten (Nutzer muss wissen, dass er mit einer KI interagiert)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-yellow-600 mt-1">&#x25B6;</span>
<span>Deepfakes und KI-generierte Bild-/Video-/Audioinhalte (muessen als kuenstlich erzeugt gekennzeichnet werden)</span>
</li>
<li className="flex items-start gap-2">
<span className="text-yellow-600 mt-1">&#x25B6;</span>
<span>KI-generierte Texte, die zu Fragen des oeffentlichen Interesses veroeffentlicht werden</span>
</li>
<li className="flex items-start gap-2">
<span className="text-yellow-600 mt-1">&#x25B6;</span>
<span>Emotionserkennungssysteme (ausserhalb verbotener Kontexte) mit Informationspflicht</span>
</li>
</ul>
</div>
</div>
{/* Minimales Risiko */}
<div className="border-l-4 border-green-500 bg-green-50 rounded-r-xl p-6">
<div className="flex items-center gap-3 mb-2">
<span className="inline-flex items-center justify-center w-8 h-8 rounded-full bg-green-100 text-green-700 font-bold text-sm">M</span>
<h3 className="text-xl font-semibold text-gray-800">Minimales Risiko -- Keine besonderen Pflichten</h3>
</div>
<p className="text-gray-600 text-sm mb-3">
Die ueberwiegende Mehrheit der KI-Systeme faellt in diese Kategorie. Fuer sie gelten keine spezifischen regulatorischen Anforderungen aus dem AI Act. Die Kommission ermutigt Anbieter jedoch, freiwillige Verhaltenskodizes zu uebernehmen.
</p>
<div className="mt-3">
<p className="text-sm font-medium text-green-800 mb-2">Beispiele:</p>
<ul className="text-sm text-gray-600 space-y-1">
<li className="flex items-start gap-2">
<span className="text-green-600 mt-1">&#x2714;</span>
<span>Spam-Filter und E-Mail-Sortierung</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-600 mt-1">&#x2714;</span>
<span>Autokorrektur und Rechtschreibpruefung</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-600 mt-1">&#x2714;</span>
<span>KI-gestuetzte Empfehlungssysteme fuer Musik oder Filme</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-600 mt-1">&#x2714;</span>
<span>KI-optimierte Lagerverwaltung und Bestandsplanung</span>
</li>
<li className="flex items-start gap-2">
<span className="text-green-600 mt-1">&#x2714;</span>
<span>Suchmaschinen-Ranking und Content-Optimierung</span>
</li>
</ul>
</div>
</div>
</div>
</section>
{/* Section 3: Zeitplan */}
<section id="zeitplan">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
3. Zeitplan der Anwendung
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Der EU AI Act tritt nicht auf einen Schlag in vollem Umfang in Kraft, sondern wird in mehreren Stufen anwendbar. Dieser gestaffelte Ansatz gibt Unternehmen Zeit, sich auf die neuen Anforderungen vorzubereiten. Die folgende Zeitleiste zeigt die wesentlichen Meilensteine:
</p>
{/* Timeline */}
<div className="relative mt-8 ml-4">
{/* Vertical Line */}
<div className="absolute left-4 top-0 bottom-0 w-0.5 bg-gradient-to-b from-primary-400 via-primary-500 to-primary-600" />
{/* Aug 2024 */}
<div className="relative flex items-start gap-6 pb-10">
<div className="flex-shrink-0 w-9 h-9 bg-primary-500 rounded-full flex items-center justify-center z-10 ring-4 ring-white">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-5 flex-1 shadow-sm">
<div className="flex items-center gap-3 mb-2">
<span className="px-2.5 py-0.5 bg-green-100 text-green-700 text-xs font-medium rounded-full">Bereits in Kraft</span>
<span className="text-sm font-semibold text-slate-900">1. August 2024</span>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-1">Inkrafttreten der Verordnung</h3>
<p className="text-gray-600 text-sm">
Der EU AI Act wurde im Amtsblatt der EU veroeffentlicht und ist formell in Kraft getreten. Ab diesem Datum beginnen die Uebergangsfristen zu laufen.
</p>
</div>
</div>
{/* Feb 2025 */}
<div className="relative flex items-start gap-6 pb-10">
<div className="flex-shrink-0 w-9 h-9 bg-red-500 rounded-full flex items-center justify-center z-10 ring-4 ring-white">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L3.732 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
</div>
<div className="bg-white border border-red-200 rounded-xl p-5 flex-1 shadow-sm">
<div className="flex items-center gap-3 mb-2">
<span className="px-2.5 py-0.5 bg-red-100 text-red-700 text-xs font-medium rounded-full">Jetzt anwendbar</span>
<span className="text-sm font-semibold text-slate-900">2. Februar 2025</span>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-1">Verbotene KI-Praktiken</h3>
<p className="text-gray-600 text-sm">
Die Verbote fuer KI-Systeme mit unakzeptablem Risiko werden durchsetzbar. Unternehmen, die Social Scoring, manipulative KI oder verbotene biometrische Systeme einsetzen, muessen diese sofort einstellen. Verstoesse koennen ab diesem Zeitpunkt sanktioniert werden.
</p>
</div>
</div>
{/* Aug 2025 */}
<div className="relative flex items-start gap-6 pb-10">
<div className="flex-shrink-0 w-9 h-9 bg-accent-500 rounded-full flex items-center justify-center z-10 ring-4 ring-white">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9.663 17h4.673M12 3v1m6.364 1.636l-.707.707M21 12h-1M4 12H3m3.343-5.657l-.707-.707m2.828 9.9a5 5 0 117.072 0l-.548.547A3.374 3.374 0 0014 18.469V19a2 2 0 11-4 0v-.531c0-.895-.356-1.754-.988-2.386l-.548-.547z" />
</svg>
</div>
<div className="bg-white border border-accent-200 rounded-xl p-5 flex-1 shadow-sm">
<div className="flex items-center gap-3 mb-2">
<span className="px-2.5 py-0.5 bg-accent-100 text-accent-700 text-xs font-medium rounded-full">Kommend</span>
<span className="text-sm font-semibold text-slate-900">2. August 2025</span>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-1">GPAI-Modelle &amp; Governance</h3>
<p className="text-gray-600 text-sm">
Pflichten fuer Anbieter von General Purpose AI (GPAI) Modellen werden anwendbar. Dies betrifft insbesondere Anbieter grosser Sprachmodelle (wie GPT, Claude, Gemini). Transparenzpflichten, Urheberrechtsschutz und Dokumentationsanforderungen greifen. Das EU AI Office und nationale Aufsichtsbehoerden nehmen ihre Arbeit auf.
</p>
</div>
</div>
{/* Aug 2026 */}
<div className="relative flex items-start gap-6 pb-10">
<div className="flex-shrink-0 w-9 h-9 bg-orange-500 rounded-full flex items-center justify-center z-10 ring-4 ring-white">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 21V5a2 2 0 00-2-2H7a2 2 0 00-2 2v16m14 0h2m-2 0h-5m-9 0H3m2 0h5M9 7h1m-1 4h1m4-4h1m-1 4h1m-5 10v-5a1 1 0 011-1h2a1 1 0 011 1v5m-4 0h4" />
</svg>
</div>
<div className="bg-white border border-orange-200 rounded-xl p-5 flex-1 shadow-sm">
<div className="flex items-center gap-3 mb-2">
<span className="px-2.5 py-0.5 bg-orange-100 text-orange-700 text-xs font-medium rounded-full">Wichtiger Meilenstein</span>
<span className="text-sm font-semibold text-slate-900">2. August 2026</span>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-1">Hochrisiko-KI-Systeme (Anhang III)</h3>
<p className="text-gray-600 text-sm">
Der Grossteil der Verordnung wird anwendbar. Alle Pflichten fuer Hochrisiko-KI-Systeme nach Anhang III greifen vollstaendig: Risikomanagementsysteme, Datenqualitaet, technische Dokumentation, Transparenz, menschliche Aufsicht und Genauigkeitsanforderungen muessen implementiert sein. Dies ist der Stichtag, auf den sich die meisten Unternehmen vorbereiten muessen.
</p>
</div>
</div>
{/* Aug 2027 */}
<div className="relative flex items-start gap-6">
<div className="flex-shrink-0 w-9 h-9 bg-slate-400 rounded-full flex items-center justify-center z-10 ring-4 ring-white">
<svg className="w-4 h-4 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-5 flex-1 shadow-sm">
<div className="flex items-center gap-3 mb-2">
<span className="px-2.5 py-0.5 bg-slate-100 text-slate-700 text-xs font-medium rounded-full">Letzte Phase</span>
<span className="text-sm font-semibold text-slate-900">2. August 2027</span>
</div>
<h3 className="text-xl font-semibold text-gray-800 mb-1">Vollstaendige Anwendung</h3>
<p className="text-gray-600 text-sm">
Auch Hochrisiko-KI-Systeme nach Anhang I (die unter bestehende EU-Produktsicherheitsvorschriften fallen, z.B. Medizinprodukte, Maschinen, Spielzeug) muessen die Anforderungen vollstaendig erfuellen. Die gesamte Verordnung ist nun ohne Ausnahme anwendbar.
</p>
</div>
</div>
</div>
</section>
{/* Section 4: Pflichten fuer Anbieter */}
<section id="pflichten-anbieter">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
4. Pflichten fuer Anbieter von Hochrisiko-KI
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Anbieter (Provider) sind Unternehmen oder Personen, die ein KI-System entwickeln oder entwickeln lassen und es unter eigenem Namen auf den Markt bringen. Die Anforderungen an Anbieter von Hochrisiko-KI sind die umfangreichsten im gesamten Regulierungsrahmen. Artikel 8 bis 15 der Verordnung definieren sechs Kernpflichten:
</p>
<div className="space-y-6 mt-6">
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-3">
<span className="text-primary-600 mr-2">4.1</span>
Risikomanagementsystem (Art. 9)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Anbieter muessen ein kontinuierliches Risikomanagementsystem einrichten, das den gesamten Lebenszyklus des KI-Systems abdeckt. Dieses System muss bekannte und vorhersehbare Risiken identifizieren und analysieren, Risiken bewerten, die bei bestimmungsgemaeSSem Gebrauch sowie bei vernuenftigerweise vorhersehbarer Fehlanwendung auftreten koennen, und geeignete Risikominderungsmassnahmen umsetzen. Das Risikomanagement ist kein einmaliger Akt, sondern ein iterativer Prozess, der regelmaessig aktualisiert werden muss.
</p>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-3">
<span className="text-primary-600 mr-2">4.2</span>
Daten-Governance (Art. 10)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Trainings-, Validierungs- und Testdaten muessen strengen Qualitaetsanforderungen genuegen. Datensaetze muessen relevant, repraesentativ, fehlerfrei und vollstaendig sein. Anbieter muessen Verzerrungen (Bias) in den Daten erkennen und beheben. Besondere Sorgfaltspflichten gelten beim Umgang mit besonderen Kategorien personenbezogener Daten (Art. 9 DSGVO). Die Datensaetze muessen dokumentiert und nachvollziehbar sein.
</p>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-3">
<span className="text-primary-600 mr-2">4.3</span>
Technische Dokumentation (Art. 11)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Vor dem Inverkehrbringen muss eine umfassende technische Dokumentation erstellt werden. Diese umfasst eine allgemeine Beschreibung des KI-Systems, detaillierte Informationen zur Entwicklung (einschliesslich Trainingsmethoden und verwendeter Daten), die Architektur des Systems, die Konformitaetsbewertung und Gebrauchsanweisungen. Die Dokumentation muss regelmaessig aktualisiert und auf Anfrage den Aufsichtsbehoerden vorgelegt werden.
</p>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-3">
<span className="text-primary-600 mr-2">4.4</span>
Transparenz und Bereitstellung von Informationen (Art. 13)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Hochrisiko-KI-Systeme muessen so konzipiert sein, dass ihr Betrieb hinreichend transparent ist. Nutzer (Deployer) muessen die Ergebnisse des Systems interpretieren und angemessen nutzen koennen. Gebrauchsanweisungen muessen Informationen ueber Leistungsmerkmale, Einschraenkungen, Risiken und Kontrollmassnahmen enthalten. Ein automatisches Protokollierungssystem (Logging) muss implementiert sein.
</p>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-3">
<span className="text-primary-600 mr-2">4.5</span>
Menschliche Aufsicht (Art. 14)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
KI-Systeme muessen so gestaltet sein, dass sie waehrend der Nutzung von natuerlichen Personen wirksam ueberwacht werden koennen. Dies bedeutet: Personen, die die Aufsicht ueben, muessen die Faehigkeiten und Grenzen des Systems verstehen, Anzeichen von Anomalien erkennen koennen und in der Lage sein, das System jederzeit zu stoppen oder zu uebersteuern. Das &quot;Human-in-the-Loop&quot;-Prinzip ist ein zentrales Element des EU AI Act.
</p>
</div>
<div className="bg-white border border-slate-200 rounded-xl p-6 shadow-sm">
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-3">
<span className="text-primary-600 mr-2">4.6</span>
Genauigkeit, Robustheit und Cybersicherheit (Art. 15)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Hochrisiko-KI-Systeme muessen ein angemessenes Niveau an Genauigkeit, Robustheit und Cybersicherheit erreichen. Die Leistungswerte muessen in der technischen Dokumentation angegeben werden. Systeme muessen widerstandsfaehig gegen Fehler, Manipulationsversuche (Adversarial Attacks) und Datenvergiftung (Data Poisoning) sein. Regelmaessige Tests und Validierungen sind erforderlich, um die Zuverlaessigkeit sicherzustellen.
</p>
</div>
</div>
</section>
{/* Section 5: Pflichten fuer Nutzer */}
<section id="pflichten-nutzer">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
5. Pflichten fuer Nutzer (Deployer) von KI-Systemen
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Auch Unternehmen, die KI-Systeme nicht selbst entwickeln, sondern von Drittanbietern beziehen und in ihren Geschaeftsprozessen einsetzen, haben Pflichten unter dem EU AI Act. Der Verordnungstext verwendet hierfuer den Begriff &quot;Deployer&quot; (Betreiber/Nutzer). Die folgenden Pflichten sind besonders relevant:
</p>
<div className="bg-slate-50 rounded-xl p-6 mt-6 space-y-4">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-700 font-bold text-sm">1</span>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-1">Bestimmungsgemaeasser Einsatz</h3>
<p className="text-gray-600 text-sm">Hochrisiko-KI-Systeme duerfen nur gemaess der vom Anbieter bereitgestellten Gebrauchsanweisung eingesetzt werden. Eine Verwendung ausserhalb des vorgesehenen Zwecks kann den Deployer selbst zum Anbieter machen -- mit allen damit verbundenen Pflichten.</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-700 font-bold text-sm">2</span>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-1">Menschliche Aufsicht gewaehrleisten</h3>
<p className="text-gray-600 text-sm">Deployer muessen sicherstellen, dass die Personen, die fuer die menschliche Aufsicht zustaendig sind, ausreichend geschult und kompetent sind. Diese Personen muessen die Autoritaet und die Moeglichkeit haben, Ergebnisse des KI-Systems zu uebersteuern oder das System abzuschalten.</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-700 font-bold text-sm">3</span>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-1">Eingabedatenqualitaet</h3>
<p className="text-gray-600 text-sm">Soweit der Deployer Kontrolle ueber die Eingabedaten hat, muss er sicherstellen, dass diese dem Verwendungszweck des Systems angemessen und repraesentativ sind.</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-700 font-bold text-sm">4</span>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-1">Monitoring und Meldepflichten</h3>
<p className="text-gray-600 text-sm">Deployer muessen den Betrieb des KI-Systems ueberwachen und den Anbieter sowie die zustaendige Behoerde informieren, wenn sie Grund zur Annahme haben, dass das System ein Risiko darstellt. Schwerwiegende Vorfaelle muessen gemeldet werden.</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-700 font-bold text-sm">5</span>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-1">Datenschutz-Folgenabschaetzung (DSFA)</h3>
<p className="text-gray-600 text-sm">Wenn der Einsatz eines Hochrisiko-KI-Systems die Verarbeitung personenbezogener Daten umfasst, muessen Deployer vor dem Einsatz eine Datenschutz-Folgenabschaetzung gemaess Art. 35 DSGVO durchfuehren. Das Ergebnis der vom Anbieter bereitgestellten Grundrechte-Folgenabschaetzung ist hierbei zu beruecksichtigen.</p>
</div>
</div>
<div className="flex items-start gap-4">
<div className="flex-shrink-0 w-8 h-8 bg-primary-100 rounded-lg flex items-center justify-center">
<span className="text-primary-700 font-bold text-sm">6</span>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-1">Aufbewahrung automatisch erzeugter Protokolle</h3>
<p className="text-gray-600 text-sm">Die vom KI-System automatisch erzeugten Protokolle muessen mindestens sechs Monate lang aufbewahrt werden, sofern nicht in anderen Rechtsvorschriften laengere Aufbewahrungsfristen vorgesehen sind.</p>
</div>
</div>
</div>
</section>
{/* Section 6: Strafen */}
<section id="strafen">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
6. Strafen bei Verstoss
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Der EU AI Act sieht ein dreistufiges Bussgeld-System vor, das sich an der Schwere des Verstosses orientiert. Aehnlich wie bei der DSGVO sind die Hoechstbetraege abschreckend hoch und richten sich nach dem weltweiten Jahresumsatz des Unternehmens. Fuer KMU und Start-ups gelten mildere Obergrenzen.
</p>
<div className="space-y-4 mt-6">
<div className="bg-red-50 border border-red-200 rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-semibold text-gray-800">Verbotene KI-Praktiken</h3>
<span className="px-3 py-1 bg-red-100 text-red-700 text-sm font-bold rounded-full">Hoechststufe</span>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div className="text-center p-4 bg-white rounded-lg">
<p className="text-3xl font-bold text-red-600">35 Mio. EUR</p>
<p className="text-sm text-gray-500 mt-1">oder</p>
<p className="text-xl font-bold text-red-600">7 % des Jahresumsatzes</p>
<p className="text-xs text-gray-400 mt-1">(der hoehere Betrag gilt)</p>
</div>
<div className="flex items-center">
<p className="text-sm text-gray-600">
Gilt fuer den Einsatz verbotener KI-Praktiken (Art. 5) und Verstoesse gegen die Anforderungen an Daten fuer das Training von KI-Systemen bei Minderjaerigen.
</p>
</div>
</div>
</div>
<div className="bg-orange-50 border border-orange-200 rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-semibold text-gray-800">Verstoesse gegen Kernpflichten</h3>
<span className="px-3 py-1 bg-orange-100 text-orange-700 text-sm font-bold rounded-full">Mittelstufe</span>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div className="text-center p-4 bg-white rounded-lg">
<p className="text-3xl font-bold text-orange-600">15 Mio. EUR</p>
<p className="text-sm text-gray-500 mt-1">oder</p>
<p className="text-xl font-bold text-orange-600">3 % des Jahresumsatzes</p>
<p className="text-xs text-gray-400 mt-1">(der hoehere Betrag gilt)</p>
</div>
<div className="flex items-center">
<p className="text-sm text-gray-600">
Gilt fuer Verstoesse gegen die Pflichten fuer Anbieter und Deployer von Hochrisiko-KI-Systemen, einschliesslich Risikomanagement, Daten-Governance, Transparenz und menschliche Aufsicht.
</p>
</div>
</div>
</div>
<div className="bg-yellow-50 border border-yellow-200 rounded-xl p-6">
<div className="flex items-center justify-between mb-3">
<h3 className="text-xl font-semibold text-gray-800">Falsche Angaben gegenueber Behoerden</h3>
<span className="px-3 py-1 bg-yellow-100 text-yellow-700 text-sm font-bold rounded-full">Grundstufe</span>
</div>
<div className="grid sm:grid-cols-2 gap-4">
<div className="text-center p-4 bg-white rounded-lg">
<p className="text-3xl font-bold text-yellow-600">7,5 Mio. EUR</p>
<p className="text-sm text-gray-500 mt-1">oder</p>
<p className="text-xl font-bold text-yellow-600">1 % des Jahresumsatzes</p>
<p className="text-xs text-gray-400 mt-1">(der hoehere Betrag gilt)</p>
</div>
<div className="flex items-center">
<p className="text-sm text-gray-600">
Gilt fuer die Bereitstellung unrichtiger, unvollstaendiger oder irrefuehrender Informationen an Aufsichtsbehoerden oder benannte Stellen.
</p>
</div>
</div>
</div>
</div>
<div className="bg-primary-50 border border-primary-200 rounded-xl p-6 my-8">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-primary-600 flex-shrink-0 mt-0.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<p className="font-semibold text-primary-900 mb-1">KMU-Erleichterungen</p>
<p className="text-primary-800 text-sm">
Fuer kleine und mittlere Unternehmen (KMU) sowie Start-ups gelten verhaeltnismaessig niedrigere Bussgeld-Obergrenzen. Zudem sollen die Aufsichtsbehoerden bei der Festsetzung von Bussgeldern die wirtschaftliche Leistungsfaehigkeit des Unternehmens beruecksichtigen. Die Verordnung sieht auch &quot;Regulatory Sandboxes&quot; vor -- kontrollierte Testumgebungen, in denen KMU und Start-ups innovative KI-Systeme unter Aufsicht entwickeln und testen koennen.
</p>
</div>
</div>
</div>
</section>
{/* Section 7: Checkliste */}
<section id="checkliste">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
7. Checkliste: Ist Ihr Unternehmen betroffen?
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Die folgenden fuenf Fragen helfen Ihnen, eine erste Einschaetzung vorzunehmen, ob und in welchem Umfang Ihr Unternehmen von den Pflichten des EU AI Act betroffen ist. Je mehr Fragen Sie mit &quot;Ja&quot; beantworten, desto dringender sollten Sie sich mit der Regulierung befassen.
</p>
<div className="space-y-4 mt-6">
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-primary-300 transition-colors">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 mt-1">
<div className="w-6 h-6 border-2 border-slate-300 rounded flex items-center justify-center bg-white">
<svg className="w-4 h-4 text-primary-600 opacity-0 group-hover:opacity-100" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
</div>
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">Frage 1: Setzen Sie KI-Systeme ein?</h3>
<p className="text-gray-600 text-sm">
Nutzt Ihr Unternehmen Software, die auf Machine Learning, Deep Learning, Natural Language Processing oder anderen KI-Techniken basiert? Dies schliesst auch eingekaufte SaaS-Loesungen, eingebettete KI-Funktionen in bestehender Software und den Einsatz von APIs grosser KI-Anbieter (z.B. OpenAI, Google, Anthropic) ein.
</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-primary-300 transition-colors">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 mt-1">
<div className="w-6 h-6 border-2 border-slate-300 rounded flex items-center justify-center bg-white" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">Frage 2: Treffen KI-Systeme Entscheidungen ueber Personen?</h3>
<p className="text-gray-600 text-sm">
Werden KI-Systeme eingesetzt, um Entscheidungen zu treffen oder vorzubereiten, die natuerliche Personen betreffen? Dazu gehoeren Bewerberauswahl, Leistungsbewertung, Kreditwuerdigkeitspruefung, Versicherungseinstufung, Zugang zu Bildung, Sozialleistungen oder oeffentlichen Diensten.
</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-primary-300 transition-colors">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 mt-1">
<div className="w-6 h-6 border-2 border-slate-300 rounded flex items-center justify-center bg-white" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">Frage 3: Entwickeln oder vertreiben Sie KI-Systeme?</h3>
<p className="text-gray-600 text-sm">
Sind Sie Anbieter (Provider) eines KI-Systems, das in der EU auf den Markt gebracht oder in Betrieb genommen wird? Auch das Rebranding oder wesentliche Veraendern eines bestehenden KI-Systems kann Sie zum Anbieter im Sinne der Verordnung machen. Pruefen Sie, ob Sie als Provider, Deployer, Importeur oder Haendler einzustufen sind.
</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-primary-300 transition-colors">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 mt-1">
<div className="w-6 h-6 border-2 border-slate-300 rounded flex items-center justify-center bg-white" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">Frage 4: Nutzen Sie KI in einem regulierten Sektor?</h3>
<p className="text-gray-600 text-sm">
Ist Ihr Unternehmen in einem der in Anhang III genannten Bereiche taetig? Dazu gehoeren unter anderem: Bildung, Personalwesen, kritische Infrastruktur, Finanzdienstleistungen, Gesundheitswesen, Strafverfolgung, Migration und Justiz. In diesen Sektoren ist die Wahrscheinlichkeit hoch, dass Ihre KI-Systeme als Hochrisiko eingestuft werden.
</p>
</div>
</div>
</div>
<div className="bg-white border-2 border-slate-200 rounded-xl p-6 hover:border-primary-300 transition-colors">
<div className="flex items-start gap-4">
<div className="flex-shrink-0 mt-1">
<div className="w-6 h-6 border-2 border-slate-300 rounded flex items-center justify-center bg-white" />
</div>
<div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">Frage 5: Haben Sie bereits ein KI-Inventar?</h3>
<p className="text-gray-600 text-sm">
Wissen Sie, welche KI-Systeme in Ihrem Unternehmen im Einsatz sind, wer sie anbietet, welche Daten sie verarbeiten und welche Entscheidungen sie beeinflussen? Ein vollstaendiges KI-Portfolio ist der erste Schritt zur Compliance. Ohne Ueberblick ueber die eigene KI-Landschaft ist eine Risikobewertung unmoeglich.
</p>
</div>
</div>
</div>
</div>
<div className="bg-slate-50 border border-slate-200 rounded-xl p-6 mt-8">
<p className="text-sm text-gray-600">
<span className="font-semibold text-gray-900">Auswertung: </span>
Wenn Sie mindestens eine der obigen Fragen mit &quot;Ja&quot; beantwortet haben, sind Sie mit hoher Wahrscheinlichkeit vom EU AI Act betroffen. Bei zwei oder mehr Ja-Antworten empfehlen wir eine detaillierte Analyse Ihrer KI-Systeme und eine formelle Risikoeinstufung. Je frueher Sie beginnen, desto mehr Zeit bleibt fuer die Umsetzung der erforderlichen Massnahmen vor den jeweiligen Stichtagen.
</p>
</div>
</section>
{/* Section 8: BreakPilot Comply */}
<section id="breakpilot-comply">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
8. Wie BreakPilot Comply hilft
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
BreakPilot Comply unterstuetzt Unternehmen dabei, die Anforderungen des EU AI Act strukturiert und effizient umzusetzen. Unsere Plattform wurde speziell fuer die Herausforderungen der KI-Regulierung entwickelt und bietet zwei Kernmodule:
</p>
<div className="grid sm:grid-cols-2 gap-6 mt-6">
<div className="bg-gradient-to-br from-primary-50 to-primary-100 rounded-xl p-6 border border-primary-200">
<div className="w-12 h-12 bg-primary-500 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 5H7a2 2 0 00-2 2v10a2 2 0 002 2h8a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2m-3 7h3m-3 4h3m-6-4h.01M9 16h.01" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">AI Portfolio</h3>
<p className="text-gray-600 text-sm">
Erfassen Sie alle KI-Systeme Ihres Unternehmens in einem zentralen Verzeichnis. Klassifizieren Sie jedes System nach Risikokategorie, dokumentieren Sie Anbieter, Verwendungszweck, verarbeitete Daten und verantwortliche Personen. Das AI Portfolio schafft die Transparenz, die fuer eine fundierte Compliance-Bewertung notwendig ist.
</p>
</div>
<div className="bg-gradient-to-br from-accent-50 to-accent-100 rounded-xl p-6 border border-accent-200">
<div className="w-12 h-12 bg-accent-500 rounded-xl flex items-center justify-center mb-4">
<svg className="w-6 h-6 text-white" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<h3 className="text-xl font-semibold text-gray-800 mt-0 mb-2">UCCA Assessment</h3>
<p className="text-gray-600 text-sm">
Fuehren Sie fuer jedes KI-System eine strukturierte Anwendungsfall-Konformitaetsbewertung (Use Case Conformity Assessment) durch. Unser gefuehrter Prozess leitet Sie durch die Risikoeinstufung, identifiziert relevante Pflichten und generiert die erforderliche Dokumentation -- von der technischen Beschreibung bis zum Risikomanagementplan.
</p>
</div>
</div>
</section>
{/* CTA Section */}
<section className="mt-16 mb-8 bg-gradient-to-br from-primary-600 to-primary-700 rounded-2xl p-8 text-center text-white">
<h2 className="text-2xl font-bold mb-3">Bereit fuer den EU AI Act?</h2>
<p className="text-primary-100 mb-6 max-w-lg mx-auto">
Starten Sie jetzt mit der Bestandsaufnahme Ihrer KI-Systeme und stellen Sie rechtzeitig Compliance sicher.
</p>
<div className="flex flex-col sm:flex-row gap-3 justify-center">
<Link
href="/kontakt"
className="inline-flex items-center justify-center px-6 py-3 bg-white text-primary-700 rounded-xl font-medium hover:bg-primary-50 transition-colors"
>
Beratungsgespraech vereinbaren
</Link>
<Link
href="/blog"
className="inline-flex items-center justify-center px-6 py-3 border border-white/30 text-white rounded-xl font-medium hover:bg-white/10 transition-colors"
>
Weitere Artikel lesen
</Link>
</div>
</section>
{/* Disclaimer */}
<div className="mt-8 p-4 bg-slate-50 rounded-lg border border-slate-200">
<p className="text-xs text-slate-500">
<span className="font-semibold">Hinweis:</span> Dieser Artikel dient ausschliesslich zu Informationszwecken und stellt keine Rechtsberatung dar. Die Inhalte wurden mit Sorgfalt recherchiert, koennen jedoch aufgrund der sich entwickelnden Regulierungslandschaft und nationaler Umsetzungsgesetze Aenderungen unterliegen. Fuer verbindliche Auskuenfte wenden Sie sich bitte an einen spezialisierten Rechtsberater. Stand: Februar 2025.
</p>
</div>
</div>
</article>
</div>
)
}

View File

@@ -0,0 +1,725 @@
import { Metadata } from 'next'
export const metadata: Metadata = {
title: 'VVT erstellen - Schritt-fuer-Schritt Anleitung | BreakPilot Comply',
description: 'Lernen Sie, wie Sie ein DSGVO-konformes Verzeichnis von Verarbeitungstaetigkeiten (VVT) nach Art. 30 erstellen.',
openGraph: {
title: 'VVT erstellen - Schritt-fuer-Schritt Anleitung',
description: 'Lernen Sie, wie Sie ein DSGVO-konformes Verzeichnis von Verarbeitungstaetigkeiten (VVT) nach Art. 30 erstellen.',
type: 'article',
locale: 'de_DE',
},
}
export default function VVTErstellenPage() {
return (
<div className="min-h-screen bg-white">
{/* Hero Section */}
<section className="bg-gradient-to-br from-sky-50 via-white to-fuchsia-50 border-b border-gray-100">
<div className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12 sm:py-16">
<div className="flex items-center gap-2 mb-6">
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-sky-100 text-sky-700">
DSGVO-Leitfaden
</span>
<span className="inline-flex items-center px-3 py-1 rounded-full text-sm font-medium bg-gray-100 text-gray-600">
Art. 30 DSGVO
</span>
</div>
<h1 className="text-4xl sm:text-5xl font-bold text-gray-900 leading-tight mb-6">
Verzeichnis von Verarbeitungstaetigkeiten (VVT) erstellen
</h1>
<p className="text-xl text-gray-600 leading-relaxed max-w-3xl">
Eine umfassende Schritt-fuer-Schritt Anleitung zur Erstellung eines
DSGVO-konformen VVT nach Art. 30 &ndash; mit Vorlagen, Praxistipps und
haeufigen Fehlern, die Sie vermeiden sollten.
</p>
<div className="flex items-center gap-4 mt-8 text-sm text-gray-500">
<span>Aktualisiert: Februar 2026</span>
<span className="w-1 h-1 rounded-full bg-gray-300" />
<span>Lesezeit: ca. 12 Minuten</span>
</div>
</div>
</section>
{/* Table of Contents */}
<nav className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-8 border-b border-gray-100">
<h2 className="text-sm font-semibold text-gray-500 uppercase tracking-wider mb-4">Inhalt</h2>
<ol className="grid sm:grid-cols-2 gap-2 text-gray-700">
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">1.</span>
<a href="#was-ist-vvt" className="hover:text-sky-600 transition-colors">Was ist ein VVT?</a>
</li>
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">2.</span>
<a href="#wer-muss-vvt-fuehren" className="hover:text-sky-600 transition-colors">Wer muss ein VVT fuehren?</a>
</li>
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">3.</span>
<a href="#pflichtinhalte" className="hover:text-sky-600 transition-colors">Was muss im VVT stehen?</a>
</li>
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">4.</span>
<a href="#schritt-fuer-schritt" className="hover:text-sky-600 transition-colors">Schritt-fuer-Schritt Anleitung</a>
</li>
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">5.</span>
<a href="#haeufige-fehler" className="hover:text-sky-600 transition-colors">Haeufige Fehler vermeiden</a>
</li>
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">6.</span>
<a href="#vvt-vorlage" className="hover:text-sky-600 transition-colors">VVT-Vorlage mit Beispieleintraegen</a>
</li>
<li className="flex items-start gap-2">
<span className="text-sky-500 font-semibold">7.</span>
<a href="#automatisierung" className="hover:text-sky-600 transition-colors">Automatisierung mit BreakPilot Comply</a>
</li>
</ol>
</nav>
{/* Article Content */}
<article className="max-w-4xl mx-auto px-4 sm:px-6 lg:px-8 py-12">
{/* Section 1: Was ist ein VVT? */}
<section id="was-ist-vvt">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
1. Was ist ein Verzeichnis von Verarbeitungstaetigkeiten?
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Das Verzeichnis von Verarbeitungstaetigkeiten (VVT) ist ein zentrales Dokumentationsinstrument der
Datenschutz-Grundverordnung (DSGVO). Es erfasst systematisch alle Prozesse innerhalb einer Organisation,
bei denen personenbezogene Daten verarbeitet werden. Das VVT bildet damit das Rueckgrat Ihres
Datenschutzmanagements und dient als Nachweis gegenueber Aufsichtsbehoerden, dass Sie Ihre
Rechenschaftspflicht nach Art. 5 Abs. 2 DSGVO ernst nehmen.
</p>
{/* Legal Reference Box */}
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6 mb-6">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<div>
<h4 className="font-semibold text-gray-800 mb-1">Rechtsgrundlage: Art. 30 DSGVO</h4>
<p className="text-gray-600 text-sm leading-relaxed">
&laquo;Jeder Verantwortliche und gegebenenfalls sein Vertreter fuehren ein Verzeichnis aller
Verarbeitungstaetigkeiten, die ihrer Zustaendigkeit unterliegen.&raquo; &ndash; Art. 30 Abs. 1 Satz 1 DSGVO.
Das VVT muss schriftlich gefuehrt werden, wobei ein elektronisches Format zulaessig ist (Art. 30 Abs. 3).
</p>
</div>
</div>
</div>
<p className="text-gray-600 leading-relaxed mb-4">
Konkret bedeutet das: Jedes Mal, wenn Ihre Organisation personenbezogene Daten erhebt, speichert,
uebertraegt, aendert oder loescht, stellt dies eine Verarbeitungstaetigkeit dar, die im VVT
dokumentiert werden muss. Typische Beispiele sind die Lohn- und Gehaltsabrechnung, das
Bewerbermanagement, der Betrieb einer Website mit Kontaktformular oder die Nutzung von
Cloud-Diensten zur E-Mail-Kommunikation.
</p>
<p className="text-gray-600 leading-relaxed mb-4">
Das VVT ist kein einmaliges Projekt, sondern ein lebendes Dokument: Es muss fortlaufend aktualisiert
werden, wenn neue Verarbeitungen hinzukommen, bestehende sich aendern oder Verarbeitungen eingestellt
werden. Aufsichtsbehoerden koennen das VVT jederzeit anfordern &ndash; eine fehlende oder unvollstaendige
Dokumentation kann Bussgelder nach Art. 83 Abs. 4 lit. a DSGVO nach sich ziehen.
</p>
</section>
{/* Section 2: Wer muss ein VVT fuehren? */}
<section id="wer-muss-vvt-fuehren">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
2. Wer muss ein VVT fuehren?
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Die kurze Antwort: Nahezu jede Organisation. Art. 30 Abs. 5 DSGVO sieht zwar eine Ausnahme fuer
Unternehmen mit weniger als 250 Mitarbeitern vor, doch diese Ausnahme greift nur, wenn die
Verarbeitung nicht regelmaessig erfolgt, keine Risiken fuer Betroffene birgt und keine besonderen
Datenkategorien (Art. 9) betrifft. In der Praxis erfuellt kaum ein Unternehmen alle drei Bedingungen
gleichzeitig &ndash; bereits eine regelmaessige Kundendatenbank oder Lohnbuchhaltung macht das VVT zur Pflicht.
</p>
<h3 className="text-xl font-semibold text-gray-800 mt-8 mb-3">
Pflichten fuer Verantwortliche (Art. 30 Abs. 1)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Als Verantwortlicher &ndash; also die natuerliche oder juristische Person, die ueber Zwecke und Mittel der
Verarbeitung entscheidet &ndash; muessen Sie ein umfassendes VVT fuehren. Dies betrifft jedes Unternehmen,
jeden Verein, jede Behoerde und jede Bildungseinrichtung, die personenbezogene Daten verarbeitet.
Das VVT des Verantwortlichen umfasst die meisten Pflichtangaben und bildet die Grundlage fuer
alle weiteren Datenschutzmassnahmen wie die Datenschutz-Folgenabschaetzung (DSFA) oder die
Beantwortung von Betroffenenanfragen.
</p>
<h3 className="text-xl font-semibold text-gray-800 mt-8 mb-3">
Pflichten fuer Auftragsverarbeiter (Art. 30 Abs. 2)
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Auch Auftragsverarbeiter &ndash; also Dienstleister, die personenbezogene Daten im Auftrag des
Verantwortlichen verarbeiten &ndash; muessen ein eigenes VVT fuehren. Typische Beispiele sind
Cloud-Anbieter, IT-Dienstleister, Lohnbueros oder Hosting-Provider. Das VVT des
Auftragsverarbeiters ist weniger umfangreich, muss aber dennoch Kategorien der Verarbeitungen,
Angaben zu Drittlandsuebermittlungen und die technisch-organisatorischen Massnahmen (TOMs) enthalten.
</p>
{/* Tip Box */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-5 mb-6">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-sky-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-sky-800 mb-1">Praxistipp</h4>
<p className="text-sky-700 text-sm leading-relaxed">
Auch wenn Sie glauben, unter die Ausnahme des Art. 30 Abs. 5 zu fallen: Fuehren Sie trotzdem
ein VVT. Ohne Verzeichnis koennen Sie die Rechenschaftspflicht nach Art. 5 Abs. 2 DSGVO
kaum erfuellen. Im Ernstfall muss Ihre Organisation nachweisen, dass die Ausnahme greift &ndash;
und genau das erfordert eine Dokumentation, die dem VVT stark aehnelt.
</p>
</div>
</div>
</div>
<h3 className="text-xl font-semibold text-gray-800 mt-8 mb-3">
Sonderfall: Bildungseinrichtungen und Schulen
</h3>
<p className="text-gray-600 leading-relaxed mb-4">
Schulen und Bildungseinrichtungen verarbeiten regelmaessig besonders schuetzenswerte Daten von
Minderjaehrigen. Hier gelten erhoehte Anforderungen: Noten, Fehlzeiten, Foerdermassnahmen und
gesundheitliche Einschraenkungen gehoeren zu den Kategorien, die besonderer Sorgfalt beduerfen.
Das VVT muss hier besonders detailliert die Rechtsgrundlagen (haeufig Art. 6 Abs. 1 lit. e DSGVO
in Verbindung mit den jeweiligen Landesschulgesetzen) und die Loeschfristen dokumentieren.
</p>
</section>
{/* Section 3: Was muss im VVT stehen? */}
<section id="pflichtinhalte">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
3. Was muss im VVT stehen? &ndash; Pflichtinhalte nach Art. 30 Abs. 1
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Art. 30 Abs. 1 DSGVO definiert einen Mindestkatalog an Angaben, die jede Verarbeitungstaetigkeit
im VVT enthalten muss. Die folgende Tabelle zeigt alle Pflichtfelder mit Erlaeuterungen und
konkreten Beispielen aus der Praxis.
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="bg-gray-900 text-white">
<th className="text-left px-4 py-3 font-semibold rounded-tl-lg">Pflichtfeld</th>
<th className="text-left px-4 py-3 font-semibold">Rechtsgrundlage</th>
<th className="text-left px-4 py-3 font-semibold rounded-tr-lg">Beispiel</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100">
<td className="px-4 py-3 font-medium text-gray-800">Name und Kontaktdaten des Verantwortlichen</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. a</td>
<td className="px-4 py-3 text-gray-600">Musterfirma GmbH, Musterstr. 1, 10115 Berlin</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-4 py-3 font-medium text-gray-800">Name des Datenschutzbeauftragten</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. a</td>
<td className="px-4 py-3 text-gray-600">Dr. Maria Muster, dsb@musterfirma.de</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-4 py-3 font-medium text-gray-800">Zwecke der Verarbeitung</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. b</td>
<td className="px-4 py-3 text-gray-600">Durchfuehrung des Beschaeftigungsverhaeltnisses</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-4 py-3 font-medium text-gray-800">Kategorien betroffener Personen</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. c</td>
<td className="px-4 py-3 text-gray-600">Beschaeftigte, Bewerber, Kunden</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-4 py-3 font-medium text-gray-800">Kategorien personenbezogener Daten</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. c</td>
<td className="px-4 py-3 text-gray-600">Stammdaten, Kontaktdaten, Bankverbindung</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-4 py-3 font-medium text-gray-800">Kategorien von Empfaengern</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. d</td>
<td className="px-4 py-3 text-gray-600">Finanzbehorden, Sozialversicherungstraeger, Steuerberater</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-4 py-3 font-medium text-gray-800">Uebermittlungen in Drittlaender</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. e</td>
<td className="px-4 py-3 text-gray-600">USA (Standardvertragsklauseln), kein Transfer</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-4 py-3 font-medium text-gray-800">Loeschfristen</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. f</td>
<td className="px-4 py-3 text-gray-600">3 Jahre nach Ende des Beschaeftigungsverhaeltnisses</td>
</tr>
<tr>
<td className="px-4 py-3 font-medium text-gray-800 rounded-bl-lg">Technisch-organisatorische Massnahmen (TOMs)</td>
<td className="px-4 py-3 text-gray-600">Art. 30 Abs. 1 lit. g</td>
<td className="px-4 py-3 text-gray-600 rounded-br-lg">Verschluesselung, Zugriffskontrolle, Backups</td>
</tr>
</tbody>
</table>
</div>
{/* Legal Reference Box */}
<div className="bg-gray-50 border border-gray-200 rounded-xl p-6 mb-6">
<div className="flex items-start gap-3">
<div className="flex-shrink-0 w-10 h-10 bg-gray-200 rounded-lg flex items-center justify-center">
<svg className="w-5 h-5 text-gray-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" />
</svg>
</div>
<div>
<h4 className="font-semibold text-gray-800 mb-1">Hinweis: Empfohlene Zusatzangaben</h4>
<p className="text-gray-600 text-sm leading-relaxed">
Ueber die Pflichtfelder hinaus empfehlen Aufsichtsbehoerden wie die DSK (Datenschutzkonferenz),
weitere Felder aufzunehmen: die Rechtsgrundlage der Verarbeitung (Art. 6 Abs. 1 DSGVO),
die verantwortliche Fachabteilung, das Datum der letzten Aktualisierung sowie einen
Verweis auf eine durchgefuehrte Datenschutz-Folgenabschaetzung (DSFA), falls erforderlich.
</p>
</div>
</div>
</div>
</section>
{/* Section 4: Schritt-fuer-Schritt */}
<section id="schritt-fuer-schritt">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
4. VVT erstellen &ndash; Schritt-fuer-Schritt Anleitung
</h2>
<p className="text-gray-600 leading-relaxed mb-6">
Die Erstellung eines VVT wirkt auf den ersten Blick komplex, laesst sich aber in fuenf
ueberschaubare Schritte gliedern. Gehen Sie methodisch vor und beziehen Sie alle Fachabteilungen
Ihrer Organisation ein.
</p>
{/* Step 1 */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-6 mb-6">
<div className="flex items-start gap-4">
<span className="flex-shrink-0 w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center font-bold text-lg">
1
</span>
<div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">Bestandsaufnahme aller Verarbeitungen</h3>
<p className="text-gray-600 leading-relaxed mb-3">
Erfassen Sie zunaechst alle Prozesse in Ihrer Organisation, bei denen personenbezogene
Daten verarbeitet werden. Fuehren Sie Interviews mit den Leitern aller Fachabteilungen:
Personalwesen, Vertrieb, Marketing, IT, Finanzbuchhaltung, Kundenservice und Geschaeftsfuehrung.
</p>
<p className="text-gray-600 leading-relaxed">
<span className="font-medium text-gray-800">Praxistipp:</span> Starten Sie mit einer einfachen
Frage an jede Abteilung: &laquo;Welche personenbezogenen Daten verarbeiten Sie im Tagesgeschaeft und
welche Software-Systeme nutzen Sie dafuer?&raquo; Erstellen Sie daraus eine Rohliste aller
Verarbeitungstaetigkeiten. Vergessen Sie nicht die weniger offensichtlichen Prozesse wie
Videoüberwachung, Zugangskontrollsysteme oder die Nutzung von Messenger-Diensten.
</p>
</div>
</div>
</div>
{/* Step 2 */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-6 mb-6">
<div className="flex items-start gap-4">
<span className="flex-shrink-0 w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center font-bold text-lg">
2
</span>
<div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">Verarbeitungen strukturiert dokumentieren</h3>
<p className="text-gray-600 leading-relaxed mb-3">
Ueberfuehren Sie die Rohliste in ein strukturiertes Format. Fuer jede Verarbeitungstaetigkeit
erfassen Sie alle Pflichtangaben nach Art. 30 Abs. 1 (siehe Tabelle oben). Verwenden Sie eine
einheitliche Vorlage, damit alle Eintraege konsistent und vergleichbar sind.
</p>
<p className="text-gray-600 leading-relaxed">
<span className="font-medium text-gray-800">Praxistipp:</span> Gruppieren Sie die Verarbeitungen
nach Fachabteilungen oder Geschaeftsprozessen. Typische Gruppierungen sind: Personalverwaltung,
Kundenbeziehungsmanagement, Marketingmassnahmen, IT-Betrieb, Finanzen/Buchhaltung und
Kommunikation. Innerhalb jeder Gruppe benennen Sie die konkreten Einzelverarbeitungen.
</p>
</div>
</div>
</div>
{/* Step 3 */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-6 mb-6">
<div className="flex items-start gap-4">
<span className="flex-shrink-0 w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center font-bold text-lg">
3
</span>
<div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">Rechtsgrundlagen und Loeschfristen festlegen</h3>
<p className="text-gray-600 leading-relaxed mb-3">
Bestimmen Sie fuer jede Verarbeitungstaetigkeit die passende Rechtsgrundlage nach Art. 6 Abs. 1
DSGVO. Die gaengigsten sind: Einwilligung (lit. a), Vertragserfullung (lit. b), rechtliche
Verpflichtung (lit. c) und berechtigtes Interesse (lit. f). Dokumentieren Sie fuer jede Rechtsgrundlage
eine konkrete Begruendung.
</p>
<p className="text-gray-600 leading-relaxed">
Legen Sie anschliessend die Loeschfristen fest. Diese ergeben sich aus gesetzlichen
Aufbewahrungspflichten (z.B. 6 Jahre fuer Geschaeftsbriefe nach HGB, 10 Jahre fuer Buchungsbelege
nach AO) und dem Grundsatz der Speicherbegrenzung (Art. 5 Abs. 1 lit. e DSGVO). Wo keine
gesetzliche Aufbewahrungspflicht besteht, sollten Daten nach Zweckerfullung zeitnah geloescht werden.
</p>
</div>
</div>
</div>
{/* Step 4 */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-6 mb-6">
<div className="flex items-start gap-4">
<span className="flex-shrink-0 w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center font-bold text-lg">
4
</span>
<div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">Technisch-organisatorische Massnahmen (TOMs) zuordnen</h3>
<p className="text-gray-600 leading-relaxed mb-3">
Ordnen Sie jeder Verarbeitungstaetigkeit die relevanten Schutzmassnahmen zu. Die TOMs muessen
dem Stand der Technik entsprechen und ein dem Risiko angemessenes Schutzniveau gewaehrleisten
(Art. 32 DSGVO). Unterscheiden Sie zwischen technischen Massnahmen (Verschluesselung,
Pseudonymisierung, Firewalls, Zugriffskontrollen) und organisatorischen Massnahmen
(Schulungen, Vertraulichkeitsvereinbarungen, Vier-Augen-Prinzip, Berechtigungskonzepte).
</p>
<p className="text-gray-600 leading-relaxed">
<span className="font-medium text-gray-800">Praxistipp:</span> Erstellen Sie ein zentrales
TOM-Dokument, auf das Sie im VVT referenzieren koennen. So vermeiden Sie Redundanzen und
stellen sicher, dass Aenderungen an den TOMs automatisch fuer alle betroffenen
Verarbeitungstaetigkeiten gelten.
</p>
</div>
</div>
</div>
{/* Step 5 */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-6 mb-6">
<div className="flex items-start gap-4">
<span className="flex-shrink-0 w-10 h-10 bg-sky-500 text-white rounded-full flex items-center justify-center font-bold text-lg">
5
</span>
<div>
<h3 className="text-xl font-semibold text-gray-800 mb-2">Regelmaessige Ueberpruefung und Aktualisierung</h3>
<p className="text-gray-600 leading-relaxed mb-3">
Ein VVT ist nur nuetzlich, wenn es aktuell ist. Definieren Sie einen festen Review-Zyklus &ndash;
mindestens jaehrlich, idealerweise quartalsweise. Legen Sie ausserdem fest, bei welchen Ereignissen
eine sofortige Aktualisierung erfolgen muss: Einfuehrung neuer Software, Wechsel von Dienstleistern,
organisatorische Umstrukturierungen oder Aenderungen gesetzlicher Rahmenbedingungen.
</p>
<p className="text-gray-600 leading-relaxed">
<span className="font-medium text-gray-800">Praxistipp:</span> Benennen Sie fuer jede
Verarbeitungstaetigkeit einen fachlichen Verantwortlichen, der Aenderungen meldet. Verknuepfen
Sie das VVT-Review mit bestehenden Prozessen wie dem Aenderungsmanagement (Change Management)
oder dem jaehrlichen Audit-Zyklus Ihrer Organisation.
</p>
</div>
</div>
</div>
</section>
{/* Section 5: Haeufige Fehler */}
<section id="haeufige-fehler">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
5. Haeufige Fehler beim VVT &ndash; und wie Sie sie vermeiden
</h2>
<p className="text-gray-600 leading-relaxed mb-6">
Aus unserer Erfahrung in der Beratung von Schulen und Bildungseinrichtungen sehen wir immer
wieder dieselben Stolperfallen. Vermeiden Sie diese vier haeufigen Fehler von Anfang an.
</p>
{/* Mistake 1 */}
<div className="bg-amber-50 border-l-4 border-amber-500 rounded-r-xl p-5 mb-5">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-amber-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<h4 className="font-semibold text-amber-800 mb-1">Fehler 1: VVT nur einmal erstellen und nie aktualisieren</h4>
<p className="text-amber-700 text-sm leading-relaxed">
Viele Organisationen erstellen ein VVT im Rahmen eines Projekts und legen es dann ab. Doch
ein veraltetes VVT ist fast so schlecht wie gar keines &ndash; es taeuscht Compliance vor, die nicht
existiert. Neue Tools, Dienstleister oder Prozesse werden nicht erfasst, und im Ernstfall
stimmt die Dokumentation nicht mit der Realitaet ueberein. Richten Sie automatische
Erinnerungen ein und machen Sie das VVT-Review zum festen Bestandteil Ihres Datenschutzkalenders.
</p>
</div>
</div>
</div>
{/* Mistake 2 */}
<div className="bg-amber-50 border-l-4 border-amber-500 rounded-r-xl p-5 mb-5">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-amber-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<h4 className="font-semibold text-amber-800 mb-1">Fehler 2: Zu grobe Granularitaet der Verarbeitungen</h4>
<p className="text-amber-700 text-sm leading-relaxed">
Ein Eintrag wie &laquo;Personalwesen&raquo; ist viel zu allgemein. Unterschiedliche Verarbeitungstaetigkeiten
innerhalb einer Abteilung haben oft unterschiedliche Rechtsgrundlagen, Loeschfristen und
Empfaengerkreise. Trennen Sie zum Beispiel: Bewerbermanagement (Rechtsgrundlage: Art. 6 Abs. 1 lit. b,
Loeschfrist: 6 Monate), Lohnabrechnung (Rechtsgrundlage: Art. 6 Abs. 1 lit. c, Loeschfrist: 10 Jahre)
und Zeiterfassung (Rechtsgrundlage: Art. 6 Abs. 1 lit. b, Loeschfrist: 2 Jahre).
</p>
</div>
</div>
</div>
{/* Mistake 3 */}
<div className="bg-amber-50 border-l-4 border-amber-500 rounded-r-xl p-5 mb-5">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-amber-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<h4 className="font-semibold text-amber-800 mb-1">Fehler 3: Drittlandsuebermittlungen uebersehen</h4>
<p className="text-amber-700 text-sm leading-relaxed">
Cloud-Dienste wie Google Workspace, Microsoft 365 oder Zoom uebermitteln Daten in Drittlaender
(insbesondere die USA). Diese Uebermittlungen muessen im VVT dokumentiert und mit geeigneten
Garantien abgesichert werden &ndash; etwa durch Standardvertragsklauseln (SCCs) oder einen
Angemessenheitsbeschluss. Pruefen Sie fuer jeden eingesetzten Dienstleister, wo die Daten
tatsaechlich gespeichert und verarbeitet werden, und halten Sie die Ergebnisse im VVT fest.
</p>
</div>
</div>
</div>
{/* Mistake 4 */}
<div className="bg-amber-50 border-l-4 border-amber-500 rounded-r-xl p-5 mb-5">
<div className="flex items-start gap-3">
<svg className="w-6 h-6 text-amber-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-2.5L13.732 4c-.77-.833-1.964-.833-2.732 0L4.082 16.5c-.77.833.192 2.5 1.732 2.5z" />
</svg>
<div>
<h4 className="font-semibold text-amber-800 mb-1">Fehler 4: Fehlende Einbindung der Fachabteilungen</h4>
<p className="text-amber-700 text-sm leading-relaxed">
Das VVT darf kein reines Datenschutz-Projekt sein. Wenn nur der DSB oder die IT-Abteilung
das Verzeichnis pflegt, werden zwangslaeufig Verarbeitungen uebersehen. Jede Fachabteilung
kennt ihre eigenen Prozesse am besten und muss aktiv in die Erstellung und Pflege eingebunden
werden. Benennen Sie Datenschutzkoordinatoren in jeder Abteilung, die als Ansprechpartner
fuer VVT-relevante Aenderungen dienen.
</p>
</div>
</div>
</div>
</section>
{/* Section 6: VVT-Vorlage */}
<section id="vvt-vorlage">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
6. VVT-Vorlage mit Beispieleintraegen
</h2>
<p className="text-gray-600 leading-relaxed mb-6">
Die folgende Tabelle zeigt drei beispielhafte Eintraege, wie sie in einem VVT einer
Bildungseinrichtung aussehen koennten. Nutzen Sie dieses Format als Ausgangspunkt fuer
Ihr eigenes Verzeichnis.
</p>
<div className="overflow-x-auto mb-6">
<table className="w-full border-collapse text-sm">
<thead>
<tr className="bg-gray-900 text-white">
<th className="text-left px-3 py-3 font-semibold rounded-tl-lg whitespace-nowrap">Feld</th>
<th className="text-left px-3 py-3 font-semibold whitespace-nowrap">Schueleranmeldung</th>
<th className="text-left px-3 py-3 font-semibold whitespace-nowrap">Lohn- und Gehaltsabrechnung</th>
<th className="text-left px-3 py-3 font-semibold rounded-tr-lg whitespace-nowrap">Website-Betrieb</th>
</tr>
</thead>
<tbody>
<tr className="border-b border-gray-100">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Bezeichnung</td>
<td className="px-3 py-3 text-gray-600">Anmeldung und Verwaltung von Schuelerdaten</td>
<td className="px-3 py-3 text-gray-600">Abrechnung und Auszahlung von Gehaeltern</td>
<td className="px-3 py-3 text-gray-600">Betrieb der oeffentlichen Website</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Verantwortlicher</td>
<td className="px-3 py-3 text-gray-600">Musterschule, Schulstr. 5, 30159 Hannover</td>
<td className="px-3 py-3 text-gray-600">Musterschule, Schulstr. 5, 30159 Hannover</td>
<td className="px-3 py-3 text-gray-600">Musterschule, Schulstr. 5, 30159 Hannover</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Zweck</td>
<td className="px-3 py-3 text-gray-600">Erfuellung der Schulpflicht, Verwaltung des Schulverhaeltnisses</td>
<td className="px-3 py-3 text-gray-600">Durchfuehrung der Beschaeftigungsverhaeltnisse</td>
<td className="px-3 py-3 text-gray-600">Oeffentlichkeitsarbeit, Information der Schulgemeinschaft</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Rechtsgrundlage</td>
<td className="px-3 py-3 text-gray-600">Art. 6 Abs. 1 lit. e DSGVO i.V.m. NSchG</td>
<td className="px-3 py-3 text-gray-600">Art. 6 Abs. 1 lit. b, c DSGVO i.V.m. EntgFG, SGB IV</td>
<td className="px-3 py-3 text-gray-600">Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Betroffene</td>
<td className="px-3 py-3 text-gray-600">Schueler, Erziehungsberechtigte</td>
<td className="px-3 py-3 text-gray-600">Lehrkraefte, Verwaltungspersonal</td>
<td className="px-3 py-3 text-gray-600">Website-Besucher</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Datenkategorien</td>
<td className="px-3 py-3 text-gray-600">Name, Geburtsdatum, Adresse, Noten, Fehlzeiten</td>
<td className="px-3 py-3 text-gray-600">Stammdaten, Bankverbindung, Steuer-ID, SV-Nummer</td>
<td className="px-3 py-3 text-gray-600">IP-Adresse, Browser-Typ, Zugriffszeitpunkte</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Empfaenger</td>
<td className="px-3 py-3 text-gray-600">Schulbehoerde, aufnehmende Schule bei Wechsel</td>
<td className="px-3 py-3 text-gray-600">Finanzamt, Krankenkasse, Rentenversicherung</td>
<td className="px-3 py-3 text-gray-600">Hosting-Anbieter (AV-Vertrag)</td>
</tr>
<tr className="border-b border-gray-100 bg-gray-50">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Drittlandtransfer</td>
<td className="px-3 py-3 text-gray-600">Nein</td>
<td className="px-3 py-3 text-gray-600">Nein</td>
<td className="px-3 py-3 text-gray-600">Nein (EU-Hosting)</td>
</tr>
<tr className="border-b border-gray-100">
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap">Loeschfrist</td>
<td className="px-3 py-3 text-gray-600">5 Jahre nach Verlassen der Schule</td>
<td className="px-3 py-3 text-gray-600">10 Jahre (Aufbewahrungspflicht AO)</td>
<td className="px-3 py-3 text-gray-600">7 Tage (Serverlogfiles)</td>
</tr>
<tr>
<td className="px-3 py-3 font-medium text-gray-800 whitespace-nowrap rounded-bl-lg">TOMs</td>
<td className="px-3 py-3 text-gray-600">Rollenbasierte Zugriffskontrolle, verschluesselte Datenbank, Backup</td>
<td className="px-3 py-3 text-gray-600">Verschluesselung, Zugriffsbeschraenkung auf HR, Vier-Augen-Prinzip</td>
<td className="px-3 py-3 text-gray-600 rounded-br-lg">TLS-Verschluesselung, automatische Log-Rotation</td>
</tr>
</tbody>
</table>
</div>
{/* Tip Box */}
<div className="bg-sky-50 border-l-4 border-sky-500 rounded-r-xl p-5 mb-6">
<div className="flex items-start gap-3">
<svg className="w-5 h-5 text-sky-500 flex-shrink-0 mt-0.5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
</svg>
<div>
<h4 className="font-semibold text-sky-800 mb-1">Tabellenformat vs. Einzeldokumente</h4>
<p className="text-sky-700 text-sm leading-relaxed">
Fuer kleine Organisationen mit weniger als 20 Verarbeitungen reicht eine Tabellenkalkulation
(z.B. LibreOffice Calc) oft aus. Ab einer gewissen Groesse empfiehlt sich jedoch eine
spezialisierte Software, die Zusammenhaenge zwischen Verarbeitungen, TOMs und Auftragsverarbeitern
automatisch verknuepft und Aenderungen versioniert.
</p>
</div>
</div>
</div>
</section>
{/* Section 7: Automatisierung */}
<section id="automatisierung">
<h2 className="text-2xl font-bold text-gray-900 mt-12 mb-4">
7. VVT automatisiert verwalten mit BreakPilot Comply
</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Die manuelle Pflege eines VVT in Tabellenkalkulationen wird mit wachsender Organisationsgroesse
schnell unuebersichtlich und fehleranfaellig. BreakPilot Comply bietet eine digitale Loesung, die
speziell fuer Bildungseinrichtungen und KMU im DACH-Raum entwickelt wurde.
</p>
<div className="bg-gradient-to-br from-sky-50 to-fuchsia-50 border border-sky-200 rounded-2xl p-8 mb-6">
<h3 className="text-xl font-semibold text-gray-800 mb-4">
So unterstuetzt BreakPilot Comply Ihr VVT
</h3>
<ul className="space-y-3">
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-sky-500 text-white rounded-full flex items-center justify-center text-sm font-bold mt-0.5">
&#10003;
</span>
<div>
<span className="font-medium text-gray-800">Vorgefertigte Vorlagen fuer Bildungseinrichtungen:</span>
<span className="text-gray-600"> Starten Sie mit branchenspezifischen Vorlagen, die typische Verarbeitungen
wie Schueleranmeldung, Notenverwaltung, Lernplattformen und Kommunikationssysteme bereits enthalten.</span>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-sky-500 text-white rounded-full flex items-center justify-center text-sm font-bold mt-0.5">
&#10003;
</span>
<div>
<span className="font-medium text-gray-800">Automatische Verknuepfung mit TOM und DSFA:</span>
<span className="text-gray-600"> Technisch-organisatorische Massnahmen und Datenschutz-Folgenabschaetzungen
werden direkt mit den relevanten VVT-Eintraegen verknuepft &ndash; Aenderungen propagieren automatisch.</span>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-sky-500 text-white rounded-full flex items-center justify-center text-sm font-bold mt-0.5">
&#10003;
</span>
<div>
<span className="font-medium text-gray-800">Loeschfristen-Management:</span>
<span className="text-gray-600"> Automatische Erinnerungen, wenn Loeschfristen ablaufen. Sie behalten
den Ueberblick ueber alle Aufbewahrungsfristen und koennen die Loeschung dokumentiert nachweisen.</span>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-sky-500 text-white rounded-full flex items-center justify-center text-sm font-bold mt-0.5">
&#10003;
</span>
<div>
<span className="font-medium text-gray-800">Audit-Trail und Versionierung:</span>
<span className="text-gray-600"> Jede Aenderung am VVT wird mit Zeitstempel und Bearbeiter protokolliert.
Bei einer Pruefung durch die Aufsichtsbehoerde koennen Sie jederzeit nachweisen, wann welche
Anpassung vorgenommen wurde.</span>
</div>
</li>
<li className="flex items-start gap-3">
<span className="flex-shrink-0 w-6 h-6 bg-sky-500 text-white rounded-full flex items-center justify-center text-sm font-bold mt-0.5">
&#10003;
</span>
<div>
<span className="font-medium text-gray-800">DSGVO-konformes Hosting in Deutschland:</span>
<span className="text-gray-600"> Alle Daten werden ausschliesslich auf Servern in Deutschland
verarbeitet und gespeichert. Kein Drittlandtransfer, keine US-Cloud-Abhaengigkeit.</span>
</div>
</li>
</ul>
{/* CTA */}
<div className="mt-8 flex flex-col sm:flex-row items-start sm:items-center gap-4">
<a
href="/kontakt"
className="inline-flex items-center px-6 py-3 bg-sky-500 hover:bg-sky-600 text-white font-semibold rounded-xl transition-colors shadow-lg shadow-sky-500/25"
>
Kostenlose Demo vereinbaren
<svg className="w-5 h-5 ml-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 8l4 4m0 0l-4 4m4-4H3" />
</svg>
</a>
<span className="text-sm text-gray-500">Unverbindlich &ndash; kein Vertrag erforderlich</span>
</div>
</div>
</section>
{/* Conclusion */}
<section className="mt-16 pt-8 border-t border-gray-200">
<h2 className="text-2xl font-bold text-gray-900 mb-4">Fazit</h2>
<p className="text-gray-600 leading-relaxed mb-4">
Das Verzeichnis von Verarbeitungstaetigkeiten ist weit mehr als eine laestige Pflichtaufgabe &ndash;
es ist das Fundament Ihres gesamten Datenschutzmanagements. Ein sorgfaeltig gepflegtes VVT
verschafft Ihnen Transparenz ueber alle Datenverarbeitungen in Ihrer Organisation, erleichtert
die Beantwortung von Betroffenenanfragen und schafft Vertrauen bei Aufsichtsbehoerden.
</p>
<p className="text-gray-600 leading-relaxed mb-4">
Beginnen Sie mit der Bestandsaufnahme, dokumentieren Sie systematisch und etablieren Sie
einen festen Aktualisierungsrhythmus. Mit den richtigen Werkzeugen und Prozessen wird das
VVT vom buerokratischen Aufwand zum echten Mehrwert fuer Ihre Organisation.
</p>
</section>
{/* Legal Disclaimer */}
<div className="mt-12 bg-gray-50 border border-gray-200 rounded-xl p-6">
<p className="text-gray-500 text-sm leading-relaxed">
<span className="font-semibold">Hinweis:</span> Dieser Artikel dient der allgemeinen Information und
stellt keine Rechtsberatung dar. Die Inhalte wurden mit groesster Sorgfalt erstellt, ersetzen
jedoch nicht die individuelle Beratung durch einen Datenschutzbeauftragten oder Rechtsanwalt.
Fuer die Richtigkeit, Vollstaendigkeit und Aktualitaet wird keine Gewaehr uebernommen.
Stand: Februar 2026.
</p>
</div>
</article>
</div>
)
}

View File

@@ -0,0 +1,396 @@
import { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = {
title: 'Compliance-Glossar | BreakPilot Comply',
description: 'Die wichtigsten Begriffe aus DSGVO, AI Act, NIS2 und IT-Compliance verstaendlich erklaert.',
}
const letters = ['A', 'B', 'C', 'D', 'E', 'G', 'H', 'I', 'L', 'N', 'P', 'R', 'T', 'V'] as const
interface GlossaryTerm {
term: string
definition: string
reference?: string
}
interface GlossarySection {
letter: string
terms: GlossaryTerm[]
}
const glossary: GlossarySection[] = [
{
letter: 'A',
terms: [
{
term: 'Auftragsverarbeitung (AVV)',
definition:
'Die Verarbeitung personenbezogener Daten durch einen Dienstleister (Auftragsverarbeiter) im Auftrag des Verantwortlichen. Zwischen beiden Parteien muss ein Auftragsverarbeitungsvertrag (AVV) geschlossen werden, der die Pflichten des Auftragsverarbeiters verbindlich regelt. Der Verantwortliche bleibt datenschutzrechtlich fuer die Verarbeitung verantwortlich.',
reference: 'Art. 28 DSGVO',
},
{
term: 'Aufsichtsbehoerde',
definition:
'Unabhaengige staatliche Stelle, die fuer die Ueberwachung der Einhaltung datenschutzrechtlicher Vorschriften zustaendig ist. In Deutschland gibt es auf Landesebene Datenschutzbehoerden (z.B. BayLDA, LDI NRW) sowie den Bundesbeauftragten fuer den Datenschutz und die Informationsfreiheit (BfDI) auf Bundesebene. Die Aufsichtsbehoerden koennen Verwarnungen aussprechen, Anweisungen erteilen und Bussgelder verhaengen.',
reference: 'Art. 51-59 DSGVO',
},
{
term: 'Angemessenheitsbeschluss',
definition:
'Eine Entscheidung der Europaeischen Kommission, dass ein Drittland (ein Land ausserhalb des EWR) ein angemessenes Datenschutzniveau bietet. Liegt ein solcher Beschluss vor, koennen personenbezogene Daten ohne zusaetzliche Schutzgarantien in dieses Land uebermittelt werden. Bekannte Beispiele sind der EU-US Data Privacy Framework und Beschluesse fuer Laender wie Japan, die Schweiz oder das Vereinigte Koenigreich.',
reference: 'Art. 45 DSGVO',
},
],
},
{
letter: 'B',
terms: [
{
term: 'Betroffenenrechte',
definition:
'Die in der DSGVO verankerten Rechte natuerlicher Personen, deren personenbezogene Daten verarbeitet werden. Dazu gehoeren das Recht auf Auskunft, Berichtigung, Loeschung (Recht auf Vergessenwerden), Einschraenkung der Verarbeitung, Datenportabilitaet und Widerspruch. Verantwortliche muessen diese Rechte innerhalb eines Monats nach Eingang des Antrags erfuellen.',
reference: 'Art. 12-23 DSGVO',
},
{
term: 'BSI (Bundesamt fuer Sicherheit in der Informationstechnik)',
definition:
'Die zentrale deutsche Bundesbehoerde fuer Cybersicherheit. Das BSI entwickelt Sicherheitsstandards (z.B. IT-Grundschutz), gibt Empfehlungen heraus und ist im Rahmen der NIS2-Umsetzung die zustaendige Behoerde fuer die Aufsicht ueber Betreiber kritischer Infrastrukturen und wesentliche Einrichtungen in Deutschland. Es nimmt auch Meldungen ueber Sicherheitsvorfaelle entgegen.',
reference: 'BSIG (BSI-Gesetz)',
},
],
},
{
letter: 'C',
terms: [
{
term: 'Compliance',
definition:
'Die Gesamtheit aller Massnahmen eines Unternehmens zur Einhaltung geltender Gesetze, Verordnungen, Richtlinien und interner Vorgaben. Im Datenschutz- und IT-Sicherheitskontext umfasst Compliance insbesondere die Einhaltung der DSGVO, des AI Acts, der NIS2-Richtlinie sowie branchenspezifischer Regulierungen. Compliance erfordert sowohl technische als auch organisatorische Massnahmen.',
},
{
term: 'Cookie-Consent',
definition:
'Die informierte Einwilligung des Nutzers in das Setzen von Cookies und aehnlichen Tracking-Technologien auf dessen Endgeraet. Technisch notwendige Cookies benoetigen keine Einwilligung, waehrend fuer Marketing-, Analyse- und Tracking-Cookies eine vorherige, aktive Zustimmung erforderlich ist. Die Einwilligung muss freiwillig, spezifisch, informiert und eindeutig sein.',
reference: 'Art. 5 Abs. 3 ePrivacy-Richtlinie, TTDSG (DE)',
},
],
},
{
letter: 'D',
terms: [
{
term: 'DSFA (Datenschutz-Folgenabschaetzung)',
definition:
'Eine strukturierte Risikoanalyse, die durchgefuehrt werden muss, wenn eine geplante Datenverarbeitung voraussichtlich ein hohes Risiko fuer die Rechte und Freiheiten natuerlicher Personen mit sich bringt. Die DSFA beschreibt die Verarbeitungsvorgaenge, bewertet die Notwendigkeit und Verhaeltnismaessigkeit und identifiziert Massnahmen zur Risikominderung. Sie ist vor Beginn der Verarbeitung durchzufuehren.',
reference: 'Art. 35 DSGVO',
},
{
term: 'DSB (Datenschutzbeauftragter)',
definition:
'Eine Person, die ein Unternehmen oder eine oeffentliche Stelle in Fragen des Datenschutzes beratet und die Einhaltung der DSGVO ueberwacht. In Deutschland ist ein DSB zu benennen, wenn mindestens 20 Personen staendig mit der automatisierten Verarbeitung personenbezogener Daten beschaeftigt sind, oder wenn die Kerntaetigkeit des Unternehmens in der umfangreichen Verarbeitung besonderer Datenkategorien besteht.',
reference: 'Art. 37-39 DSGVO, Paragraph 38 BDSG',
},
{
term: 'DSGVO (Datenschutz-Grundverordnung)',
definition:
'Die seit Mai 2018 geltende europaeische Verordnung (EU 2016/679) zum Schutz natuerlicher Personen bei der Verarbeitung personenbezogener Daten. Die DSGVO harmonisiert das Datenschutzrecht in der EU und gilt unmittelbar in allen Mitgliedstaaten. Sie regelt unter anderem die Grundsaetze der Verarbeitung, Betroffenenrechte, Pflichten der Verantwortlichen und Bussgelder bei Verstoessen.',
reference: 'Verordnung (EU) 2016/679',
},
{
term: 'Datenpanne',
definition:
'Eine Verletzung des Schutzes personenbezogener Daten, die zur unbeabsichtigten oder unrechtmaessigen Vernichtung, zum Verlust, zur Veraenderung oder zur unbefugten Offenlegung bzw. zum unbefugten Zugang zu personenbezogenen Daten fuehrt. Datenpannen muessen innerhalb von 72 Stunden an die zustaendige Aufsichtsbehoerde gemeldet werden, sofern ein Risiko fuer die Rechte und Freiheiten der betroffenen Personen besteht.',
reference: 'Art. 33-34 DSGVO',
},
],
},
{
letter: 'E',
terms: [
{
term: 'Einwilligung',
definition:
'Eine der Rechtsgrundlagen fuer die Verarbeitung personenbezogener Daten. Die Einwilligung muss freiwillig, fuer den bestimmten Fall, informiert und in Form einer eindeutigen bestaetigen Handlung erteilt werden. Sie kann jederzeit widerrufen werden, wobei die Rechtmaessigkeit der bis zum Widerruf erfolgten Verarbeitung unberuehrt bleibt. Fuer besondere Datenkategorien ist eine ausdrueckliche Einwilligung erforderlich.',
reference: 'Art. 6 Abs. 1 lit. a, Art. 7 DSGVO',
},
{
term: 'Empfaenger',
definition:
'Jede natuerliche oder juristische Person, Behoerde, Einrichtung oder andere Stelle, der personenbezogene Daten offengelegt werden. Dazu zaehlen auch Auftragsverarbeiter. Behoerden, die im Rahmen eines bestimmten Untersuchungsauftrags Daten erhalten, gelten nicht als Empfaenger. Die Information ueber Empfaenger gehoert zu den Pflichtangaben in der Datenschutzerklaerung.',
reference: 'Art. 4 Nr. 9 DSGVO',
},
],
},
{
letter: 'G',
terms: [
{
term: 'GPAI (General Purpose AI)',
definition:
'KI-Systeme fuer allgemeine Zwecke, die ein breites Spektrum an Aufgaben erfuellen koennen, fuer die sie nicht speziell entwickelt wurden. Der EU AI Act sieht spezielle Vorschriften fuer GPAI-Modelle vor, darunter Transparenzpflichten (z.B. Dokumentation der Trainingsdaten), Urheberrechtskonformitaet und bei systemischen Risiken erweiterte Pflichten wie Red-Teaming und Modellbewertungen. Prominente Beispiele sind GPT-4, Claude oder Gemini.',
reference: 'AI Act Kapitel V, Art. 52a-52c',
},
{
term: 'Grundsaetze der Verarbeitung',
definition:
'Die in Art. 5 DSGVO definierten fundamentalen Prinzipien fuer die Verarbeitung personenbezogener Daten: Rechtmaessigkeit, Verarbeitung nach Treu und Glauben, Transparenz, Zweckbindung, Datenminimierung, Richtigkeit, Speicherbegrenzung, Integritaet und Vertraulichkeit sowie Rechenschaftspflicht. Jede Verarbeitung muss diesen Grundsaetzen genuegen.',
reference: 'Art. 5 DSGVO',
},
],
},
{
letter: 'H',
terms: [
{
term: 'Hochrisiko-KI',
definition:
'KI-Systeme, die im Rahmen des EU AI Acts als hochriskant eingestuft werden, weil sie erhebliche Auswirkungen auf Grundrechte haben koennen. Dazu gehoeren KI-Systeme im Bildungsbereich (z.B. Pruefungsbewertung, Zugang zu Bildungseinrichtungen), in der Personalverwaltung, bei der Kreditwuerdigkeitspruefung und in der Strafverfolgung. Fuer Hochrisiko-KI gelten strenge Anforderungen an Risikomanagement, Datenqualitaet, Transparenz und menschliche Aufsicht.',
reference: 'AI Act Anhang III, Art. 6-15',
},
{
term: 'Hinweisgeberschutzgesetz (HinSchG)',
definition:
'Das deutsche Gesetz zur Umsetzung der EU-Whistleblower-Richtlinie (2019/1937). Es schuetzt Personen, die Verstoesse gegen bestimmte Rechtsvorschriften melden, vor Repressalien. Unternehmen mit 50 oder mehr Beschaeftigten muessen interne Meldekanale einrichten, ueber die Hinweisgeber Missstaende vertraulich melden koennen. Im Compliance-Kontext ist das HinSchG wichtig, weil Datenschutzverstoesse und IT-Sicherheitsvorfaelle zu den meldewuerdigen Verstoessen zaehlen.',
reference: 'HinSchG (Hinweisgeberschutzgesetz), EU-RL 2019/1937',
},
],
},
{
letter: 'I',
terms: [
{
term: 'ISMS (Information Security Management System)',
definition:
'Ein systematischer Ansatz zum Management von Informationssicherheit in einer Organisation. Ein ISMS umfasst Richtlinien, Prozesse, Verfahren und Technologien zur Identifizierung, Bewertung und Behandlung von Informationssicherheitsrisiken. Der international anerkannte Standard hierfuer ist ISO/IEC 27001. Im Kontext der NIS2-Richtlinie bildet ein ISMS haeufig die Grundlage fuer die Erfuellung der Cybersicherheitsanforderungen.',
reference: 'ISO/IEC 27001, NIS2 Art. 21',
},
{
term: 'Incident (Sicherheitsvorfall)',
definition:
'Ein Ereignis, das die Vertraulichkeit, Integritaet oder Verfuegbarkeit von Informationssystemen oder Daten beeintraechtigt oder beeintraechtigen kann. Im Rahmen der NIS2-Richtlinie muessen erhebliche Sicherheitsvorfaelle innerhalb von 24 Stunden (Fruehwarnung), 72 Stunden (detaillierte Meldung) und einem Monat (Abschlussbericht) an die zustaendige Behoerde gemeldet werden.',
reference: 'NIS2 Art. 23, Art. 33 DSGVO (Datenpannen)',
},
],
},
{
letter: 'L',
terms: [
{
term: 'Loeschkonzept',
definition:
'Ein dokumentiertes Verfahren, das festlegt, wann und wie personenbezogene Daten geloescht oder anonymisiert werden. Das Loeschkonzept orientiert sich am Grundsatz der Speicherbegrenzung und definiert fuer jede Datenkategorie konkrete Aufbewahrungsfristen und Loeschroutinen. Es beruecksichtigt dabei auch gesetzliche Aufbewahrungspflichten (z.B. handels- und steuerrechtliche Fristen).',
reference: 'Art. 5 Abs. 1 lit. e, Art. 17 DSGVO',
},
{
term: 'Loeschfrist',
definition:
'Der definierte Zeitraum, nach dem personenbezogene Daten geloescht oder anonymisiert werden muessen, sobald der Verarbeitungszweck entfallen ist und keine gesetzlichen Aufbewahrungspflichten entgegenstehen. Typische Loeschfristen sind z.B. 3 Jahre fuer Vertragsdaten nach Vertragsende, 6 Jahre fuer Handelsbriefe oder 10 Jahre fuer Buchungsbelege.',
reference: 'Art. 5 Abs. 1 lit. e DSGVO, Paragraph 257 HGB, Paragraph 147 AO',
},
],
},
{
letter: 'N',
terms: [
{
term: 'NIS2 (Network and Information Security Directive 2)',
definition:
'Die EU-Richtlinie 2022/2555 ueber Massnahmen fuer ein hohes gemeinsames Cybersicherheitsniveau in der Union. NIS2 erweitert den Anwendungsbereich der urspruenglichen NIS-Richtlinie erheblich und erfasst nun 18 Sektoren. Sie definiert Mindestanforderungen an das Cybersicherheits-Risikomanagement, Meldepflichten fuer Vorfaelle und Sanktionen. Die nationale Umsetzung erfolgt in Deutschland durch das NIS2UmsuCG.',
reference: 'EU-Richtlinie 2022/2555',
},
{
term: 'Notfallplan (Incident Response Plan)',
definition:
'Ein dokumentierter Plan, der die Vorgehensweise bei Sicherheitsvorfaellen oder IT-Notfaellen beschreibt. Er umfasst Erkennungsmechanismen, Eskalationswege, Kommunikationsplaene, Eindaemmungsmassnahmen und Wiederherstellungsverfahren. Im Rahmen von NIS2 und DSGVO ist ein funktionierender Notfallplan essenziell, um Meldepflichten einzuhalten und Schaeden zu minimieren.',
reference: 'NIS2 Art. 21 Abs. 2 lit. b und c',
},
],
},
{
letter: 'P',
terms: [
{
term: 'Privacy by Design',
definition:
'Der Grundsatz, dass der Schutz personenbezogener Daten bereits bei der Konzeption und Entwicklung von Systemen, Produkten und Dienstleistungen beruecksichtigt werden muss. Datenschutzfreundliche Voreinstellungen (Privacy by Default) gehoeren ebenfalls dazu. In der Praxis bedeutet dies, dass z.B. bei der Softwareentwicklung nur die fuer den jeweiligen Zweck erforderlichen Daten erhoben und verarbeitet werden.',
reference: 'Art. 25 DSGVO',
},
{
term: 'PII (Personally Identifiable Information)',
definition:
'Ein vorwiegend im angloamerikanischen Raum verwendeter Begriff fuer Informationen, die eine natuerliche Person direkt oder indirekt identifizieren koennen. Beispiele sind Name, E-Mail-Adresse, Sozialversicherungsnummer, IP-Adresse oder biometrische Daten. Im EU-Kontext entspricht der Begriff weitgehend den personenbezogenen Daten im Sinne der DSGVO.',
reference: 'Vgl. Art. 4 Nr. 1 DSGVO (personenbezogene Daten)',
},
],
},
{
letter: 'R',
terms: [
{
term: 'Risikobewertung',
definition:
'Die systematische Identifizierung, Analyse und Bewertung von Risiken fuer die Sicherheit personenbezogener Daten bzw. von Informationssystemen. Im Datenschutzkontext ist die Risikobewertung Grundlage fuer die Auswahl angemessener technischer und organisatorischer Massnahmen. Im NIS2-Kontext bildet sie den Ausgangspunkt fuer das gesamte Cybersicherheits-Risikomanagement.',
reference: 'Art. 32 DSGVO, NIS2 Art. 21 Abs. 2 lit. a',
},
{
term: 'Rechenschaftspflicht (Accountability)',
definition:
'Die Pflicht des Verantwortlichen, die Einhaltung aller Datenschutzgrundsaetze nicht nur sicherzustellen, sondern auch nachweisen zu koennen. Dies erfordert eine umfassende Dokumentation aller Verarbeitungstaetigkeiten, Sicherheitsmassnahmen, Einwilligungen und Datenschutz-Folgenabschaetzungen. Die Rechenschaftspflicht kehrt die Beweislast um: Der Verantwortliche muss belegen, dass er datenschutzkonform handelt.',
reference: 'Art. 5 Abs. 2 DSGVO',
},
],
},
{
letter: 'T',
terms: [
{
term: 'TOM (Technische und Organisatorische Massnahmen)',
definition:
'Massnahmen, die Verantwortliche und Auftragsverarbeiter ergreifen muessen, um ein dem Risiko angemessenes Schutzniveau fuer personenbezogene Daten zu gewaehrleisten. Technische Massnahmen umfassen z.B. Verschluesselung, Zugriffskontrollen, Firewalls und Pseudonymisierung. Organisatorische Massnahmen beinhalten z.B. Schulungen, Richtlinien, Zugangsberechtigungskonzepte und regelmaessige Audits. Die TOM muessen dokumentiert und regelmaessig ueberprueft werden.',
reference: 'Art. 32 DSGVO',
},
{
term: 'Transparenzpflicht',
definition:
'Die Verpflichtung des Verantwortlichen, betroffene Personen in praeziser, transparenter, verstaendlicher und leicht zugaenglicher Form ueber die Verarbeitung ihrer personenbezogenen Daten zu informieren. Im KI-Kontext verlangt der AI Act zusaetzliche Transparenz: Nutzer muessen informiert werden, wenn sie mit einem KI-System interagieren, und bei Hochrisiko-KI-Systemen muessen ausfuehrliche Informationen zur Funktionsweise bereitgestellt werden.',
reference: 'Art. 12-14 DSGVO, AI Act Art. 52',
},
],
},
{
letter: 'V',
terms: [
{
term: 'VVT (Verarbeitungsverzeichnis / Verzeichnis von Verarbeitungstaetigkeiten)',
definition:
'Ein zentrales Dokumentationsinstrument der DSGVO, in dem alle Verarbeitungstaetigkeiten des Verantwortlichen systematisch erfasst werden. Das VVT muss unter anderem die Zwecke der Verarbeitung, die Kategorien betroffener Personen und personenbezogener Daten, Empfaenger, Uebermittlungen in Drittlaender, vorgesehene Loeschfristen und technisch-organisatorische Massnahmen dokumentieren. Es muss der Aufsichtsbehoerde auf Anfrage vorgelegt werden koennen.',
reference: 'Art. 30 DSGVO',
},
{
term: 'Verantwortlicher',
definition:
'Die natuerliche oder juristische Person, Behoerde, Einrichtung oder andere Stelle, die allein oder gemeinsam mit anderen ueber die Zwecke und Mittel der Verarbeitung personenbezogener Daten entscheidet. Der Verantwortliche traegt die Hauptverantwortung fuer die Einhaltung der DSGVO und muss dies gegenueber der Aufsichtsbehoerde nachweisen koennen. Bei gemeinsamer Verantwortlichkeit muss eine Vereinbarung ueber die jeweiligen Pflichten getroffen werden.',
reference: 'Art. 4 Nr. 7, Art. 26 DSGVO',
},
],
},
]
export default function GlossarPage() {
return (
<div className="min-h-screen bg-white">
{/* Hero */}
<section className="pt-12 pb-12 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-primary-50 to-white">
<div className="max-w-4xl mx-auto text-center">
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-accent-100 text-accent-700 rounded-full text-sm font-medium mb-6">
DSGVO &middot; AI Act &middot; NIS2
</div>
<h1 className="text-4xl sm:text-5xl font-bold text-slate-900 tracking-tight">
Compliance-Glossar
</h1>
<p className="mt-6 text-xl text-slate-600 max-w-2xl mx-auto">
Die wichtigsten Begriffe aus Datenschutz, KI-Regulierung, Cybersicherheit und IT-Compliance &ndash; verstaendlich erklaert.
</p>
</div>
</section>
{/* Alphabet Navigation */}
<section className="py-6 px-4 sm:px-6 lg:px-8 border-b border-slate-100 sticky top-16 bg-white/90 backdrop-blur-md z-40">
<div className="max-w-3xl mx-auto">
<nav className="flex flex-wrap justify-center gap-2">
{letters.map((letter) => (
<a
key={letter}
href={`#letter-${letter}`}
className="w-10 h-10 flex items-center justify-center rounded-lg text-sm font-bold text-primary-600 hover:bg-primary-50 hover:text-primary-800 transition-all"
>
{letter}
</a>
))}
</nav>
</div>
</section>
{/* Glossary Content */}
<article className="py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="space-y-12">
{glossary.map((section) => (
<section key={section.letter} id={`letter-${section.letter}`}>
{/* Letter Header */}
<div className="flex items-center gap-4 mb-6">
<div className="w-14 h-14 bg-gradient-to-br from-primary-500 to-accent-500 rounded-2xl flex items-center justify-center">
<span className="text-2xl font-bold text-white">{section.letter}</span>
</div>
<div className="flex-1 h-px bg-slate-200" />
</div>
{/* Terms */}
<div className="space-y-4">
{section.terms.map((entry) => (
<div
key={entry.term}
className="bg-slate-50 rounded-xl p-6 border border-slate-100 hover:border-primary-200 hover:shadow-sm transition-all"
>
<h3 className="text-lg font-bold text-slate-900 mb-2">
{entry.term}
</h3>
<p className="text-slate-600 leading-relaxed text-sm">
{entry.definition}
</p>
{entry.reference && (
<p className="mt-3 text-xs italic text-slate-400">
Rechtsgrundlage: {entry.reference}
</p>
)}
</div>
))}
</div>
</section>
))}
</div>
</div>
</article>
{/* CTA Section */}
<section className="py-16 px-4 sm:px-6 lg:px-8 bg-gradient-to-br from-primary-50 to-accent-50">
<div className="max-w-3xl mx-auto text-center">
<h2 className="text-2xl font-bold text-slate-900 mb-4">
Compliance strukturiert umsetzen
</h2>
<p className="text-slate-600 mb-8 max-w-xl mx-auto">
BreakPilot Comply hilft Ihnen, die Anforderungen aus DSGVO, AI Act und NIS2 mit strukturierten Workflows und automatisierter Dokumentation umzusetzen.
</p>
<div className="flex flex-col sm:flex-row gap-4 justify-center">
<Link
href="/blog/nis2-checkliste"
className="inline-flex items-center justify-center px-6 py-3 rounded-xl bg-primary-600 text-white font-medium hover:bg-primary-700 transition-all"
>
NIS2-Checkliste lesen
</Link>
<Link
href="/blog"
className="inline-flex items-center justify-center px-6 py-3 rounded-xl bg-white text-slate-700 font-medium hover:bg-slate-100 transition-all border border-slate-200"
>
Alle Artikel
</Link>
</div>
</div>
</section>
{/* Back to Blog */}
<section className="py-8 px-4 sm:px-6 lg:px-8 border-t border-slate-100">
<div className="max-w-3xl mx-auto">
<Link href="/blog" className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-800 transition-colors text-sm font-medium">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zum Blog
</Link>
</div>
</section>
</div>
)
}

View File

@@ -0,0 +1,73 @@
import Link from 'next/link'
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'Ressourcen-Hub | BreakPilot Comply',
description:
'Praxisleitfaeden, Checklisten und Glossar zu DSGVO, AI Act und NIS2 — fuer Unternehmen, die Compliance ernst nehmen.',
openGraph: {
title: 'Compliance Ressourcen-Hub | BreakPilot Comply',
description:
'Praxisleitfaeden, Checklisten und Glossar zu DSGVO, AI Act und NIS2.',
locale: 'de_DE',
type: 'website',
},
}
export default function BlogLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="min-h-screen bg-white">
{/* Blog Header */}
<header className="sticky top-0 z-40 bg-white/80 backdrop-blur-md border-b border-slate-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex items-center justify-between h-16">
{/* Logo + Subtitle */}
<Link href="/" className="flex items-center space-x-3">
<div className="w-8 h-8 bg-gradient-to-br from-primary-500 to-accent-500 rounded-lg flex items-center justify-center">
<span className="text-white font-bold text-lg">B</span>
</div>
<div className="flex items-baseline space-x-2">
<span className="font-semibold text-xl text-slate-900">
BreakPilot Comply
</span>
<span className="hidden sm:inline text-sm text-slate-400 font-medium">
Ressourcen-Hub
</span>
</div>
</Link>
{/* Back to Home */}
<Link
href="/"
className="inline-flex items-center space-x-1.5 text-sm text-slate-500 hover:text-primary-600 transition-colors"
>
<svg
className="w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M10 19l-7-7m0 0l7-7m-7 7h18"
/>
</svg>
<span>Zur Startseite</span>
</Link>
</div>
</div>
</header>
{/* Content */}
<main>
{children}
</main>
</div>
)
}

View File

@@ -0,0 +1,552 @@
import { Metadata } from 'next'
import Link from 'next/link'
export const metadata: Metadata = {
title: 'NIS2-Richtlinie Checkliste | BreakPilot Comply',
description: 'Pruefen Sie mit unserer Checkliste, ob und wie Ihr Unternehmen von der NIS2-Richtlinie betroffen ist.',
}
export default function NIS2ChecklistePage() {
return (
<div className="min-h-screen bg-white">
{/* Hero */}
<section className="pt-12 pb-12 px-4 sm:px-6 lg:px-8 bg-gradient-to-b from-primary-50 to-white">
<div className="max-w-4xl mx-auto text-center">
<div className="inline-flex items-center gap-2 px-4 py-1.5 bg-primary-100 text-primary-700 rounded-full text-sm font-medium mb-6">
EU-Richtlinie 2022/2555
</div>
<h1 className="text-4xl sm:text-5xl font-bold text-slate-900 tracking-tight">
NIS2-Richtlinie Checkliste
</h1>
<p className="mt-6 text-xl text-slate-600 max-w-2xl mx-auto">
Pruefen Sie mit unserer Checkliste, ob und wie Ihr Unternehmen von der NIS2-Richtlinie betroffen ist &ndash; und welche Massnahmen Sie umsetzen muessen.
</p>
</div>
</section>
{/* Table of Contents */}
<section className="py-8 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
<div className="bg-slate-50 border border-slate-200 rounded-2xl p-6">
<h2 className="text-lg font-semibold text-slate-900 mb-4">Inhalt</h2>
<nav className="space-y-2 text-sm">
<a href="#was-ist-nis2" className="block text-primary-600 hover:text-primary-800 transition-colors">1. Was ist die NIS2-Richtlinie?</a>
<a href="#wer-ist-betroffen" className="block text-primary-600 hover:text-primary-800 transition-colors">2. Wer ist betroffen?</a>
<a href="#schwellenwerte" className="block text-primary-600 hover:text-primary-800 transition-colors">3. Schwellenwerte</a>
<a href="#mindestanforderungen" className="block text-primary-600 hover:text-primary-800 transition-colors">4. Die 10 Mindestanforderungen</a>
<a href="#meldepflichten" className="block text-primary-600 hover:text-primary-800 transition-colors">5. Meldepflichten</a>
<a href="#strafen" className="block text-primary-600 hover:text-primary-800 transition-colors">6. Strafen und Bussgelder</a>
<a href="#checkliste" className="block text-primary-600 hover:text-primary-800 transition-colors">7. Checkliste: In 5 Schritten NIS2-konform</a>
<a href="#breakpilot-comply" className="block text-primary-600 hover:text-primary-800 transition-colors">8. BreakPilot Comply und NIS2</a>
</nav>
</div>
</div>
</section>
{/* Article Content */}
<article className="py-12 px-4 sm:px-6 lg:px-8">
<div className="max-w-3xl mx-auto">
{/* Section 1 */}
<section id="was-ist-nis2" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
1. Was ist die NIS2-Richtlinie?
</h2>
<div className="prose prose-slate max-w-none space-y-4">
<p className="text-lg text-slate-600 leading-relaxed">
Die <strong className="text-slate-900">NIS2-Richtlinie</strong> (Network and Information Security Directive 2, EU 2022/2555) ist die ueberarbeitete EU-Richtlinie fuer Cybersicherheit. Sie loest die urspruengliche NIS-Richtlinie von 2016 ab und erweitert deren Anwendungsbereich erheblich.
</p>
<p className="text-lg text-slate-600 leading-relaxed">
Ziel der NIS2 ist es, ein einheitlich hohes Cybersicherheitsniveau in allen EU-Mitgliedstaaten sicherzustellen. Die Richtlinie verpflichtet Unternehmen und Einrichtungen in kritischen und wichtigen Sektoren, angemessene Sicherheitsmassnahmen zu ergreifen und Sicherheitsvorfaelle zu melden.
</p>
<p className="text-lg text-slate-600 leading-relaxed">
In Deutschland wird die NIS2-Richtlinie durch das <strong className="text-slate-900">NIS2-Umsetzungs- und Cybersicherheitsstaerkungsgesetz (NIS2UmsuCG)</strong> in nationales Recht umgesetzt. Dieses Gesetz aendert unter anderem das BSI-Gesetz (BSIG) und fuehrt neue Pflichten fuer Unternehmen ein. Die nationale Umsetzung haette bis Oktober 2024 erfolgen sollen; in Deutschland befindet sich das Gesetz Stand 2025 noch im Gesetzgebungsverfahren.
</p>
<div className="bg-primary-50 border border-primary-200 rounded-xl p-5 mt-6">
<p className="text-primary-800 font-medium">
Wichtig: Auch wenn das deutsche Umsetzungsgesetz noch nicht in Kraft getreten ist, sollten Unternehmen bereits jetzt mit der Vorbereitung beginnen. Die Anforderungen der EU-Richtlinie sind klar definiert und die Umsetzungsfristen laufen.
</p>
</div>
</div>
</section>
{/* Section 2 */}
<section id="wer-ist-betroffen" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
2. Wer ist betroffen?
</h2>
<div className="space-y-6">
<p className="text-lg text-slate-600 leading-relaxed">
Die NIS2-Richtlinie unterscheidet zwei Kategorien von Einrichtungen, die jeweils unterschiedlichen Pflichten und Sanktionsrahmen unterliegen:
</p>
{/* Wesentliche Einrichtungen */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-primary-600 text-white px-6 py-3">
<h3 className="text-lg font-semibold">Wesentliche Einrichtungen (Essential Entities)</h3>
</div>
<div className="p-6">
<p className="text-slate-600 mb-4">
Unternehmen in Sektoren mit hoher Kritikalitaet nach Anhang I der Richtlinie:
</p>
<div className="grid sm:grid-cols-2 gap-3">
{[
'Energie (Strom, Gas, Oel, Fernwaerme, Wasserstoff)',
'Verkehr (Luft, Schiene, Wasser, Strasse)',
'Bankwesen',
'Finanzmarktinfrastrukturen',
'Gesundheitswesen',
'Trinkwasserversorgung',
'Abwasserentsorgung',
'Digitale Infrastruktur (DNS, TLD, Cloud, RZ)',
'ICT-Service-Management (B2B)',
'Oeffentliche Verwaltung',
'Weltraum',
].map((sector) => (
<div key={sector} className="flex items-start gap-2">
<span className="text-primary-500 mt-1 flex-shrink-0">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</span>
<span className="text-sm text-slate-700">{sector}</span>
</div>
))}
</div>
</div>
</div>
{/* Wichtige Einrichtungen */}
<div className="bg-white rounded-xl border border-slate-200 overflow-hidden">
<div className="bg-accent-600 text-white px-6 py-3">
<h3 className="text-lg font-semibold">Wichtige Einrichtungen (Important Entities)</h3>
</div>
<div className="p-6">
<p className="text-slate-600 mb-4">
Unternehmen in weiteren kritischen Sektoren nach Anhang II:
</p>
<div className="grid sm:grid-cols-2 gap-3">
{[
'Post- und Kurierdienste',
'Abfallwirtschaft',
'Chemische Industrie',
'Lebensmittelproduktion und -vertrieb',
'Verarbeitendes Gewerbe (Medizinprodukte, Elektronik, Maschinenbau, Kfz)',
'Digitale Dienste (Marktplaetze, Suchmaschinen, soziale Netzwerke)',
'Forschungseinrichtungen',
].map((sector) => (
<div key={sector} className="flex items-start gap-2">
<span className="text-accent-500 mt-1 flex-shrink-0">
<svg className="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</span>
<span className="text-sm text-slate-700">{sector}</span>
</div>
))}
</div>
</div>
</div>
</div>
</section>
{/* Section 3 */}
<section id="schwellenwerte" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
3. Schwellenwerte
</h2>
<p className="text-lg text-slate-600 leading-relaxed mb-6">
Ob ein Unternehmen unter die NIS2-Richtlinie faellt, haengt neben der Sektorzugehoerigkeit auch von der Unternehmensgroesse ab. Die Richtlinie nutzt die EU-KMU-Definition:
</p>
<div className="overflow-x-auto">
<table className="w-full border-collapse">
<thead>
<tr className="bg-slate-100">
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700 border border-slate-200">Kategorie</th>
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700 border border-slate-200">Mitarbeiter</th>
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700 border border-slate-200">Jahresumsatz</th>
<th className="text-left px-4 py-3 text-sm font-semibold text-slate-700 border border-slate-200">Einstufung</th>
</tr>
</thead>
<tbody>
<tr className="bg-primary-50">
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200 font-medium">Grossunternehmen</td>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200">&ge; 250</td>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200">&gt; 50 Mio. EUR</td>
<td className="px-4 py-3 text-sm border border-slate-200">
<span className="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-primary-100 text-primary-800">
Wesentlich (Anhang I) / Wichtig (Anhang II)
</span>
</td>
</tr>
<tr>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200 font-medium">Mittleres Unternehmen</td>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200">50&ndash;249</td>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200">10&ndash;50 Mio. EUR</td>
<td className="px-4 py-3 text-sm border border-slate-200">
<span className="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-accent-100 text-accent-800">
Wichtig (beide Anhaenge)
</span>
</td>
</tr>
<tr className="bg-slate-50">
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200 font-medium">Kleinunternehmen</td>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200">&lt; 50</td>
<td className="px-4 py-3 text-sm text-slate-700 border border-slate-200">&lt; 10 Mio. EUR</td>
<td className="px-4 py-3 text-sm border border-slate-200">
<span className="inline-flex px-2 py-0.5 rounded-full text-xs font-medium bg-slate-100 text-slate-600">
Grundsaetzlich nicht betroffen*
</span>
</td>
</tr>
</tbody>
</table>
</div>
<p className="text-sm text-slate-500 mt-3">
* Ausnahmen: Bestimmte Einrichtungen wie DNS-Diensteanbieter, TLD-Registrierungsstellen, qualifizierte Vertrauensdiensteanbieter und Betreiber oeffentlicher Kommunikationsnetze fallen unabhaengig von ihrer Groesse unter die NIS2-Richtlinie.
</p>
</section>
{/* Section 4 */}
<section id="mindestanforderungen" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
4. Die 10 Mindestanforderungen
</h2>
<p className="text-lg text-slate-600 leading-relaxed mb-8">
Artikel 21 der NIS2-Richtlinie definiert zehn Mindestmassnahmen, die betroffene Einrichtungen umsetzen muessen. Diese bilden das Fundament eines angemessenen Cybersicherheitsniveaus:
</p>
<div className="space-y-4">
{[
{
num: 1,
title: 'Risikoanalyse und Sicherheitskonzepte',
desc: 'Erstellung und Pflege von Konzepten fuer die Risikoanalyse und Sicherheit von Informationssystemen. Dazu gehoert eine systematische Bewertung von Bedrohungen, Schwachstellen und deren potenziellen Auswirkungen.',
},
{
num: 2,
title: 'Bewertung von Sicherheitsvorfaellen (Incident Handling)',
desc: 'Etablierung von Prozessen zur Erkennung, Analyse, Eindaemmung und Behebung von Sicherheitsvorfaellen. Dazu gehoert auch ein klar definierter Eskalationsprozess.',
},
{
num: 3,
title: 'Business Continuity und Krisenmanagement',
desc: 'Sicherstellung der Geschaeftskontinuitaet durch Backup-Management, Disaster Recovery, Notfallplaene und Krisenmanagement-Verfahren.',
},
{
num: 4,
title: 'Sicherheit in der Lieferkette',
desc: 'Bewertung und Management von Cybersicherheitsrisiken in der gesamten Lieferkette, einschliesslich der Beziehungen zu direkten Lieferanten und Dienstleistern.',
},
{
num: 5,
title: 'Sicherheit bei Erwerb, Entwicklung und Wartung von IT-Systemen',
desc: 'Integration von Sicherheitsanforderungen in den gesamten Lebenszyklus von Netzwerk- und Informationssystemen, einschliesslich des Umgangs mit Schwachstellen.',
},
{
num: 6,
title: 'Management von Schwachstellen (Vulnerability Management)',
desc: 'Konzepte und Verfahren zur Bewertung der Wirksamkeit von Risikomanagement-Massnahmen. Regelmaessige Schwachstellen-Scans und Patch-Management gehoeren dazu.',
},
{
num: 7,
title: 'Bewertung der Wirksamkeit von Sicherheitsmassnahmen',
desc: 'Regelmaessige Ueberpruefung und Bewertung der implementierten Cybersicherheitsmassnahmen, z.B. durch Audits, Penetrationstests oder Tabletop-Uebungen.',
},
{
num: 8,
title: 'Kryptographie und Verschluesselung',
desc: 'Konzepte und Verfahren fuer den Einsatz von Kryptographie und gegebenenfalls Verschluesselung zum Schutz von Daten bei der Uebertragung und Speicherung.',
},
{
num: 9,
title: 'Personalsicherheit und Schulungen',
desc: 'Sicherheit des Personals, Zugriffskontrollkonzepte und Management von Anlagenwerten. Dazu gehoeren regelmaessige Cybersicherheitsschulungen fuer alle Mitarbeiter und insbesondere fuer die Geschaeftsleitung.',
},
{
num: 10,
title: 'Multi-Faktor-Authentifizierung und Zugangskontrolle',
desc: 'Verwendung von Multi-Faktor-Authentifizierung (MFA), gesicherten Kommunikationsloesungen und ggf. gesicherten Notfallkommunikationssystemen.',
},
].map((item) => (
<div key={item.num} className="flex gap-4 bg-white rounded-xl p-5 border border-slate-200 hover:border-primary-200 hover:shadow-md transition-all">
<div className="flex-shrink-0 w-10 h-10 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-sm">
{item.num}
</div>
<div className="flex-1">
<h3 className="text-lg font-semibold text-slate-900 mb-1">{item.title}</h3>
<p className="text-slate-600 text-sm leading-relaxed">{item.desc}</p>
</div>
</div>
))}
</div>
</section>
{/* Section 5 */}
<section id="meldepflichten" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
5. Meldepflichten
</h2>
<p className="text-lg text-slate-600 leading-relaxed mb-8">
Bei erheblichen Sicherheitsvorfaellen muessen betroffene Einrichtungen einen gestuften Meldeprozess einhalten. Die Meldungen gehen in Deutschland an das BSI (Bundesamt fuer Sicherheit in der Informationstechnik):
</p>
<div className="space-y-6">
{[
{
time: '24 Stunden',
title: 'Fruehwarnung',
desc: 'Innerhalb von 24 Stunden nach Kenntnisnahme eines erheblichen Sicherheitsvorfalls muss eine Fruehwarnung an die zustaendige Behoerde (BSI) erfolgen. Diese soll angeben, ob der Vorfall vermutlich auf rechtswidriges oder boesartiges Handeln zurueckzufuehren ist und ob grenzueberschreitende Auswirkungen moeglich sind.',
color: 'bg-red-50 border-red-200',
badge: 'bg-red-100 text-red-800',
},
{
time: '72 Stunden',
title: 'Meldung des Vorfalls',
desc: 'Innerhalb von 72 Stunden muss eine ausfuehrliche Meldung des Vorfalls erfolgen. Diese aktualisiert die Fruehwarnung und enthaelt eine erste Bewertung des Vorfalls, einschliesslich Schweregrad, Auswirkungen sowie gegebenenfalls Indikatoren fuer eine Kompromittierung (IoCs).',
color: 'bg-amber-50 border-amber-200',
badge: 'bg-amber-100 text-amber-800',
},
{
time: '1 Monat',
title: 'Abschlussbericht',
desc: 'Spaetestens einen Monat nach Einreichung der Vorfallsmeldung ist ein Abschlussbericht vorzulegen. Dieser enthaelt eine ausfuehrliche Beschreibung des Vorfalls einschliesslich Schweregrad und Auswirkungen, die Art der Bedrohung, Grundursachen, ergriffene und laufende Abhilfemassnahmen sowie ggf. grenzueberschreitende Auswirkungen.',
color: 'bg-green-50 border-green-200',
badge: 'bg-green-100 text-green-800',
},
].map((step) => (
<div key={step.time} className={`rounded-xl p-6 border ${step.color}`}>
<div className="flex items-center gap-3 mb-3">
<span className={`inline-flex px-3 py-1 rounded-full text-sm font-bold ${step.badge}`}>
{step.time}
</span>
<h3 className="text-lg font-semibold text-slate-900">{step.title}</h3>
</div>
<p className="text-slate-600 text-sm leading-relaxed">{step.desc}</p>
</div>
))}
</div>
</section>
{/* Section 6 */}
<section id="strafen" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
6. Strafen und Bussgelder
</h2>
<p className="text-lg text-slate-600 leading-relaxed mb-8">
Die NIS2-Richtlinie sieht empfindliche Sanktionen bei Verstoessen vor. Erstmals werden auch persoenliche Haftungsregelungen fuer die Geschaeftsleitung eingefuehrt:
</p>
<div className="grid sm:grid-cols-2 gap-6">
<div className="bg-primary-50 rounded-xl p-6 border border-primary-200">
<h3 className="text-lg font-bold text-primary-900 mb-3">Wesentliche Einrichtungen</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className="text-2xl font-bold text-primary-700">10 Mio. EUR</span>
</div>
<p className="text-sm text-primary-800">
oder <strong>2% des weltweiten Jahresumsatzes</strong> &ndash; je nachdem, welcher Betrag hoeher ist.
</p>
</div>
</div>
<div className="bg-accent-50 rounded-xl p-6 border border-accent-200">
<h3 className="text-lg font-bold text-accent-900 mb-3">Wichtige Einrichtungen</h3>
<div className="space-y-3">
<div className="flex items-center gap-2">
<span className="text-2xl font-bold text-accent-700">7 Mio. EUR</span>
</div>
<p className="text-sm text-accent-800">
oder <strong>1,4% des weltweiten Jahresumsatzes</strong> &ndash; je nachdem, welcher Betrag hoeher ist.
</p>
</div>
</div>
</div>
<div className="bg-red-50 border border-red-200 rounded-xl p-5 mt-6">
<p className="text-red-800 text-sm font-medium">
Neu: Die Geschaeftsleitung kann persoenlich haftbar gemacht werden, wenn sie ihren Aufsichtspflichten nicht nachkommt. Geschaeftsfuehrer und Vorstaende muessen die Cybersicherheitsmassnahmen billigen und deren Umsetzung ueberwachen. Sie sind zudem verpflichtet, an Cybersicherheitsschulungen teilzunehmen.
</p>
</div>
</section>
{/* Section 7 */}
<section id="checkliste" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
7. Checkliste: In 5 Schritten NIS2-konform
</h2>
<p className="text-lg text-slate-600 leading-relaxed mb-8">
Nutzen Sie diese Schritt-fuer-Schritt-Anleitung, um Ihre NIS2-Konformitaet systematisch aufzubauen:
</p>
<div className="space-y-8">
{/* Step 1 */}
<div className="relative">
<div className="flex gap-4">
<div className="flex-shrink-0 w-12 h-12 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-lg">
1
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-slate-900 mb-4">Betroffenheit pruefen</h3>
<div className="space-y-2">
{[
'Gehoert Ihr Unternehmen zu einem der 18 NIS2-Sektoren (Anhang I oder II)?',
'Erfuellen Sie die Schwellenwerte (Mitarbeiterzahl, Umsatz, Bilanzsumme)?',
'Fallen Sie unter eine der Sonderregelungen (z.B. DNS, TLD, Vertrauensdienste)?',
'Klaeren Sie, ob Sie als wesentliche oder wichtige Einrichtung gelten.',
].map((item) => (
<label key={item} className="flex items-start gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input type="checkbox" className="mt-1 h-4 w-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
<span className="text-sm text-slate-700">{item}</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* Step 2 */}
<div className="relative">
<div className="flex gap-4">
<div className="flex-shrink-0 w-12 h-12 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-lg">
2
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-slate-900 mb-4">Gap-Analyse durchfuehren</h3>
<div className="space-y-2">
{[
'Bestandsaufnahme der vorhandenen Sicherheitsmassnahmen machen.',
'Abgleich mit den 10 Mindestanforderungen aus Art. 21 NIS2.',
'Luecken (Gaps) identifizieren und priorisieren.',
'Bestehende Zertifizierungen (ISO 27001, BSI Grundschutz) als Basis nutzen.',
].map((item) => (
<label key={item} className="flex items-start gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input type="checkbox" className="mt-1 h-4 w-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
<span className="text-sm text-slate-700">{item}</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* Step 3 */}
<div className="relative">
<div className="flex gap-4">
<div className="flex-shrink-0 w-12 h-12 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-lg">
3
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-slate-900 mb-4">Governance und Verantwortlichkeiten definieren</h3>
<div className="space-y-2">
{[
'Geschaeftsleitung in die Verantwortung nehmen (Billigungspflicht).',
'CISO oder IT-Sicherheitsbeauftragten benennen.',
'Cybersicherheitsschulungen fuer die Geschaeftsleitung planen.',
'Berichtswege und Eskalationspfade festlegen.',
'Budget fuer Cybersicherheitsmassnahmen freigeben.',
].map((item) => (
<label key={item} className="flex items-start gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input type="checkbox" className="mt-1 h-4 w-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
<span className="text-sm text-slate-700">{item}</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* Step 4 */}
<div className="relative">
<div className="flex gap-4">
<div className="flex-shrink-0 w-12 h-12 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-lg">
4
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-slate-900 mb-4">Technische und organisatorische Massnahmen umsetzen</h3>
<div className="space-y-2">
{[
'ISMS (Information Security Management System) aufbauen oder erweitern.',
'Incident-Response-Plan erstellen und testen.',
'Business-Continuity-Plan und Disaster-Recovery-Konzept implementieren.',
'Lieferketten-Risikomanagement etablieren.',
'MFA (Multi-Faktor-Authentifizierung) flaechendeckend einfuehren.',
'Verschluesselungskonzepte fuer Daten at rest und in transit umsetzen.',
'Regelmaessige Schwachstellen-Scans und Penetrationstests einfuehren.',
].map((item) => (
<label key={item} className="flex items-start gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input type="checkbox" className="mt-1 h-4 w-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
<span className="text-sm text-slate-700">{item}</span>
</label>
))}
</div>
</div>
</div>
</div>
{/* Step 5 */}
<div className="relative">
<div className="flex gap-4">
<div className="flex-shrink-0 w-12 h-12 bg-primary-600 text-white rounded-full flex items-center justify-center font-bold text-lg">
5
</div>
<div className="flex-1">
<h3 className="text-xl font-semibold text-slate-900 mb-4">Meldeprozesse und Dokumentation einrichten</h3>
<div className="space-y-2">
{[
'Meldeprozess fuer Sicherheitsvorfaelle definieren (24h/72h/1 Monat).',
'Kontaktdaten beim BSI hinterlegen (Registrierungspflicht).',
'Interne Meldestrukturen und Kommunikationswege festlegen.',
'Dokumentation aller Massnahmen sicherstellen (Nachweispflicht).',
'Regelmaessige Audits und Reviews planen.',
].map((item) => (
<label key={item} className="flex items-start gap-3 p-2 rounded-lg hover:bg-slate-50 cursor-pointer">
<input type="checkbox" className="mt-1 h-4 w-4 rounded border-slate-300 text-primary-600 focus:ring-primary-500" />
<span className="text-sm text-slate-700">{item}</span>
</label>
))}
</div>
</div>
</div>
</div>
</div>
</section>
{/* Section 8 */}
<section id="breakpilot-comply" className="mb-16">
<h2 className="text-3xl font-bold text-slate-900 mb-6">
8. BreakPilot Comply und NIS2
</h2>
<div className="bg-gradient-to-br from-primary-50 to-accent-50 rounded-2xl p-8 border border-primary-200">
<p className="text-lg text-slate-700 leading-relaxed mb-6">
BreakPilot Comply unterstuetzt Sie bei der systematischen Umsetzung der NIS2-Anforderungen. Unsere Plattform bietet Ihnen strukturierte Workflows fuer Risikoanalysen, Massnahmen-Tracking, Incident-Management und die erforderliche Dokumentation &ndash; alles an einem Ort.
</p>
<div className="flex flex-col sm:flex-row gap-4">
<Link
href="/blog"
className="inline-flex items-center justify-center px-6 py-3 rounded-xl bg-primary-600 text-white font-medium hover:bg-primary-700 transition-all"
>
Mehr erfahren
</Link>
<Link
href="/blog/glossar"
className="inline-flex items-center justify-center px-6 py-3 rounded-xl bg-white text-slate-700 font-medium hover:bg-slate-100 transition-all border border-slate-200"
>
Compliance-Glossar
</Link>
</div>
</div>
</section>
</div>
</article>
{/* Back to Blog */}
<section className="py-8 px-4 sm:px-6 lg:px-8 border-t border-slate-100">
<div className="max-w-3xl mx-auto">
<Link href="/blog" className="inline-flex items-center gap-2 text-primary-600 hover:text-primary-800 transition-colors text-sm font-medium">
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M15 19l-7-7 7-7" />
</svg>
Zurueck zum Blog
</Link>
</div>
</section>
</div>
)
}

268
website/app/blog/page.tsx Normal file
View File

@@ -0,0 +1,268 @@
'use client'
import { useState } from 'react'
import Link from 'next/link'
// ---------------------------------------------------------------------------
// Types
// ---------------------------------------------------------------------------
interface Article {
slug: string
title: string
excerpt: string
category: Category
readingTime: string
date: string
}
type Category = 'DSGVO' | 'AI Act' | 'NIS2' | 'Glossar'
type FilterOption = 'Alle' | Category
// ---------------------------------------------------------------------------
// Data
// ---------------------------------------------------------------------------
const ARTICLES: Article[] = [
{
slug: 'dsgvo-vvt-erstellen',
title: 'Verzeichnis von Verarbeitungstaetigkeiten (VVT) erstellen',
excerpt:
'Schritt-fuer-Schritt Anleitung zur Erstellung eines DSGVO-konformen VVT nach Art. 30.',
category: 'DSGVO',
readingTime: '8 Min.',
date: '2026-02-10',
},
{
slug: 'ai-act-ueberblick',
title: 'EU AI Act: Was Unternehmen jetzt wissen muessen',
excerpt:
'Der EU AI Act tritt stufenweise in Kraft. Welche Pflichten gelten fuer Ihr Unternehmen?',
category: 'AI Act',
readingTime: '10 Min.',
date: '2026-02-08',
},
{
slug: 'nis2-checkliste',
title: 'NIS2-Richtlinie: Checkliste fuer betroffene Unternehmen',
excerpt:
'Pruefen Sie mit unserer Checkliste, ob Ihr Unternehmen unter die NIS2-Richtlinie faellt.',
category: 'NIS2',
readingTime: '6 Min.',
date: '2026-02-05',
},
{
slug: 'tom-massnahmen-uebersicht',
title: 'Technische und Organisatorische Massnahmen (TOM)',
excerpt:
'Welche TOMs verlangt die DSGVO? Eine praxisnahe Uebersicht mit Beispielen.',
category: 'DSGVO',
readingTime: '7 Min.',
date: '2026-01-28',
},
{
slug: 'auftragsverarbeitung-avv',
title: 'Auftragsverarbeitung und AVV nach Art. 28 DSGVO',
excerpt:
'Wann brauchen Sie einen AVV? Was muss drin stehen? Praxisleitfaden.',
category: 'DSGVO',
readingTime: '9 Min.',
date: '2026-01-20',
},
{
slug: 'glossar',
title: 'Compliance-Glossar: Die wichtigsten Begriffe',
excerpt:
'Von AVV bis ZAB — alle Compliance-Fachbegriffe verstaendlich erklaert.',
category: 'Glossar',
readingTime: '15 Min.',
date: '2026-02-12',
},
]
const FILTER_OPTIONS: FilterOption[] = [
'Alle',
'DSGVO',
'AI Act',
'NIS2',
'Glossar',
]
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------
const CATEGORY_STYLES: Record<Category, string> = {
DSGVO: 'bg-blue-50 text-blue-700 ring-blue-600/10',
'AI Act': 'bg-purple-50 text-purple-700 ring-purple-600/10',
NIS2: 'bg-orange-50 text-orange-700 ring-orange-600/10',
Glossar: 'bg-green-50 text-green-700 ring-green-600/10',
}
function formatDate(dateStr: string): string {
return new Date(dateStr).toLocaleDateString('de-DE', {
day: '2-digit',
month: 'long',
year: 'numeric',
})
}
// ---------------------------------------------------------------------------
// Components
// ---------------------------------------------------------------------------
function CategoryBadge({ category }: { category: Category }) {
return (
<span
className={`inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium ring-1 ring-inset ${CATEGORY_STYLES[category]}`}
>
{category}
</span>
)
}
function ArticleCard({ article }: { article: Article }) {
return (
<Link
href={`/blog/${article.slug}`}
className="group flex flex-col bg-white rounded-xl border border-slate-200 overflow-hidden card-hover"
>
{/* Card Body */}
<div className="flex-1 p-6">
<div className="flex items-center justify-between mb-3">
<CategoryBadge category={article.category} />
<span className="text-xs text-slate-400">{article.readingTime}</span>
</div>
<h3 className="text-lg font-semibold text-slate-900 group-hover:text-primary-600 transition-colors leading-snug mb-2">
{article.title}
</h3>
<p className="text-sm text-slate-500 leading-relaxed">
{article.excerpt}
</p>
</div>
{/* Card Footer */}
<div className="px-6 pb-5 pt-0 flex items-center justify-between">
<time className="text-xs text-slate-400" dateTime={article.date}>
{formatDate(article.date)}
</time>
<span className="inline-flex items-center text-sm font-medium text-primary-600 group-hover:translate-x-1 transition-transform">
Lesen
<svg
className="ml-1 w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</span>
</div>
</Link>
)
}
// ---------------------------------------------------------------------------
// Page
// ---------------------------------------------------------------------------
export default function BlogPage() {
const [activeFilter, setActiveFilter] = useState<FilterOption>('Alle')
const filteredArticles =
activeFilter === 'Alle'
? ARTICLES
: ARTICLES.filter((a) => a.category === activeFilter)
return (
<div className="pb-20 max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
{/* Hero */}
<section className="pt-16 pb-12 text-center">
<h1 className="text-4xl sm:text-5xl font-bold text-slate-900 tracking-tight">
Compliance{' '}
<span className="gradient-text">Ressourcen-Hub</span>
</h1>
<p className="mt-4 max-w-2xl mx-auto text-lg text-slate-500">
Praxisleitfaeden, Checklisten und Glossar zu DSGVO, AI Act und NIS2
damit Sie jederzeit compliant bleiben.
</p>
</section>
{/* Category Filters */}
<section className="mb-10">
<div className="flex flex-wrap items-center justify-center gap-2">
{FILTER_OPTIONS.map((option) => {
const isActive = activeFilter === option
return (
<button
key={option}
onClick={() => setActiveFilter(option)}
className={`px-4 py-2 rounded-full text-sm font-medium transition-colors ${
isActive
? 'bg-primary-600 text-white shadow-sm'
: 'bg-slate-100 text-slate-600 hover:bg-slate-200'
}`}
>
{option}
</button>
)
})}
</div>
</section>
{/* Articles Grid */}
<section>
{filteredArticles.length > 0 ? (
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{filteredArticles.map((article) => (
<ArticleCard key={article.slug} article={article} />
))}
</div>
) : (
<p className="text-center text-slate-400 py-16">
Keine Artikel in dieser Kategorie.
</p>
)}
</section>
{/* CTA */}
<section className="mt-20 text-center bg-gradient-to-br from-primary-50 to-accent-50 rounded-2xl py-14 px-6">
<h2 className="text-2xl sm:text-3xl font-bold text-slate-900">
Compliance automatisieren?
</h2>
<p className="mt-3 text-slate-600 max-w-xl mx-auto">
BreakPilot Comply hilft Ihnen, DSGVO, AI Act und NIS2 Anforderungen
effizient umzusetzen mit KI-gestuetzten Tools.
</p>
<Link
href="/"
className="mt-6 inline-flex items-center px-6 py-3 bg-primary-600 text-white rounded-lg font-medium hover:bg-primary-700 transition-colors btn-press"
>
Kostenlos testen
<svg
className="ml-2 w-4 h-4"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
strokeWidth={2}
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M17 8l4 4m0 0l-4 4m4-4H3"
/>
</svg>
</Link>
</section>
</div>
)
}