refactor: Remove all SDK/compliance pages and API routes from admin-lehrer

SDK/compliance content belongs exclusively in admin-compliance (port 3007).
Removed:
- All (sdk)/ pages (document-crawler, dsb-portal, industry-templates, multi-tenant, sso)
- All api/sdk/ proxy routes
- All developers/sdk/ documentation pages
- Unused lib/sdk/ modules (kept: catalog-manager + its deps for dashboard)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-14 09:24:36 +01:00
parent 27f1899428
commit 6a53f8d79c
131 changed files with 0 additions and 47136 deletions

View File

@@ -1,256 +0,0 @@
import { DevPortalLayout, CodeBlock, InfoBox, ParameterTable } from '@/components/developers/DevPortalLayout'
export default function SDKConfigurationPage() {
return (
<DevPortalLayout
title="SDK Konfiguration"
description="Alle Konfigurationsoptionen des AI Compliance SDK"
>
<h2>SDKProvider Props</h2>
<p>
Der SDKProvider akzeptiert folgende Konfigurationsoptionen:
</p>
<ParameterTable
parameters={[
{
name: 'tenantId',
type: 'string',
required: true,
description: 'Ihre eindeutige Tenant-ID (erhalten nach Abo-Abschluss)',
},
{
name: 'apiKey',
type: 'string',
required: false,
description: 'API Key fuer authentifizierte Anfragen (nur serverseitig verwenden)',
},
{
name: 'userId',
type: 'string',
required: false,
description: 'Benutzer-ID fuer Audit-Trail und Checkpoints',
},
{
name: 'enableBackendSync',
type: 'boolean',
required: false,
description: 'Aktiviert automatische Synchronisation mit dem Backend (default: false)',
},
{
name: 'apiBaseUrl',
type: 'string',
required: false,
description: 'Custom API URL fuer Self-Hosted Installationen',
},
{
name: 'syncInterval',
type: 'number',
required: false,
description: 'Intervall fuer Auto-Sync in Millisekunden (default: 30000)',
},
{
name: 'enableOfflineSupport',
type: 'boolean',
required: false,
description: 'Aktiviert localStorage Fallback bei Offline (default: true)',
},
{
name: 'initialStep',
type: 'string',
required: false,
description: 'Initialer Schritt beim ersten Laden (default: "advisory-board")',
},
{
name: 'onError',
type: '(error: Error) => void',
required: false,
description: 'Callback fuer Fehlerbehandlung',
},
{
name: 'onStateChange',
type: '(state: SDKState) => void',
required: false,
description: 'Callback bei State-Aenderungen',
},
]}
/>
<h2>Vollstaendiges Beispiel</h2>
<CodeBlock language="typescript" filename="app/layout.tsx">
{`'use client'
import { SDKProvider } from '@breakpilot/compliance-sdk'
import { useRouter } from 'next/navigation'
export default function SDKLayout({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<SDKProvider
tenantId={process.env.NEXT_PUBLIC_TENANT_ID!}
userId="user-123"
enableBackendSync={true}
syncInterval={60000} // Sync alle 60 Sekunden
enableOfflineSupport={true}
initialStep="use-case-workshop"
onError={(error) => {
console.error('SDK Error:', error)
// Optional: Sentry oder anderes Error-Tracking
}}
onStateChange={(state) => {
console.log('State changed:', state.currentStep)
// Optional: Analytics-Events
}}
>
{children}
</SDKProvider>
)
}`}
</CodeBlock>
<h2>Synchronisations-Strategien</h2>
<h3>1. Nur localStorage (Offline-Only)</h3>
<CodeBlock language="typescript" filename="Offline-Only">
{`<SDKProvider
tenantId="my-tenant"
enableBackendSync={false}
enableOfflineSupport={true}
>
{children}
</SDKProvider>`}
</CodeBlock>
<p>
Ideal fuer: Lokale Entwicklung, Demos, Privacy-fokussierte Installationen.
Daten werden nur im Browser gespeichert.
</p>
<h3>2. Backend-Sync mit Fallback</h3>
<CodeBlock language="typescript" filename="Backend + Fallback">
{`<SDKProvider
tenantId="my-tenant"
enableBackendSync={true}
enableOfflineSupport={true}
syncInterval={30000}
>
{children}
</SDKProvider>`}
</CodeBlock>
<p>
Empfohlen fuer: Produktionsumgebungen. Daten werden mit dem Backend
synchronisiert, localStorage dient als Fallback bei Netzwerkproblemen.
</p>
<h3>3. Nur Backend (kein lokaler Cache)</h3>
<CodeBlock language="typescript" filename="Backend-Only">
{`<SDKProvider
tenantId="my-tenant"
enableBackendSync={true}
enableOfflineSupport={false}
>
{children}
</SDKProvider>`}
</CodeBlock>
<p>
Ideal fuer: Strenge Compliance-Anforderungen, Multi-User-Szenarien.
Daten werden nur im Backend gespeichert.
</p>
<InfoBox type="warning" title="Backend-Only Modus">
Im Backend-Only Modus ist eine aktive Internetverbindung erforderlich.
Bei Netzwerkproblemen koennen Daten verloren gehen.
</InfoBox>
<h2>API URL Konfiguration</h2>
<h3>Cloud-Version (Standard)</h3>
<p>Keine zusaetzliche Konfiguration erforderlich:</p>
<CodeBlock language="typescript" filename="Cloud">
{`<SDKProvider tenantId="my-tenant">
{/* Nutzt automatisch https://api.breakpilot.io/sdk/v1 */}
</SDKProvider>`}
</CodeBlock>
<h3>Self-Hosted</h3>
<CodeBlock language="typescript" filename="Self-Hosted">
{`<SDKProvider
tenantId="my-tenant"
apiBaseUrl="https://your-server.com/sdk/v1"
>
{children}
</SDKProvider>`}
</CodeBlock>
<h3>Lokale Entwicklung</h3>
<CodeBlock language="typescript" filename="Local Dev">
{`<SDKProvider
tenantId="dev-tenant"
apiBaseUrl="http://localhost:8085/sdk/v1"
enableBackendSync={true}
>
{children}
</SDKProvider>`}
</CodeBlock>
<h2>Feature Flags</h2>
<p>
Das SDK unterstuetzt Feature Flags ueber Subscription-Levels:
</p>
<CodeBlock language="typescript" filename="Feature Checks">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function MyComponent() {
const { state } = useSDK()
// Subscription-basierte Features
const isEnterprise = state.subscription === 'ENTERPRISE'
const isProfessional = ['PROFESSIONAL', 'ENTERPRISE'].includes(state.subscription)
// Feature-Gates
const canExportPDF = isProfessional
const canUseRAG = isProfessional
const canCustomizeDSFA = isEnterprise
const canUseAPI = isProfessional
return (
<div>
{canExportPDF && <button>PDF Export</button>}
{canUseRAG && <RAGSearchPanel />}
</div>
)
}`}
</CodeBlock>
<h2>Logging & Debugging</h2>
<p>
Aktivieren Sie detailliertes Logging fuer die Entwicklung:
</p>
<CodeBlock language="typescript" filename="Debug Mode">
{`// In Ihrer .env.local
NEXT_PUBLIC_SDK_DEBUG=true
// Oder programmatisch
<SDKProvider
tenantId="my-tenant"
onStateChange={(state) => {
if (process.env.NODE_ENV === 'development') {
console.log('[SDK] State Update:', {
phase: state.currentPhase,
step: state.currentStep,
useCases: state.useCases.length,
risks: state.risks.length,
})
}
}}
>
{children}
</SDKProvider>`}
</CodeBlock>
<InfoBox type="info" title="React DevTools">
Der SDK-State ist im React DevTools unter dem SDKProvider-Context sichtbar.
Installieren Sie die React Developer Tools Browser-Extension fuer einfaches Debugging.
</InfoBox>
</DevPortalLayout>
)
}

View File

@@ -1,482 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
onClick={handleCopy}
className="p-2 hover:bg-gray-700 rounded transition-colors"
title="Kopieren"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-gray-400" />
)}
</button>
)
}
function CodeBlock({ code }: { code: string }) {
return (
<div className="relative bg-gray-900 rounded-lg overflow-hidden">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
)
}
function MethodCard({
name,
signature,
description,
params,
returns,
example,
}: {
name: string
signature: string
description: string
params?: { name: string; type: string; description: string }[]
returns?: string
example?: string
}) {
return (
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 bg-gray-50">
<code className="text-violet-600 font-mono font-semibold">{name}</code>
</div>
<div className="p-6">
<div className="bg-gray-100 rounded-lg p-3 mb-4">
<code className="text-sm font-mono text-gray-800">{signature}</code>
</div>
<p className="text-gray-600 mb-4">{description}</p>
{params && params.length > 0 && (
<div className="mb-4">
<h4 className="font-medium text-gray-900 mb-2">Parameter</h4>
<table className="min-w-full">
<tbody className="divide-y divide-gray-200">
{params.map((param) => (
<tr key={param.name}>
<td className="py-2 pr-4">
<code className="text-sm text-violet-600">{param.name}</code>
</td>
<td className="py-2 pr-4">
<code className="text-sm text-gray-500">{param.type}</code>
</td>
<td className="py-2 text-sm text-gray-600">{param.description}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
{returns && (
<div className="mb-4">
<h4 className="font-medium text-gray-900 mb-2">Rueckgabe</h4>
<code className="text-sm text-gray-600">{returns}</code>
</div>
)}
{example && (
<div>
<h4 className="font-medium text-gray-900 mb-2">Beispiel</h4>
<CodeBlock code={example} />
</div>
)}
</div>
</div>
)
}
export default function APIReferencePage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<h1 className="text-3xl font-bold text-gray-900 mb-2">API Referenz</h1>
<p className="text-lg text-gray-600 mb-8">
Vollstaendige Dokumentation aller Methoden und Konfigurationsoptionen des Consent SDK.
</p>
{/* ConsentManager */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-gray-900 mb-6">ConsentManager</h2>
<p className="text-gray-600 mb-6">
Die zentrale Klasse fuer das Consent Management. Verwaltet Einwilligungen, Script-Blocking und Events.
</p>
{/* Constructor */}
<div className="space-y-6">
<MethodCard
name="constructor"
signature="new ConsentManager(config: ConsentConfig)"
description="Erstellt eine neue Instanz des ConsentManagers mit der angegebenen Konfiguration."
params={[
{
name: 'config',
type: 'ConsentConfig',
description: 'Konfigurationsobjekt fuer den Manager',
},
]}
example={`const consent = new ConsentManager({
apiEndpoint: 'https://api.example.com/consent',
siteId: 'my-site',
debug: true,
});`}
/>
<MethodCard
name="init"
signature="async init(): Promise<void>"
description="Initialisiert das SDK, laedt bestehenden Consent und startet das Script-Blocking. Zeigt den Banner an falls noetig."
example={`await consent.init();`}
/>
<MethodCard
name="hasConsent"
signature="hasConsent(category: ConsentCategory): boolean"
description="Prueft ob Einwilligung fuer eine Kategorie vorliegt."
params={[
{
name: 'category',
type: 'ConsentCategory',
description: 'essential | functional | analytics | marketing | social',
},
]}
returns="boolean - true wenn Einwilligung vorliegt"
example={`if (consent.hasConsent('analytics')) {
// Analytics laden
loadGoogleAnalytics();
}`}
/>
<MethodCard
name="setConsent"
signature="async setConsent(input: ConsentInput): Promise<void>"
description="Setzt die Einwilligungen und speichert sie lokal sowie auf dem Server."
params={[
{
name: 'input',
type: 'ConsentInput',
description: 'Objekt mit Kategorien und optionalen Vendors',
},
]}
example={`await consent.setConsent({
essential: true,
functional: true,
analytics: true,
marketing: false,
social: false,
});`}
/>
<MethodCard
name="acceptAll"
signature="async acceptAll(): Promise<void>"
description="Akzeptiert alle Consent-Kategorien und schliesst den Banner."
example={`document.getElementById('accept-all').addEventListener('click', async () => {
await consent.acceptAll();
});`}
/>
<MethodCard
name="rejectAll"
signature="async rejectAll(): Promise<void>"
description="Lehnt alle nicht-essentiellen Kategorien ab und schliesst den Banner."
example={`document.getElementById('reject-all').addEventListener('click', async () => {
await consent.rejectAll();
});`}
/>
<MethodCard
name="revokeAll"
signature="async revokeAll(): Promise<void>"
description="Widerruft alle Einwilligungen und loescht den gespeicherten Consent."
example={`document.getElementById('revoke').addEventListener('click', async () => {
await consent.revokeAll();
location.reload();
});`}
/>
<MethodCard
name="on"
signature="on<T>(event: ConsentEventType, callback: (data: T) => void): () => void"
description="Registriert einen Event-Listener. Gibt eine Unsubscribe-Funktion zurueck."
params={[
{
name: 'event',
type: 'ConsentEventType',
description: 'init | change | accept_all | reject_all | banner_show | banner_hide | etc.',
},
{
name: 'callback',
type: 'function',
description: 'Callback-Funktion die bei Event aufgerufen wird',
},
]}
returns="() => void - Funktion zum Entfernen des Listeners"
example={`const unsubscribe = consent.on('change', (state) => {
console.log('Consent geaendert:', state);
});
// Spaeter: Listener entfernen
unsubscribe();`}
/>
<MethodCard
name="getConsent"
signature="getConsent(): ConsentState | null"
description="Gibt den aktuellen Consent-Status zurueck oder null falls kein Consent vorliegt."
returns="ConsentState | null"
example={`const state = consent.getConsent();
if (state) {
console.log('Consent ID:', state.consentId);
console.log('Kategorien:', state.categories);
}`}
/>
<MethodCard
name="exportConsent"
signature="async exportConsent(): Promise<string>"
description="Exportiert alle Consent-Daten als JSON-String (DSGVO Art. 20 Datenportabilitaet)."
returns="Promise<string> - JSON-formatierter Export"
example={`const exportData = await consent.exportConsent();
downloadAsFile(exportData, 'consent-export.json');`}
/>
</div>
</section>
{/* Configuration */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Konfiguration</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Option
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Typ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Default
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Beschreibung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">apiEndpoint</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">string</code>
</td>
<td className="px-6 py-4 text-sm text-gray-500">erforderlich</td>
<td className="px-6 py-4 text-sm text-gray-600">URL des Consent-Backends</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">siteId</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">string</code>
</td>
<td className="px-6 py-4 text-sm text-gray-500">erforderlich</td>
<td className="px-6 py-4 text-sm text-gray-600">Eindeutige Site-ID</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">debug</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">boolean</code>
</td>
<td className="px-6 py-4 text-sm text-gray-500">false</td>
<td className="px-6 py-4 text-sm text-gray-600">Aktiviert Debug-Logging</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">language</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">string</code>
</td>
<td className="px-6 py-4 text-sm text-gray-500">&apos;de&apos;</td>
<td className="px-6 py-4 text-sm text-gray-600">Sprache fuer UI-Texte</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">consent.rememberDays</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">number</code>
</td>
<td className="px-6 py-4 text-sm text-gray-500">365</td>
<td className="px-6 py-4 text-sm text-gray-600">Gueltigkeitsdauer in Tagen</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">consent.recheckAfterDays</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">number</code>
</td>
<td className="px-6 py-4 text-sm text-gray-500">180</td>
<td className="px-6 py-4 text-sm text-gray-600">Erneute Abfrage nach X Tagen</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Events */}
<section className="mb-12">
<h2 className="text-2xl font-semibold text-gray-900 mb-6">Events</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Event
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Daten
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Beschreibung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">init</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">ConsentState | null</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisiert</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">change</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">ConsentState</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">Consent geaendert</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">accept_all</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">ConsentState</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">Alle akzeptiert</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">reject_all</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">ConsentState</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">Alle abgelehnt</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">banner_show</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">undefined</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">Banner angezeigt</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">banner_hide</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">undefined</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">Banner versteckt</td>
</tr>
<tr>
<td className="px-6 py-4">
<code className="text-sm text-violet-600">error</code>
</td>
<td className="px-6 py-4">
<code className="text-sm text-gray-500">Error</code>
</td>
<td className="px-6 py-4 text-sm text-gray-600">Fehler aufgetreten</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Types */}
<section>
<h2 className="text-2xl font-semibold text-gray-900 mb-6">TypeScript Types</h2>
<CodeBlock
code={`// Consent-Kategorien
type ConsentCategory = 'essential' | 'functional' | 'analytics' | 'marketing' | 'social';
// Consent-Status
interface ConsentState {
categories: Record<ConsentCategory, boolean>;
vendors: Record<string, boolean>;
timestamp: string;
version: string;
consentId?: string;
expiresAt?: string;
}
// Konfiguration
interface ConsentConfig {
apiEndpoint: string;
siteId: string;
debug?: boolean;
language?: string;
fallbackLanguage?: string;
ui?: ConsentUIConfig;
consent?: ConsentBehaviorConfig;
onConsentChange?: (state: ConsentState) => void;
onBannerShow?: () => void;
onBannerHide?: () => void;
onError?: (error: Error) => void;
}`}
/>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,281 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
</button>
)
}
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{filename && (
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
{filename}
</div>
)}
<div className="relative">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default function AngularIntegrationPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-red-500 flex items-center justify-center">
<span className="text-white font-bold">A</span>
</div>
<h1 className="text-3xl font-bold text-gray-900">Angular Integration</h1>
</div>
<p className="text-lg text-gray-600 mb-8">
Service und Module fuer Angular 14+ Projekte.
</p>
{/* Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
<CodeBlock code="npm install @breakpilot/consent-sdk" />
</section>
{/* Module Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Module Setup</h2>
<CodeBlock
filename="app.module.ts"
code={`import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { ConsentModule } from '@breakpilot/consent-sdk/angular';
import { environment } from '../environments/environment';
@NgModule({
imports: [
BrowserModule,
ConsentModule.forRoot({
apiEndpoint: environment.consentApi,
siteId: 'my-site',
debug: !environment.production,
}),
],
})
export class AppModule {}`}
/>
</section>
{/* Standalone Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Standalone Setup (Angular 15+)</h2>
<CodeBlock
filename="app.config.ts"
code={`import { ApplicationConfig } from '@angular/core';
import { provideConsent } from '@breakpilot/consent-sdk/angular';
import { environment } from '../environments/environment';
export const appConfig: ApplicationConfig = {
providers: [
provideConsent({
apiEndpoint: environment.consentApi,
siteId: 'my-site',
debug: !environment.production,
}),
],
};`}
/>
</section>
{/* Service Usage */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Service Usage</h2>
<CodeBlock
filename="components/analytics.component.ts"
code={`import { Component, OnInit } from '@angular/core';
import { ConsentService } from '@breakpilot/consent-sdk/angular';
@Component({
selector: 'app-analytics',
template: \`
<div *ngIf="hasAnalyticsConsent$ | async">
<!-- Analytics Code hier -->
</div>
\`,
})
export class AnalyticsComponent implements OnInit {
hasAnalyticsConsent$ = this.consentService.hasConsent$('analytics');
constructor(private consentService: ConsentService) {}
async loadAnalytics() {
if (await this.consentService.hasConsent('analytics')) {
// Load analytics
}
}
}`}
/>
</section>
{/* Cookie Banner */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Cookie Banner Component</h2>
<CodeBlock
filename="components/cookie-banner.component.ts"
code={`import { Component } from '@angular/core';
import { ConsentService } from '@breakpilot/consent-sdk/angular';
@Component({
selector: 'app-cookie-banner',
template: \`
<div
*ngIf="isBannerVisible$ | async"
class="fixed bottom-0 inset-x-0 bg-white border-t shadow-lg p-4 z-50"
>
<div class="max-w-4xl mx-auto flex items-center justify-between">
<p class="text-sm text-gray-600">
Wir verwenden Cookies um Ihr Erlebnis zu verbessern.
</p>
<div class="flex gap-2">
<button (click)="rejectAll()" class="px-4 py-2 border rounded">
Ablehnen
</button>
<button (click)="showSettings()" class="px-4 py-2 border rounded">
Einstellungen
</button>
<button (click)="acceptAll()" class="px-4 py-2 bg-blue-600 text-white rounded">
Alle akzeptieren
</button>
</div>
</div>
</div>
\`,
})
export class CookieBannerComponent {
isBannerVisible$ = this.consentService.isBannerVisible$;
constructor(private consentService: ConsentService) {}
async acceptAll() {
await this.consentService.acceptAll();
}
async rejectAll() {
await this.consentService.rejectAll();
}
showSettings() {
this.consentService.showSettings();
}
}`}
/>
</section>
{/* Directive */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">ConsentGate Directive</h2>
<CodeBlock
filename="template.html"
code={`<!-- Zeigt Element nur wenn Consent vorhanden -->
<iframe
*consentGate="'social'"
src="https://www.youtube.com/embed/VIDEO_ID"
width="560"
height="315"
></iframe>
<!-- Mit Custom Fallback -->
<div *consentGate="'analytics'; else noConsent">
<app-analytics-dashboard></app-analytics-dashboard>
</div>
<ng-template #noConsent>
<div class="bg-gray-100 p-4 rounded-lg text-center">
<p>Bitte stimmen Sie Statistik-Cookies zu.</p>
<button (click)="showSettings()">Einstellungen</button>
</div>
</ng-template>`}
/>
</section>
{/* Service API */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">Service API</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Property/Method
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Typ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Beschreibung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4"><code className="text-violet-600">consent$</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Observable&lt;ConsentState&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Observable des aktuellen Consent</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent$()</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Observable&lt;boolean&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Reaktive Consent-Pruefung</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent()</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Promise&lt;boolean&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Async Consent-Pruefung</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisible$</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Observable&lt;boolean&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Banner-Sichtbarkeit</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Promise&lt;void&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Akzeptiert alle</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Promise&lt;void&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Lehnt alle ab</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">setConsent()</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Promise&lt;void&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Setzt spezifische Kategorien</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,98 +0,0 @@
'use client'
import React from 'react'
import Link from 'next/link'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { ChevronRight } from 'lucide-react'
const frameworks = [
{
name: 'React',
href: '/developers/sdk/consent/frameworks/react',
logo: '/logos/react.svg',
description: 'Hooks und Provider fuer React 17+ und Next.js',
features: ['ConsentProvider', 'useConsent Hook', 'ConsentGate Component'],
color: 'bg-cyan-500',
},
{
name: 'Vue 3',
href: '/developers/sdk/consent/frameworks/vue',
logo: '/logos/vue.svg',
description: 'Composables und Plugin fuer Vue 3 und Nuxt',
features: ['Vue Plugin', 'useConsent Composable', 'ConsentGate Component'],
color: 'bg-emerald-500',
},
{
name: 'Angular',
href: '/developers/sdk/consent/frameworks/angular',
logo: '/logos/angular.svg',
description: 'Service und Module fuer Angular 14+',
features: ['ConsentService', 'ConsentModule', 'Dependency Injection'],
color: 'bg-red-500',
},
]
export default function FrameworksPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Framework Integration</h1>
<p className="text-lg text-gray-600 mb-8">
Das Consent SDK bietet native Integrationen fuer alle gaengigen Frontend-Frameworks.
</p>
<div className="space-y-4">
{frameworks.map((framework) => (
<Link
key={framework.name}
href={framework.href}
className="block bg-white rounded-xl border border-gray-200 p-6 hover:border-violet-300 hover:shadow-md transition-all group"
>
<div className="flex items-start gap-4">
<div className={`w-12 h-12 rounded-xl ${framework.color} flex items-center justify-center shrink-0`}>
<span className="text-white font-bold text-lg">{framework.name[0]}</span>
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold text-gray-900 group-hover:text-violet-600 transition-colors">
{framework.name}
</h2>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-violet-600 transition-colors" />
</div>
<p className="text-gray-600 mt-1">{framework.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{framework.features.map((feature) => (
<span
key={feature}
className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-md"
>
{feature}
</span>
))}
</div>
</div>
</div>
</Link>
))}
</div>
{/* Vanilla JS Note */}
<div className="mt-8 p-4 bg-blue-50 border border-blue-200 rounded-xl">
<h3 className="font-medium text-blue-900">Vanilla JavaScript</h3>
<p className="text-sm text-blue-700 mt-1">
Sie koennen das SDK auch ohne Framework verwenden. Importieren Sie einfach den ConsentManager direkt
aus dem Hauptpaket. Siehe{' '}
<Link href="/developers/sdk/consent/installation" className="underline">
Installation
</Link>{' '}
fuer Details.
</p>
</div>
</div>
</main>
</div>
)
}

View File

@@ -1,277 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
</button>
)
}
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{filename && (
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
{filename}
</div>
)}
<div className="relative">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default function ReactIntegrationPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-cyan-500 flex items-center justify-center">
<span className="text-white font-bold">R</span>
</div>
<h1 className="text-3xl font-bold text-gray-900">React Integration</h1>
</div>
<p className="text-lg text-gray-600 mb-8">
Hooks und Provider fuer React 17+ und Next.js Projekte.
</p>
{/* Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
<CodeBlock code="npm install @breakpilot/consent-sdk" />
</section>
{/* Provider Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Provider Setup</h2>
<p className="text-gray-600 mb-4">
Umschliessen Sie Ihre App mit dem ConsentProvider:
</p>
<CodeBlock
filename="app/layout.tsx"
code={`import { ConsentProvider } from '@breakpilot/consent-sdk/react';
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de">
<body>
<ConsentProvider
config={{
apiEndpoint: process.env.NEXT_PUBLIC_CONSENT_API!,
siteId: 'my-site',
debug: process.env.NODE_ENV === 'development',
}}
>
{children}
</ConsentProvider>
</body>
</html>
);
}`}
/>
</section>
{/* useConsent Hook */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">useConsent Hook</h2>
<p className="text-gray-600 mb-4">
Verwenden Sie den Hook in jeder Komponente:
</p>
<CodeBlock
filename="components/Analytics.tsx"
code={`import { useConsent } from '@breakpilot/consent-sdk/react';
export function Analytics() {
const { hasConsent, acceptAll, rejectAll, showSettings } = useConsent();
if (!hasConsent('analytics')) {
return null;
}
return (
<Script
src="https://www.googletagmanager.com/gtag/js?id=GA_ID"
strategy="afterInteractive"
/>
);
}`}
/>
</section>
{/* ConsentGate */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">ConsentGate Component</h2>
<p className="text-gray-600 mb-4">
Zeigt Inhalte nur wenn Consent vorhanden ist:
</p>
<CodeBlock
filename="components/YouTubeEmbed.tsx"
code={`import { ConsentGate } from '@breakpilot/consent-sdk/react';
export function YouTubeEmbed({ videoId }: { videoId: string }) {
return (
<ConsentGate
category="social"
fallback={
<div className="bg-gray-100 p-4 rounded-lg text-center">
<p>Video erfordert Ihre Zustimmung.</p>
<button onClick={() => showSettings()}>
Einstellungen oeffnen
</button>
</div>
}
>
<iframe
src={\`https://www.youtube.com/embed/\${videoId}\`}
width="560"
height="315"
allowFullScreen
/>
</ConsentGate>
);
}`}
/>
</section>
{/* Custom Banner */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Custom Cookie Banner</h2>
<CodeBlock
filename="components/CookieBanner.tsx"
code={`import { useConsent } from '@breakpilot/consent-sdk/react';
export function CookieBanner() {
const {
isBannerVisible,
acceptAll,
rejectAll,
showSettings,
} = useConsent();
if (!isBannerVisible) return null;
return (
<div className="fixed bottom-0 inset-x-0 bg-white border-t shadow-lg p-4">
<div className="max-w-4xl mx-auto flex items-center justify-between">
<p className="text-sm text-gray-600">
Wir verwenden Cookies um Ihr Erlebnis zu verbessern.
</p>
<div className="flex gap-2">
<button
onClick={rejectAll}
className="px-4 py-2 text-sm border rounded"
>
Ablehnen
</button>
<button
onClick={showSettings}
className="px-4 py-2 text-sm border rounded"
>
Einstellungen
</button>
<button
onClick={acceptAll}
className="px-4 py-2 text-sm bg-blue-600 text-white rounded"
>
Alle akzeptieren
</button>
</div>
</div>
</div>
);
}`}
/>
</section>
{/* Hook API */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">Hook API</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Property
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Typ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Beschreibung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent</code></td>
<td className="px-6 py-4"><code className="text-gray-500">(category) =&gt; boolean</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Prueft Consent fuer Kategorie</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">consent</code></td>
<td className="px-6 py-4"><code className="text-gray-500">ConsentState | null</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Aktueller Consent-Status</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">acceptAll</code></td>
<td className="px-6 py-4"><code className="text-gray-500">() =&gt; Promise</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Akzeptiert alle Kategorien</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">rejectAll</code></td>
<td className="px-6 py-4"><code className="text-gray-500">() =&gt; Promise</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Lehnt alle ab (ausser essential)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">setConsent</code></td>
<td className="px-6 py-4"><code className="text-gray-500">(input) =&gt; Promise</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Setzt spezifische Kategorien</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisible</code></td>
<td className="px-6 py-4"><code className="text-gray-500">boolean</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Banner sichtbar?</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">showBanner</code></td>
<td className="px-6 py-4"><code className="text-gray-500">() =&gt; void</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Zeigt den Banner</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">showSettings</code></td>
<td className="px-6 py-4"><code className="text-gray-500">() =&gt; void</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Oeffnet Einstellungen</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,277 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
</button>
)
}
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{filename && (
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
{filename}
</div>
)}
<div className="relative">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default function VueIntegrationPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-emerald-500 flex items-center justify-center">
<span className="text-white font-bold">V</span>
</div>
<h1 className="text-3xl font-bold text-gray-900">Vue 3 Integration</h1>
</div>
<p className="text-lg text-gray-600 mb-8">
Composables und Plugin fuer Vue 3 und Nuxt Projekte.
</p>
{/* Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
<CodeBlock code="npm install @breakpilot/consent-sdk" />
</section>
{/* Plugin Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Plugin Setup</h2>
<CodeBlock
filename="main.ts"
code={`import { createApp } from 'vue';
import App from './App.vue';
import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
const app = createApp(App);
app.use(ConsentPlugin, {
apiEndpoint: import.meta.env.VITE_CONSENT_API,
siteId: 'my-site',
debug: import.meta.env.DEV,
});
app.mount('#app');`}
/>
</section>
{/* Composable */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">useConsent Composable</h2>
<CodeBlock
filename="components/Analytics.vue"
code={`<script setup lang="ts">
import { useConsent } from '@breakpilot/consent-sdk/vue';
const { hasConsent, acceptAll, rejectAll } = useConsent();
</script>
<template>
<div v-if="hasConsent('analytics')">
<!-- Analytics Code hier -->
</div>
</template>`}
/>
</section>
{/* Cookie Banner */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Cookie Banner Component</h2>
<CodeBlock
filename="components/CookieBanner.vue"
code={`<script setup lang="ts">
import { useConsent } from '@breakpilot/consent-sdk/vue';
const {
isBannerVisible,
acceptAll,
rejectAll,
showSettings,
} = useConsent();
</script>
<template>
<Transition name="slide">
<div
v-if="isBannerVisible"
class="fixed bottom-0 inset-x-0 bg-white border-t shadow-lg p-4 z-50"
>
<div class="max-w-4xl mx-auto flex items-center justify-between">
<p class="text-sm text-gray-600">
Wir verwenden Cookies um Ihr Erlebnis zu verbessern.
</p>
<div class="flex gap-2">
<button
@click="rejectAll"
class="px-4 py-2 text-sm border rounded hover:bg-gray-50"
>
Ablehnen
</button>
<button
@click="showSettings"
class="px-4 py-2 text-sm border rounded hover:bg-gray-50"
>
Einstellungen
</button>
<button
@click="acceptAll"
class="px-4 py-2 text-sm bg-blue-600 text-white rounded hover:bg-blue-700"
>
Alle akzeptieren
</button>
</div>
</div>
</div>
</Transition>
</template>
<style scoped>
.slide-enter-active,
.slide-leave-active {
transition: transform 0.3s ease;
}
.slide-enter-from,
.slide-leave-to {
transform: translateY(100%);
}
</style>`}
/>
</section>
{/* ConsentGate */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">ConsentGate Component</h2>
<CodeBlock
filename="components/YouTubeEmbed.vue"
code={`<script setup lang="ts">
import { ConsentGate } from '@breakpilot/consent-sdk/vue';
defineProps<{
videoId: string;
}>();
</script>
<template>
<ConsentGate category="social">
<template #default>
<iframe
:src="\`https://www.youtube.com/embed/\${videoId}\`"
width="560"
height="315"
allowfullscreen
/>
</template>
<template #fallback>
<div class="bg-gray-100 p-4 rounded-lg text-center">
<p>Video erfordert Ihre Zustimmung.</p>
<button class="mt-2 px-4 py-2 bg-blue-600 text-white rounded">
Zustimmen
</button>
</div>
</template>
</ConsentGate>
</template>`}
/>
</section>
{/* Nuxt */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Nuxt 3 Setup</h2>
<CodeBlock
filename="plugins/consent.client.ts"
code={`import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
export default defineNuxtPlugin((nuxtApp) => {
nuxtApp.vueApp.use(ConsentPlugin, {
apiEndpoint: useRuntimeConfig().public.consentApi,
siteId: 'my-site',
});
});`}
/>
</section>
{/* Composable API */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">Composable API</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Property
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Typ
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Beschreibung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent</code></td>
<td className="px-6 py-4"><code className="text-gray-500">(category) =&gt; boolean</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Reaktive Consent-Pruefung</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">consent</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Ref&lt;ConsentState&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Reaktiver Consent-Status</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisible</code></td>
<td className="px-6 py-4"><code className="text-gray-500">Ref&lt;boolean&gt;</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Reaktive Banner-Sichtbarkeit</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">acceptAll</code></td>
<td className="px-6 py-4"><code className="text-gray-500">() =&gt; Promise</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Akzeptiert alle</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">rejectAll</code></td>
<td className="px-6 py-4"><code className="text-gray-500">() =&gt; Promise</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Lehnt alle ab</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">setConsent</code></td>
<td className="px-6 py-4"><code className="text-gray-500">(input) =&gt; Promise</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Setzt spezifische Kategorien</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,303 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check, Info, AlertTriangle } from 'lucide-react'
type PackageManager = 'npm' | 'yarn' | 'pnpm'
const installCommands: Record<PackageManager, string> = {
npm: 'npm install @breakpilot/consent-sdk',
yarn: 'yarn add @breakpilot/consent-sdk',
pnpm: 'pnpm add @breakpilot/consent-sdk',
}
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
onClick={handleCopy}
className="p-2 hover:bg-gray-700 rounded transition-colors"
title="Kopieren"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-gray-400" />
)}
</button>
)
}
function CodeBlock({ code, language = 'typescript' }: { code: string; language?: string }) {
return (
<div className="relative bg-gray-900 rounded-lg overflow-hidden">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
)
}
function InfoBox({ type = 'info', children }: { type?: 'info' | 'warning'; children: React.ReactNode }) {
const styles = {
info: 'bg-blue-50 border-blue-200 text-blue-800',
warning: 'bg-yellow-50 border-yellow-200 text-yellow-800',
}
const Icon = type === 'warning' ? AlertTriangle : Info
return (
<div className={`p-4 border rounded-lg ${styles[type]} flex items-start gap-3`}>
<Icon className="w-5 h-5 shrink-0 mt-0.5" />
<div className="text-sm">{children}</div>
</div>
)
}
export default function InstallationPage() {
const [selectedPM, setSelectedPM] = useState<PackageManager>('npm')
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Installation</h1>
<p className="text-lg text-gray-600 mb-8">
Installieren Sie das Consent SDK in Ihrem Projekt.
</p>
{/* Package Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">NPM Package</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-4 py-3 border-b border-gray-200 flex gap-1 bg-gray-50">
{(['npm', 'yarn', 'pnpm'] as const).map((pm) => (
<button
key={pm}
onClick={() => setSelectedPM(pm)}
className={`px-3 py-1.5 text-sm rounded-md transition-colors ${
selectedPM === pm
? 'bg-white text-gray-900 shadow-sm border border-gray-200'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{pm}
</button>
))}
</div>
<div className="bg-gray-900 px-4 py-4 flex items-center justify-between">
<code className="text-green-400 font-mono text-sm">
$ {installCommands[selectedPM]}
</code>
<CopyButton text={installCommands[selectedPM]} />
</div>
</div>
</section>
{/* Framework-specific */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Framework-spezifische Imports</h2>
<div className="space-y-6">
<div>
<h3 className="font-medium text-gray-900 mb-2">Vanilla JavaScript</h3>
<CodeBlock
code={`import { ConsentManager } from '@breakpilot/consent-sdk';
const consent = new ConsentManager({
apiEndpoint: 'https://api.example.com/consent',
siteId: 'your-site-id',
});
await consent.init();`}
/>
</div>
<div>
<h3 className="font-medium text-gray-900 mb-2">React</h3>
<CodeBlock
code={`import { ConsentProvider, useConsent } from '@breakpilot/consent-sdk/react';
function App() {
return (
<ConsentProvider
config={{
apiEndpoint: 'https://api.example.com/consent',
siteId: 'your-site-id',
}}
>
<YourApp />
</ConsentProvider>
);
}`}
/>
</div>
<div>
<h3 className="font-medium text-gray-900 mb-2">Vue 3</h3>
<CodeBlock
code={`import { createApp } from 'vue';
import { ConsentPlugin } from '@breakpilot/consent-sdk/vue';
const app = createApp(App);
app.use(ConsentPlugin, {
apiEndpoint: 'https://api.example.com/consent',
siteId: 'your-site-id',
});`}
/>
</div>
</div>
</section>
{/* Script Blocking Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Script Blocking einrichten</h2>
<p className="text-gray-600 mb-4">
Um Third-Party Scripts automatisch zu blockieren, verwenden Sie das{' '}
<code className="px-1.5 py-0.5 bg-gray-100 rounded text-sm">data-consent</code> Attribut:
</p>
<CodeBlock
language="html"
code={`<!-- Analytics Script (blockiert bis Consent) -->
<script
data-consent="analytics"
data-src="https://www.googletagmanager.com/gtag/js?id=GA_MEASUREMENT_ID"
type="text/plain"
></script>
<!-- Marketing Script (blockiert bis Consent) -->
<script data-consent="marketing" type="text/plain">
fbq('init', 'YOUR_PIXEL_ID');
</script>
<!-- Embedded iFrame (blockiert bis Consent) -->
<iframe
data-consent="social"
data-src="https://www.youtube.com/embed/VIDEO_ID"
width="560"
height="315"
></iframe>`}
/>
</section>
{/* Requirements */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Anforderung
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Minimum
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 text-sm text-gray-900">Node.js</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 18.0.0</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm text-gray-900">React (optional)</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 17.0.0</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm text-gray-900">Vue (optional)</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 3.0.0</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm text-gray-900">TypeScript (optional)</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 4.7.0</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Browser Support */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Browser-Unterstuetzung</h2>
<InfoBox type="info">
Das SDK unterstuetzt alle modernen Browser mit ES2017+ Unterstuetzung.
Fuer aeltere Browser wird ein automatischer Fallback fuer Crypto-Funktionen bereitgestellt.
</InfoBox>
<div className="mt-4 bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Browser
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Minimum Version
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 text-sm text-gray-900">Chrome</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 60</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm text-gray-900">Firefox</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 55</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm text-gray-900">Safari</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 11</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm text-gray-900">Edge</td>
<td className="px-6 py-4 text-sm text-gray-600">&gt;= 79 (Chromium)</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Next Steps */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">Naechste Schritte</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<a
href="/developers/sdk/consent/api-reference"
className="p-4 bg-white rounded-xl border border-gray-200 hover:border-violet-300 hover:shadow-md transition-all"
>
<h3 className="font-medium text-gray-900">API Referenz</h3>
<p className="text-sm text-gray-500 mt-1">
Vollstaendige Dokumentation aller Methoden und Konfigurationsoptionen.
</p>
</a>
<a
href="/developers/sdk/consent/frameworks"
className="p-4 bg-white rounded-xl border border-gray-200 hover:border-violet-300 hover:shadow-md transition-all"
>
<h3 className="font-medium text-gray-900">Framework Integration</h3>
<p className="text-sm text-gray-500 mt-1">
Detaillierte Anleitungen fuer React, Vue und Angular.
</p>
</a>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,269 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check, Smartphone } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
</button>
)
}
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{filename && (
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
{filename}
</div>
)}
<div className="relative">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default function AndroidSDKPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-green-600 flex items-center justify-center">
<Smartphone className="w-6 h-6 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900">Android SDK (Kotlin)</h1>
</div>
<p className="text-lg text-gray-600 mb-8">
Native Kotlin SDK fuer Android API 26+ mit Jetpack Compose Unterstuetzung.
</p>
{/* Requirements */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Kotlin Version</td>
<td className="px-6 py-4 text-sm text-gray-600">1.9+</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Min SDK</td>
<td className="px-6 py-4 text-sm text-gray-600">API 26 (Android 8.0)</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Compile SDK</td>
<td className="px-6 py-4 text-sm text-gray-600">34+</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
<CodeBlock
filename="build.gradle.kts (Module)"
code={`dependencies {
implementation("com.breakpilot:consent-sdk:1.0.0")
// Fuer Jetpack Compose
implementation("com.breakpilot:consent-sdk-compose:1.0.0")
}`}
/>
</section>
{/* Basic Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Grundlegende Einrichtung</h2>
<CodeBlock
filename="MyApplication.kt"
code={`import android.app.Application
import com.breakpilot.consent.ConsentManager
class MyApplication : Application() {
override fun onCreate() {
super.onCreate()
// Consent Manager konfigurieren
ConsentManager.configure(
context = this,
config = ConsentConfig(
apiEndpoint = "https://api.example.com/consent",
siteId = "my-android-app"
)
)
// Initialisieren
lifecycleScope.launch {
ConsentManager.initialize()
}
}
}`}
/>
</section>
{/* Jetpack Compose */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Jetpack Compose Integration</h2>
<CodeBlock
filename="MainActivity.kt"
code={`import androidx.compose.runtime.*
import com.breakpilot.consent.compose.*
@Composable
fun MainScreen() {
val consent = rememberConsentState()
Column {
// Consent-abhaengige UI
if (consent.hasConsent(ConsentCategory.ANALYTICS)) {
AnalyticsView()
}
// Buttons
Button(onClick = { consent.acceptAll() }) {
Text("Alle akzeptieren")
}
Button(onClick = { consent.rejectAll() }) {
Text("Alle ablehnen")
}
}
// Consent Banner (automatisch angezeigt wenn noetig)
ConsentBanner()
}
// ConsentGate Composable
@Composable
fun ProtectedContent() {
ConsentGate(
category = ConsentCategory.MARKETING,
fallback = {
Text("Marketing-Zustimmung erforderlich")
}
) {
MarketingContent()
}
}`}
/>
</section>
{/* Traditional Android */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">View-basierte Integration</h2>
<CodeBlock
filename="MainActivity.kt"
code={`import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.breakpilot.consent.ConsentManager
import kotlinx.coroutines.launch
import kotlinx.coroutines.flow.collect
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// Auf Consent-Aenderungen reagieren
lifecycleScope.launch {
ConsentManager.consentFlow.collect { state ->
updateUI(state)
}
}
// Banner anzeigen wenn noetig
if (ConsentManager.needsConsent()) {
ConsentManager.showBanner(this)
}
}
private fun updateUI(state: ConsentState?) {
if (state?.hasConsent(ConsentCategory.ANALYTICS) == true) {
loadAnalytics()
}
}
fun onAcceptAllClick(view: View) {
lifecycleScope.launch {
ConsentManager.acceptAll()
}
}
}`}
/>
</section>
{/* API Reference */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Referenz</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Methode</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4"><code className="text-violet-600">configure()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">SDK konfigurieren</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">initialize()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisieren (suspend)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Consent fuer Kategorie pruefen</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">consentFlow</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Flow fuer reaktive Updates</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Alle akzeptieren (suspend)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Alle ablehnen (suspend)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">setConsent()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Kategorien setzen (suspend)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">showBanner()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Banner als DialogFragment</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,313 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check, Smartphone } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
</button>
)
}
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{filename && (
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
{filename}
</div>
)}
<div className="relative">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default function FlutterSDKPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-blue-500 flex items-center justify-center">
<Smartphone className="w-6 h-6 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900">Flutter SDK</h1>
</div>
<p className="text-lg text-gray-600 mb-8">
Cross-Platform SDK fuer Flutter 3.16+ mit iOS, Android und Web Support.
</p>
{/* Requirements */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Dart Version</td>
<td className="px-6 py-4 text-sm text-gray-600">3.0+</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Flutter Version</td>
<td className="px-6 py-4 text-sm text-gray-600">3.16+</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Plattformen</td>
<td className="px-6 py-4 text-sm text-gray-600">iOS, Android, Web</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
<CodeBlock
filename="pubspec.yaml"
code={`dependencies:
consent_sdk: ^1.0.0`}
/>
<div className="mt-4">
<CodeBlock code="flutter pub get" />
</div>
</section>
{/* Basic Setup */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Grundlegende Einrichtung</h2>
<CodeBlock
filename="main.dart"
code={`import 'package:flutter/material.dart';
import 'package:consent_sdk/consent_sdk.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Consent SDK initialisieren
await ConsentManager.instance.initialize(
config: ConsentConfig(
apiEndpoint: 'https://api.example.com/consent',
siteId: 'my-flutter-app',
),
);
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp(
home: const ConsentWrapper(
child: HomeScreen(),
),
);
}
}`}
/>
</section>
{/* Widget Usage */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Widget Integration</h2>
<CodeBlock
filename="home_screen.dart"
code={`import 'package:flutter/material.dart';
import 'package:consent_sdk/consent_sdk.dart';
class HomeScreen extends StatelessWidget {
const HomeScreen({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(
children: [
// StreamBuilder fuer reaktive Updates
StreamBuilder<ConsentState?>(
stream: ConsentManager.instance.consentStream,
builder: (context, snapshot) {
final consent = snapshot.data;
if (consent?.hasConsent(ConsentCategory.analytics) ?? false) {
return const AnalyticsWidget();
}
return const SizedBox.shrink();
},
),
// ConsentGate Widget
ConsentGate(
category: ConsentCategory.marketing,
fallback: const Center(
child: Text('Marketing-Zustimmung erforderlich'),
),
child: const MarketingWidget(),
),
// Buttons
ElevatedButton(
onPressed: () => ConsentManager.instance.acceptAll(),
child: const Text('Alle akzeptieren'),
),
ElevatedButton(
onPressed: () => ConsentManager.instance.rejectAll(),
child: const Text('Alle ablehnen'),
),
TextButton(
onPressed: () => ConsentManager.instance.showSettings(context),
child: const Text('Einstellungen'),
),
],
),
);
}
}`}
/>
</section>
{/* Custom Banner */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Custom Cookie Banner</h2>
<CodeBlock
filename="cookie_banner.dart"
code={`import 'package:flutter/material.dart';
import 'package:consent_sdk/consent_sdk.dart';
class CustomCookieBanner extends StatelessWidget {
const CustomCookieBanner({super.key});
@override
Widget build(BuildContext context) {
return StreamBuilder<bool>(
stream: ConsentManager.instance.isBannerVisibleStream,
builder: (context, snapshot) {
if (!(snapshot.data ?? false)) {
return const SizedBox.shrink();
}
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: Colors.white,
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.1),
blurRadius: 10,
),
],
),
child: SafeArea(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'Wir verwenden Cookies um Ihr Erlebnis zu verbessern.',
style: TextStyle(fontSize: 14),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.end,
children: [
TextButton(
onPressed: () => ConsentManager.instance.rejectAll(),
child: const Text('Ablehnen'),
),
TextButton(
onPressed: () => ConsentManager.instance.showSettings(context),
child: const Text('Einstellungen'),
),
ElevatedButton(
onPressed: () => ConsentManager.instance.acceptAll(),
child: const Text('Alle akzeptieren'),
),
],
),
],
),
),
);
},
);
}
}`}
/>
</section>
{/* API Reference */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Referenz</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Methode/Property</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4"><code className="text-violet-600">initialize()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisieren (Future)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Consent pruefen</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">consentStream</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Stream fuer Consent-Updates</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">isBannerVisibleStream</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Stream fuer Banner-Sichtbarkeit</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Alle akzeptieren (Future)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Alle ablehnen (Future)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">setConsent()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Kategorien setzen (Future)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">showSettings()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Einstellungs-Dialog oeffnen</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,283 +0,0 @@
'use client'
import React, { useState } from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Copy, Check, Apple } from 'lucide-react'
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button onClick={handleCopy} className="p-2 hover:bg-gray-700 rounded transition-colors">
{copied ? <Check className="w-4 h-4 text-green-400" /> : <Copy className="w-4 h-4 text-gray-400" />}
</button>
)
}
function CodeBlock({ code, filename }: { code: string; filename?: string }) {
return (
<div className="bg-gray-900 rounded-lg overflow-hidden">
{filename && (
<div className="px-4 py-2 bg-gray-800 text-gray-400 text-xs border-b border-gray-700">
{filename}
</div>
)}
<div className="relative">
<div className="absolute top-2 right-2">
<CopyButton text={code} />
</div>
<pre className="p-4 text-sm text-gray-300 font-mono overflow-x-auto">
<code>{code}</code>
</pre>
</div>
</div>
)
}
export default function iOSSDKPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<div className="flex items-center gap-3 mb-2">
<div className="w-10 h-10 rounded-lg bg-gray-900 flex items-center justify-center">
<Apple className="w-6 h-6 text-white" />
</div>
<h1 className="text-3xl font-bold text-gray-900">iOS SDK (Swift)</h1>
</div>
<p className="text-lg text-gray-600 mb-8">
Native Swift SDK fuer iOS 15+ und iPadOS mit SwiftUI-Unterstuetzung.
</p>
{/* Requirements */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Systemvoraussetzungen</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Swift Version</td>
<td className="px-6 py-4 text-sm text-gray-600">5.9+</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">iOS Deployment Target</td>
<td className="px-6 py-4 text-sm text-gray-600">iOS 15.0+</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Xcode Version</td>
<td className="px-6 py-4 text-sm text-gray-600">15.0+</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* Installation */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Installation</h2>
<h3 className="font-medium text-gray-900 mb-2">Swift Package Manager</h3>
<CodeBlock
filename="Package.swift"
code={`dependencies: [
.package(url: "https://github.com/breakpilot/consent-sdk-ios.git", from: "1.0.0")
]`}
/>
<p className="text-sm text-gray-600 mt-4">
Oder in Xcode: File Add Package Dependencies URL eingeben
</p>
</section>
{/* Basic Usage */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Grundlegende Verwendung</h2>
<CodeBlock
filename="AppDelegate.swift"
code={`import ConsentSDK
@main
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(
_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
) -> Bool {
// Consent Manager konfigurieren
ConsentManager.shared.configure(
apiEndpoint: "https://api.example.com/consent",
siteId: "my-ios-app"
)
// Initialisieren
Task {
await ConsentManager.shared.initialize()
}
return true
}
}`}
/>
</section>
{/* SwiftUI Integration */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">SwiftUI Integration</h2>
<CodeBlock
filename="ContentView.swift"
code={`import SwiftUI
import ConsentSDK
struct ContentView: View {
@StateObject private var consent = ConsentManager.shared
var body: some View {
VStack {
if consent.hasConsent(.analytics) {
AnalyticsView()
}
Button("Alle akzeptieren") {
Task {
await consent.acceptAll()
}
}
}
.consentBanner() // Zeigt Banner automatisch
}
}
// Consent Gate Modifier
struct ProtectedView: View {
var body: some View {
Text("Geschuetzter Inhalt")
.requiresConsent(.marketing) {
// Fallback wenn kein Consent
Text("Marketing-Zustimmung erforderlich")
}
}
}`}
/>
</section>
{/* UIKit Integration */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">UIKit Integration</h2>
<CodeBlock
filename="ViewController.swift"
code={`import UIKit
import ConsentSDK
import Combine
class ViewController: UIViewController {
private var cancellables = Set<AnyCancellable>()
override func viewDidLoad() {
super.viewDidLoad()
// Reaktiv auf Consent-Aenderungen reagieren
ConsentManager.shared.$consent
.receive(on: DispatchQueue.main)
.sink { [weak self] state in
self?.updateUI(consent: state)
}
.store(in: &cancellables)
}
private func updateUI(consent: ConsentState?) {
if consent?.hasConsent(.analytics) == true {
loadAnalytics()
}
}
@IBAction func acceptAllTapped(_ sender: UIButton) {
Task {
await ConsentManager.shared.acceptAll()
}
}
}`}
/>
</section>
{/* Consent Categories */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Consent-Kategorien</h2>
<CodeBlock
code={`// Verfuegbare Kategorien
enum ConsentCategory {
case essential // Immer aktiv
case functional // Funktionale Features
case analytics // Statistik & Analyse
case marketing // Werbung & Tracking
case social // Social Media Integration
}
// Consent pruefen
if ConsentManager.shared.hasConsent(.analytics) {
// Analytics laden
}
// Mehrere Kategorien pruefen
if ConsentManager.shared.hasConsent([.analytics, .marketing]) {
// Beide Kategorien haben Consent
}`}
/>
</section>
{/* API Reference */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-4">API Referenz</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Methode</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">Beschreibung</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4"><code className="text-violet-600">configure()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">SDK konfigurieren</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">initialize()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">SDK initialisieren (async)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">hasConsent(_:)</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Consent fuer Kategorie pruefen</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">acceptAll()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Alle Kategorien akzeptieren (async)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">rejectAll()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Alle ablehnen (async)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">setConsent(_:)</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Spezifische Kategorien setzen (async)</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">showBanner()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Banner anzeigen</td>
</tr>
<tr>
<td className="px-6 py-4"><code className="text-violet-600">exportConsent()</code></td>
<td className="px-6 py-4 text-sm text-gray-600">Consent-Daten exportieren (DSGVO)</td>
</tr>
</tbody>
</table>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,95 +0,0 @@
'use client'
import React from 'react'
import Link from 'next/link'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { ChevronRight, Apple, Smartphone } from 'lucide-react'
const platforms = [
{
name: 'iOS (Swift)',
href: '/developers/sdk/consent/mobile/ios',
description: 'Native Swift SDK fuer iOS 15+ und iPadOS',
features: ['Swift 5.9+', 'iOS 15.0+', 'SwiftUI Support', 'Combine Integration'],
color: 'bg-gray-900',
icon: Apple,
},
{
name: 'Android (Kotlin)',
href: '/developers/sdk/consent/mobile/android',
description: 'Native Kotlin SDK fuer Android API 26+',
features: ['Kotlin 1.9+', 'API 26+', 'Jetpack Compose', 'Coroutines'],
color: 'bg-green-600',
icon: Smartphone,
},
{
name: 'Flutter',
href: '/developers/sdk/consent/mobile/flutter',
description: 'Cross-Platform SDK fuer Flutter 3.16+',
features: ['Dart 3.0+', 'Flutter 3.16+', 'iOS & Android', 'Web Support'],
color: 'bg-blue-500',
icon: Smartphone,
},
]
export default function MobileSDKsPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Mobile SDKs</h1>
<p className="text-lg text-gray-600 mb-8">
Native SDKs fuer iOS, Android und Flutter mit vollstaendiger DSGVO-Konformitaet.
</p>
<div className="space-y-4">
{platforms.map((platform) => (
<Link
key={platform.name}
href={platform.href}
className="block bg-white rounded-xl border border-gray-200 p-6 hover:border-violet-300 hover:shadow-md transition-all group"
>
<div className="flex items-start gap-4">
<div className={`w-12 h-12 rounded-xl ${platform.color} flex items-center justify-center shrink-0`}>
<platform.icon className="w-6 h-6 text-white" />
</div>
<div className="flex-1">
<div className="flex items-center gap-2">
<h2 className="text-xl font-semibold text-gray-900 group-hover:text-violet-600 transition-colors">
{platform.name}
</h2>
<ChevronRight className="w-5 h-5 text-gray-400 group-hover:text-violet-600 transition-colors" />
</div>
<p className="text-gray-600 mt-1">{platform.description}</p>
<div className="flex flex-wrap gap-2 mt-3">
{platform.features.map((feature) => (
<span
key={feature}
className="px-2 py-1 bg-gray-100 text-gray-600 text-xs rounded-md"
>
{feature}
</span>
))}
</div>
</div>
</div>
</Link>
))}
</div>
{/* Cross-Platform Note */}
<div className="mt-8 p-4 bg-blue-50 border border-blue-200 rounded-xl">
<h3 className="font-medium text-blue-900">Cross-Platform Konsistenz</h3>
<p className="text-sm text-blue-700 mt-1">
Alle Mobile SDKs bieten dieselbe API-Oberflaeche wie das Web SDK.
Consent-Daten werden ueber die API synchronisiert, sodass Benutzer auf allen Geraeten
denselben Consent-Status haben.
</p>
</div>
</div>
</main>
</div>
)
}

View File

@@ -1,262 +0,0 @@
'use client'
import React, { useState } from 'react'
import Link from 'next/link'
import {
Shield, Code, Download, Smartphone, FileCode, Lock,
ChevronRight, Copy, Check, Zap, Globe, Layers,
BookOpen, Terminal
} from 'lucide-react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
type Framework = 'npm' | 'yarn' | 'pnpm'
const installCommands: Record<Framework, string> = {
npm: 'npm install @breakpilot/consent-sdk',
yarn: 'yarn add @breakpilot/consent-sdk',
pnpm: 'pnpm add @breakpilot/consent-sdk',
}
function CopyButton({ text }: { text: string }) {
const [copied, setCopied] = useState(false)
const handleCopy = async () => {
await navigator.clipboard.writeText(text)
setCopied(true)
setTimeout(() => setCopied(false), 2000)
}
return (
<button
onClick={handleCopy}
className="p-2 hover:bg-gray-700 rounded transition-colors"
title="Kopieren"
>
{copied ? (
<Check className="w-4 h-4 text-green-400" />
) : (
<Copy className="w-4 h-4 text-gray-400" />
)}
</button>
)
}
export default function ConsentSDKHubPage() {
const [selectedPM, setSelectedPM] = useState<Framework>('npm')
const quickLinks = [
{
title: 'Installation',
description: 'SDK in wenigen Minuten einrichten',
href: '/developers/sdk/consent/installation',
icon: Download,
color: 'bg-blue-500',
},
{
title: 'API Referenz',
description: 'Vollstaendige API-Dokumentation',
href: '/developers/sdk/consent/api-reference',
icon: FileCode,
color: 'bg-purple-500',
},
{
title: 'Frameworks',
description: 'React, Vue, Angular Integration',
href: '/developers/sdk/consent/frameworks',
icon: Layers,
color: 'bg-green-500',
},
{
title: 'Mobile SDKs',
description: 'iOS, Android, Flutter',
href: '/developers/sdk/consent/mobile',
icon: Smartphone,
color: 'bg-orange-500',
},
{
title: 'Sicherheit',
description: 'Best Practices & Compliance',
href: '/developers/sdk/consent/security',
icon: Lock,
color: 'bg-red-500',
},
]
const features = [
{
title: 'DSGVO & TTDSG Konform',
description: 'Vollstaendige Unterstuetzung fuer EU-Datenschutzverordnungen mit revisionssicherer Consent-Speicherung.',
icon: Shield,
},
{
title: 'Google Consent Mode v2',
description: 'Native Integration mit automatischer Synchronisation zu Google Analytics und Ads.',
icon: Globe,
},
{
title: 'Script Blocking',
description: 'Automatisches Blockieren von Third-Party Scripts bis zur Einwilligung.',
icon: Code,
},
{
title: 'Multi-Platform',
description: 'Unterstuetzung fuer Web, PWA, iOS, Android und Flutter aus einer Codebasis.',
icon: Smartphone,
},
]
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-5xl mx-auto px-8 py-12">
{/* Header */}
<div className="mb-12">
<div className="flex items-center gap-3 mb-4">
<div className="w-12 h-12 rounded-xl bg-gradient-to-br from-violet-600 to-purple-600 flex items-center justify-center">
<Shield className="w-7 h-7 text-white" />
</div>
<div>
<h1 className="text-3xl font-bold text-gray-900">Consent SDK</h1>
<div className="flex items-center gap-2 mt-1">
<span className="px-2 py-0.5 bg-green-100 text-green-800 text-xs font-medium rounded-full">
v1.0.0
</span>
<span className="text-sm text-gray-500">DSGVO/TTDSG Compliant</span>
</div>
</div>
</div>
<p className="text-lg text-gray-600 max-w-3xl">
Das Consent SDK ermoeglicht DSGVO-konforme Einwilligungsverwaltung fuer Web, PWA und Mobile Apps.
Mit nativer Unterstuetzung fuer React, Vue, Angular und Mobile Platforms.
</p>
</div>
{/* Quick Install */}
<div className="mb-12 bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200 flex items-center justify-between">
<h2 className="font-semibold text-gray-900">Schnellinstallation</h2>
<div className="flex gap-1 bg-gray-100 rounded-lg p-1">
{(['npm', 'yarn', 'pnpm'] as const).map((pm) => (
<button
key={pm}
onClick={() => setSelectedPM(pm)}
className={`px-3 py-1 text-sm rounded-md transition-colors ${
selectedPM === pm
? 'bg-white text-gray-900 shadow-sm'
: 'text-gray-600 hover:text-gray-900'
}`}
>
{pm}
</button>
))}
</div>
</div>
<div className="bg-gray-900 px-6 py-4 flex items-center justify-between">
<code className="text-green-400 font-mono text-sm">
$ {installCommands[selectedPM]}
</code>
<CopyButton text={installCommands[selectedPM]} />
</div>
</div>
{/* Quick Links */}
<div className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Dokumentation</h2>
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
{quickLinks.map((link) => (
<Link
key={link.href}
href={link.href}
className="group p-4 bg-white rounded-xl border border-gray-200 hover:border-violet-300 hover:shadow-md transition-all"
>
<div className="flex items-start gap-3">
<div className={`w-10 h-10 rounded-lg ${link.color} flex items-center justify-center shrink-0`}>
<link.icon className="w-5 h-5 text-white" />
</div>
<div className="flex-1 min-w-0">
<h3 className="font-medium text-gray-900 group-hover:text-violet-600 transition-colors flex items-center gap-1">
{link.title}
<ChevronRight className="w-4 h-4 opacity-0 group-hover:opacity-100 transition-opacity" />
</h3>
<p className="text-sm text-gray-500 mt-1">{link.description}</p>
</div>
</div>
</Link>
))}
</div>
</div>
{/* Quick Start Code */}
<div className="mb-12 bg-white rounded-xl border border-gray-200 overflow-hidden">
<div className="px-6 py-4 border-b border-gray-200">
<h2 className="font-semibold text-gray-900">Schnellstart</h2>
</div>
<div className="bg-gray-900 p-6">
<pre className="text-sm text-gray-300 font-mono overflow-x-auto">
{`import { ConsentManager } from '@breakpilot/consent-sdk';
// Manager initialisieren
const consent = new ConsentManager({
apiEndpoint: 'https://api.example.com/consent',
siteId: 'your-site-id',
});
// SDK starten
await consent.init();
// Consent pruefen
if (consent.hasConsent('analytics')) {
// Analytics laden
}
// Events abonnieren
consent.on('change', (state) => {
console.log('Consent geaendert:', state);
});`}
</pre>
</div>
</div>
{/* Features */}
<div className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-4">Features</h2>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
{features.map((feature) => (
<div
key={feature.title}
className="p-4 bg-white rounded-xl border border-gray-200"
>
<div className="flex items-start gap-3">
<div className="w-10 h-10 rounded-lg bg-violet-100 flex items-center justify-center shrink-0">
<feature.icon className="w-5 h-5 text-violet-600" />
</div>
<div>
<h3 className="font-medium text-gray-900">{feature.title}</h3>
<p className="text-sm text-gray-500 mt-1">{feature.description}</p>
</div>
</div>
</div>
))}
</div>
</div>
{/* Compliance Notice */}
<div className="p-4 bg-blue-50 border border-blue-200 rounded-xl">
<div className="flex items-start gap-3">
<Shield className="w-5 h-5 text-blue-600 mt-0.5" />
<div>
<h3 className="font-medium text-blue-900">DSGVO & TTDSG Compliance</h3>
<p className="text-sm text-blue-700 mt-1">
Das Consent SDK erfuellt alle Anforderungen der DSGVO (Art. 6, 7, 13, 14, 17, 20) und des TTDSG (§ 25).
Alle Einwilligungen werden revisionssicher gespeichert und koennen jederzeit exportiert werden.
</p>
</div>
</div>
</div>
</div>
</main>
</div>
)
}

View File

@@ -1,290 +0,0 @@
'use client'
import React from 'react'
import { SDKDocsSidebar } from '@/components/developers/SDKDocsSidebar'
import { Shield, Lock, Eye, Database, Key, AlertTriangle, CheckCircle } from 'lucide-react'
function SecurityCard({
title,
description,
icon: Icon,
items,
}: {
title: string
description: string
icon: React.ComponentType<{ className?: string }>
items: string[]
}) {
return (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-violet-100 flex items-center justify-center shrink-0">
<Icon className="w-5 h-5 text-violet-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">{title}</h3>
<p className="text-sm text-gray-600 mt-1">{description}</p>
<ul className="mt-3 space-y-1">
{items.map((item, i) => (
<li key={i} className="flex items-center gap-2 text-sm text-gray-600">
<CheckCircle className="w-4 h-4 text-green-500 shrink-0" />
{item}
</li>
))}
</ul>
</div>
</div>
</div>
)
}
export default function SecurityPage() {
return (
<div className="min-h-screen bg-gray-50 flex">
<SDKDocsSidebar />
<main className="flex-1 ml-64">
<div className="max-w-4xl mx-auto px-8 py-12">
<h1 className="text-3xl font-bold text-gray-900 mb-2">Sicherheit & Compliance</h1>
<p className="text-lg text-gray-600 mb-8">
Best Practices fuer sichere Implementierung und DSGVO-konforme Nutzung des Consent SDK.
</p>
{/* Security Features */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Sicherheits-Features</h2>
<div className="grid gap-4">
<SecurityCard
title="Datenverschluesselung"
description="Alle Daten werden verschluesselt uebertragen und gespeichert."
icon={Lock}
items={[
'TLS 1.3 fuer alle API-Kommunikation',
'HMAC-Signatur fuer lokale Storage-Integritaet',
'Keine Klartextspeicherung sensibler Daten',
]}
/>
<SecurityCard
title="Datenschutzkonformes Fingerprinting"
description="Anonymisiertes Fingerprinting ohne invasive Techniken."
icon={Eye}
items={[
'Kein Canvas/WebGL/Audio Fingerprinting',
'Nur anonymisierte Browser-Eigenschaften',
'SHA-256 Hash der Komponenten',
'Nicht eindeutig identifizierend',
]}
/>
<SecurityCard
title="Sichere Speicherung"
description="Lokale Speicherung mit Manipulationsschutz."
icon={Database}
items={[
'Signierte localStorage-Eintraege',
'Automatische Signaturverifikation',
'HttpOnly Cookies fuer SSR',
'SameSite=Lax gegen CSRF',
]}
/>
<SecurityCard
title="API-Sicherheit"
description="Sichere Backend-Kommunikation."
icon={Key}
items={[
'Request-Signierung mit Timestamp',
'Credentials-Include fuer Session-Cookies',
'CORS-Konfiguration erforderlich',
'Rate-Limiting auf Server-Seite',
]}
/>
</div>
</section>
{/* DSGVO Compliance */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-6">DSGVO Compliance</h2>
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
DSGVO Artikel
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
Anforderung
</th>
<th className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase">
SDK-Unterstuetzung
</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-200">
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 6</td>
<td className="px-6 py-4 text-sm text-gray-600">Rechtmaessigkeit der Verarbeitung</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
Vollstaendig
</span>
</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 7</td>
<td className="px-6 py-4 text-sm text-gray-600">Bedingungen fuer Einwilligung</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
Vollstaendig
</span>
</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 13/14</td>
<td className="px-6 py-4 text-sm text-gray-600">Informationspflichten</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
Vollstaendig
</span>
</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 17</td>
<td className="px-6 py-4 text-sm text-gray-600">Recht auf Loeschung</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
Vollstaendig
</span>
</td>
</tr>
<tr>
<td className="px-6 py-4 text-sm font-medium text-gray-900">Art. 20</td>
<td className="px-6 py-4 text-sm text-gray-600">Datenportabilitaet</td>
<td className="px-6 py-4">
<span className="px-2 py-1 bg-green-100 text-green-800 text-xs rounded-full">
Vollstaendig
</span>
</td>
</tr>
</tbody>
</table>
</div>
</section>
{/* TTDSG Compliance */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-6">TTDSG Compliance</h2>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-start gap-4">
<div className="w-10 h-10 rounded-lg bg-blue-100 flex items-center justify-center shrink-0">
<Shield className="w-5 h-5 text-blue-600" />
</div>
<div>
<h3 className="font-semibold text-gray-900">§ 25 TTDSG - Schutz der Privatsphaere</h3>
<p className="text-sm text-gray-600 mt-1">
Das SDK erfuellt alle Anforderungen des § 25 TTDSG (Telemediengesetz):
</p>
<ul className="mt-3 space-y-2">
<li className="flex items-start gap-2 text-sm text-gray-600">
<CheckCircle className="w-4 h-4 text-green-500 shrink-0 mt-0.5" />
<span>
<strong>Einwilligung vor Speicherung:</strong> Cookies und localStorage werden erst nach
Einwilligung gesetzt (ausser technisch notwendige).
</span>
</li>
<li className="flex items-start gap-2 text-sm text-gray-600">
<CheckCircle className="w-4 h-4 text-green-500 shrink-0 mt-0.5" />
<span>
<strong>Informierte Einwilligung:</strong> Klare Kategorisierung und Beschreibung
aller Cookies und Tracker.
</span>
</li>
<li className="flex items-start gap-2 text-sm text-gray-600">
<CheckCircle className="w-4 h-4 text-green-500 shrink-0 mt-0.5" />
<span>
<strong>Widerrufsrecht:</strong> Jederzeit widerrufbare Einwilligung mit einem Klick.
</span>
</li>
</ul>
</div>
</div>
</div>
</section>
{/* Best Practices */}
<section className="mb-12">
<h2 className="text-xl font-semibold text-gray-900 mb-6">Best Practices</h2>
<div className="space-y-4">
<div className="bg-green-50 border border-green-200 rounded-xl p-4">
<h3 className="font-medium text-green-900 flex items-center gap-2">
<CheckCircle className="w-5 h-5" />
Empfohlen
</h3>
<ul className="mt-2 space-y-1 text-sm text-green-800">
<li> HTTPS fuer alle API-Aufrufe verwenden</li>
<li> Consent-Banner vor dem Laden von Third-Party Scripts anzeigen</li>
<li> Alle Kategorien klar und verstaendlich beschreiben</li>
<li> Ablehnen-Button gleichwertig zum Akzeptieren-Button darstellen</li>
<li> Consent-Aenderungen serverseitig protokollieren</li>
<li> Regelmaessige Ueberpruefung der Consent-Gultigkeit (recheckAfterDays)</li>
</ul>
</div>
<div className="bg-red-50 border border-red-200 rounded-xl p-4">
<h3 className="font-medium text-red-900 flex items-center gap-2">
<AlertTriangle className="w-5 h-5" />
Vermeiden
</h3>
<ul className="mt-2 space-y-1 text-sm text-red-800">
<li> Dark Patterns (versteckte Ablehnen-Buttons)</li>
<li> Pre-checked Consent-Optionen</li>
<li> Tracking vor Einwilligung</li>
<li> Cookie-Walls ohne echte Alternative</li>
<li> Unklare oder irrefuehrende Kategoriebezeichnungen</li>
</ul>
</div>
</div>
</section>
{/* Audit Trail */}
<section>
<h2 className="text-xl font-semibold text-gray-900 mb-6">Audit Trail</h2>
<div className="bg-white rounded-xl border border-gray-200 p-6">
<p className="text-gray-600 mb-4">
Das SDK speichert fuer jeden Consent-Vorgang revisionssichere Daten:
</p>
<div className="bg-gray-50 rounded-lg p-4 font-mono text-sm">
<pre className="text-gray-700">
{`{
"consentId": "consent_abc123...",
"timestamp": "2024-01-15T10:30:00.000Z",
"categories": {
"essential": true,
"analytics": true,
"marketing": false
},
"metadata": {
"userAgent": "Mozilla/5.0...",
"language": "de-DE",
"platform": "web",
"screenResolution": "1920x1080"
},
"signature": "sha256=...",
"version": "1.0.0"
}`}
</pre>
</div>
<p className="text-sm text-gray-500 mt-4">
Diese Daten werden sowohl lokal als auch auf dem Server gespeichert und koennen
jederzeit fuer Audits exportiert werden.
</p>
</div>
</section>
</div>
</main>
</div>
)
}

View File

@@ -1,186 +0,0 @@
import { DevPortalLayout, CodeBlock, InfoBox, ParameterTable } from '@/components/developers/DevPortalLayout'
export default function SDKInstallationPage() {
return (
<DevPortalLayout
title="SDK Installation"
description="Installationsanleitung fuer das AI Compliance SDK"
>
<h2>Voraussetzungen</h2>
<ul>
<li>Node.js 18 oder hoeher</li>
<li>React 18+ / Next.js 14+</li>
<li>TypeScript 5.0+ (empfohlen)</li>
</ul>
<h2>Installation</h2>
<p>
Installieren Sie das SDK ueber Ihren bevorzugten Paketmanager:
</p>
<CodeBlock language="bash" filename="npm">
{`npm install @breakpilot/compliance-sdk`}
</CodeBlock>
<CodeBlock language="bash" filename="yarn">
{`yarn add @breakpilot/compliance-sdk`}
</CodeBlock>
<CodeBlock language="bash" filename="pnpm">
{`pnpm add @breakpilot/compliance-sdk`}
</CodeBlock>
<h2>Peer Dependencies</h2>
<p>
Das SDK hat folgende Peer Dependencies, die automatisch installiert werden sollten:
</p>
<CodeBlock language="json" filename="package.json">
{`{
"peerDependencies": {
"react": ">=18.0.0",
"react-dom": ">=18.0.0"
}
}`}
</CodeBlock>
<h2>Zusaetzliche Pakete (optional)</h2>
<p>
Fuer erweiterte Funktionen koennen Sie folgende Pakete installieren:
</p>
<ParameterTable
parameters={[
{
name: 'jspdf',
type: 'npm package',
required: false,
description: 'Fuer PDF-Export (wird automatisch geladen wenn verfuegbar)',
},
{
name: 'jszip',
type: 'npm package',
required: false,
description: 'Fuer ZIP-Export aller Dokumente',
},
]}
/>
<h2>TypeScript Konfiguration</h2>
<p>
Das SDK ist vollstaendig in TypeScript geschrieben. Stellen Sie sicher,
dass Ihre tsconfig.json folgende Optionen enthaelt:
</p>
<CodeBlock language="json" filename="tsconfig.json">
{`{
"compilerOptions": {
"target": "ES2020",
"lib": ["dom", "dom.iterable", "esnext"],
"module": "esnext",
"moduleResolution": "bundler",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}`}
</CodeBlock>
<h2>Next.js Integration</h2>
<p>
Fuer Next.js 14+ mit App Router, fuegen Sie den Provider in Ihr Root-Layout ein:
</p>
<CodeBlock language="typescript" filename="app/layout.tsx">
{`import { SDKProvider } from '@breakpilot/compliance-sdk'
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="de">
<body>
<SDKProvider
tenantId={process.env.NEXT_PUBLIC_TENANT_ID!}
apiKey={process.env.BREAKPILOT_API_KEY}
enableBackendSync={true}
>
{children}
</SDKProvider>
</body>
</html>
)
}`}
</CodeBlock>
<InfoBox type="warning" title="Wichtig fuer Server Components">
Der SDKProvider ist ein Client-Component. Wenn Sie Server Components
verwenden, wrappen Sie nur die Teile der App, die das SDK benoetigen.
</InfoBox>
<h2>Umgebungsvariablen</h2>
<p>
Erstellen Sie eine .env.local Datei mit folgenden Variablen:
</p>
<CodeBlock language="bash" filename=".env.local">
{`# Pflicht
NEXT_PUBLIC_TENANT_ID=your-tenant-id
# Optional (fuer Backend-Sync)
BREAKPILOT_API_KEY=sk_live_...
# Optional (fuer Self-Hosted)
NEXT_PUBLIC_SDK_API_URL=https://your-server.com/sdk/v1`}
</CodeBlock>
<InfoBox type="info" title="API Key Sicherheit">
Der API Key sollte niemals im Frontend-Code oder in NEXT_PUBLIC_ Variablen
erscheinen. Verwenden Sie Server-Side API Routes fuer authentifizierte Anfragen.
</InfoBox>
<h2>Verifizierung</h2>
<p>
Testen Sie die Installation mit einer einfachen Komponente:
</p>
<CodeBlock language="typescript" filename="app/test/page.tsx">
{`'use client'
import { useSDK } from '@breakpilot/compliance-sdk'
export default function TestPage() {
const { state, completionPercentage } = useSDK()
return (
<div>
<h1>SDK Test</h1>
<p>Fortschritt: {completionPercentage}%</p>
<p>Aktuelle Phase: {state.currentPhase}</p>
<p>Use Cases: {state.useCases.length}</p>
</div>
)
}`}
</CodeBlock>
<h2>Fehlerbehebung</h2>
<h3>Error: useSDK must be used within SDKProvider</h3>
<p>
Stellen Sie sicher, dass der SDKProvider das gesamte Layout umschliesst
und dass Sie {'\'use client\''} in Client-Komponenten verwenden.
</p>
<h3>Error: Module not found</h3>
<p>
Loeschen Sie node_modules und package-lock.json, dann reinstallieren:
</p>
<CodeBlock language="bash" filename="Terminal">
{`rm -rf node_modules package-lock.json
npm install`}
</CodeBlock>
<h3>TypeScript Errors</h3>
<p>
Stellen Sie sicher, dass TypeScript 5.0+ installiert ist:
</p>
<CodeBlock language="bash" filename="Terminal">
{`npm install typescript@latest`}
</CodeBlock>
</DevPortalLayout>
)
}

View File

@@ -1,281 +0,0 @@
import Link from 'next/link'
import { DevPortalLayout, CodeBlock, InfoBox } from '@/components/developers/DevPortalLayout'
export default function SDKOverviewPage() {
return (
<DevPortalLayout
title="SDK Documentation"
description="TypeScript SDK für React und Next.js Integration"
>
<h2>Übersicht</h2>
<p>
Das AI Compliance SDK ist ein TypeScript-Paket für die Integration des
Compliance-Workflows in React und Next.js Anwendungen. Es bietet:
</p>
<ul>
<li>React Context Provider für State Management</li>
<li>Hooks für einfachen Zugriff auf Compliance-Daten</li>
<li>Automatische Synchronisation mit dem Backend</li>
<li>Offline-Support mit localStorage Fallback</li>
<li>Export-Funktionen (PDF, JSON, ZIP)</li>
</ul>
<h2>Kernkomponenten</h2>
<h3>SDKProvider</h3>
<p>
Der Provider wrappet Ihre App und stellt den SDK-Kontext bereit:
</p>
<CodeBlock language="typescript" filename="app/layout.tsx">
{`import { SDKProvider } from '@breakpilot/compliance-sdk'
export default function Layout({ children }) {
return (
<SDKProvider
tenantId="your-tenant"
enableBackendSync={true}
>
{children}
</SDKProvider>
)
}`}
</CodeBlock>
<h3>useSDK Hook</h3>
<p>
Der Haupt-Hook für den Zugriff auf alle SDK-Funktionen:
</p>
<CodeBlock language="typescript" filename="component.tsx">
{`import { useSDK } from '@breakpilot/compliance-sdk'
function MyComponent() {
const {
// State
state,
dispatch,
// Navigation
currentStep,
goToStep,
goToNextStep,
goToPreviousStep,
canGoNext,
canGoPrevious,
// Progress
completionPercentage,
phase1Completion,
phase2Completion,
// Checkpoints
validateCheckpoint,
overrideCheckpoint,
getCheckpointStatus,
// Data Updates
updateUseCase,
addRisk,
updateControl,
// Persistence
saveState,
loadState,
// Demo Data
seedDemoData,
clearDemoData,
isDemoDataLoaded,
// Sync
syncState,
forceSyncToServer,
isOnline,
// Export
exportState,
// Command Bar
isCommandBarOpen,
setCommandBarOpen,
} = useSDK()
return (
<div>
<h1>Progress: {completionPercentage}%</h1>
<button onClick={() => goToStep('risks')}>
Zur Risikoanalyse
</button>
</div>
)
}`}
</CodeBlock>
<h2>Types</h2>
<p>
Das SDK exportiert alle TypeScript-Types für volle Typsicherheit:
</p>
<CodeBlock language="typescript" filename="types.ts">
{`import type {
// Core Types
SDKState,
SDKAction,
SDKStep,
SDKPhase,
// Use Cases
UseCaseAssessment,
AssessmentResult,
// Risk Management
Risk,
RiskSeverity,
RiskMitigation,
// Controls & Evidence
Control,
Evidence,
Requirement,
// Checkpoints
Checkpoint,
CheckpointStatus,
ValidationError,
// DSFA
DSFA,
DSFASection,
DSFAApproval,
// TOMs & VVT
TOM,
ProcessingActivity,
RetentionPolicy,
// AI Act
AIActResult,
AIActRiskCategory,
} from '@breakpilot/compliance-sdk'`}
</CodeBlock>
<h2>Utility Functions</h2>
<p>
Hilfreiche Funktionen für die Arbeit mit dem SDK:
</p>
<CodeBlock language="typescript" filename="utils.ts">
{`import {
// Step Navigation
getStepById,
getStepByUrl,
getNextStep,
getPreviousStep,
getStepsForPhase,
// Risk Calculation
calculateRiskScore,
getRiskSeverityFromScore,
calculateResidualRisk,
// Progress
getCompletionPercentage,
getPhaseCompletionPercentage,
} from '@breakpilot/compliance-sdk'
// Beispiel: Risk Score berechnen
const inherentRisk = calculateRiskScore(4, 5) // likelihood * impact = 20
const severity = getRiskSeverityFromScore(20) // 'CRITICAL'`}
</CodeBlock>
<h2>API Client</h2>
<p>
Für direkten API-Zugriff ohne React Context:
</p>
<CodeBlock language="typescript" filename="api.ts">
{`import {
getSDKApiClient,
SDKApiClient,
} from '@breakpilot/compliance-sdk'
const client = getSDKApiClient('your-tenant-id')
// State laden
const state = await client.getState()
// State speichern
await client.saveState(updatedState)
// Checkpoint validieren
const result = await client.validateCheckpoint('CP-UC', state)
// Export
const blob = await client.exportState('pdf')`}
</CodeBlock>
<h2>RAG & LLM Client</h2>
<p>
Zugriff auf die RAG-Suche und Dokumentengenerierung:
</p>
<CodeBlock language="typescript" filename="rag.ts">
{`import {
getSDKBackendClient,
isLegalQuery,
} from '@breakpilot/compliance-sdk'
const client = getSDKBackendClient()
// RAG-Suche
const results = await client.search('DSGVO Art. 5', 5)
console.log(results) // SearchResult[]
// Dokumentengenerierung
const dsfa = await client.generateDSFA(context)
const toms = await client.generateTOM(context)
const vvt = await client.generateVVT(context)
// Prüfen ob eine Query rechtliche Inhalte betrifft
if (isLegalQuery('Einwilligung DSGVO')) {
// RAG-Suche durchführen
}`}
</CodeBlock>
<h2>Export</h2>
<p>
Exportieren Sie Compliance-Daten in verschiedenen Formaten:
</p>
<CodeBlock language="typescript" filename="export.ts">
{`import { exportToPDF, exportToZIP, downloadExport } from '@breakpilot/compliance-sdk'
// PDF Export
const pdfBlob = await exportToPDF(state)
downloadExport(pdfBlob, 'compliance-report.pdf')
// ZIP Export (alle Dokumente)
const zipBlob = await exportToZIP(state)
downloadExport(zipBlob, 'compliance-export.zip')
// Über den Hook
const { exportState } = useSDK()
const blob = await exportState('pdf') // 'json' | 'pdf' | 'zip'`}
</CodeBlock>
<InfoBox type="success" title="Weitere Dokumentation">
<ul className="list-disc list-inside space-y-1">
<li>
<Link href="/developers/sdk/installation" className="text-blue-600 hover:underline">
Installation Guide
</Link>
</li>
<li>
<Link href="/developers/sdk/configuration" className="text-blue-600 hover:underline">
Konfigurationsoptionen
</Link>
</li>
<li>
<Link href="/developers/guides/phase1" className="text-blue-600 hover:underline">
Phase 1 Workflow Guide
</Link>
</li>
</ul>
</InfoBox>
</DevPortalLayout>
)
}

View File

@@ -1,839 +0,0 @@
'use client'
import { useState, useEffect, useCallback } from 'react'
// =============================================================================
// TYPES
// =============================================================================
interface CrawlSource {
id: string
name: string
source_type: string
path: string
file_extensions: string[]
max_depth: number
exclude_patterns: string[]
enabled: boolean
created_at: string
}
interface CrawlJob {
id: string
source_id: string
source_name?: string
status: 'pending' | 'running' | 'completed' | 'failed' | 'cancelled'
job_type: 'full' | 'delta'
files_found: number
files_processed: number
files_new: number
files_changed: number
files_skipped: number
files_error: number
error_message?: string
started_at?: string
completed_at?: string
created_at: string
}
interface CrawlDocument {
id: string
file_name: string
file_extension: string
file_size_bytes: number
classification: string | null
classification_confidence: number | null
classification_corrected: boolean
extraction_status: string
archived: boolean
ipfs_cid: string | null
first_seen_at: string
last_seen_at: string
version_count: number
source_name?: string
}
interface OnboardingReport {
id: string
total_documents_found: number
classification_breakdown: Record<string, number>
gaps: GapItem[]
compliance_score: number
gap_summary?: { critical: number; high: number; medium: number }
created_at: string
}
interface GapItem {
id: string
category: string
description: string
severity: 'CRITICAL' | 'HIGH' | 'MEDIUM'
regulation: string
requiredAction: string
}
// =============================================================================
// API HELPERS
// =============================================================================
const TENANT_ID = '00000000-0000-0000-0000-000000000001' // Default tenant
async function api(path: string, options: RequestInit = {}) {
const res = await fetch(`/api/sdk/v1/crawler/${path}`, {
...options,
headers: {
'Content-Type': 'application/json',
'X-Tenant-ID': TENANT_ID,
...options.headers,
},
})
if (res.status === 204) return null
return res.json()
}
// =============================================================================
// CLASSIFICATION LABELS
// =============================================================================
const CLASSIFICATION_LABELS: Record<string, { label: string; color: string }> = {
VVT: { label: 'VVT', color: 'bg-blue-100 text-blue-700' },
TOM: { label: 'TOM', color: 'bg-green-100 text-green-700' },
DSE: { label: 'DSE', color: 'bg-purple-100 text-purple-700' },
AVV: { label: 'AVV', color: 'bg-orange-100 text-orange-700' },
DSFA: { label: 'DSFA', color: 'bg-red-100 text-red-700' },
Loeschkonzept: { label: 'Loeschkonzept', color: 'bg-yellow-100 text-yellow-700' },
Einwilligung: { label: 'Einwilligung', color: 'bg-pink-100 text-pink-700' },
Vertrag: { label: 'Vertrag', color: 'bg-indigo-100 text-indigo-700' },
Richtlinie: { label: 'Richtlinie', color: 'bg-teal-100 text-teal-700' },
Schulungsnachweis: { label: 'Schulung', color: 'bg-cyan-100 text-cyan-700' },
Sonstiges: { label: 'Sonstiges', color: 'bg-gray-100 text-gray-700' },
}
const ALL_CLASSIFICATIONS = Object.keys(CLASSIFICATION_LABELS)
// =============================================================================
// TAB: QUELLEN (Sources)
// =============================================================================
function SourcesTab() {
const [sources, setSources] = useState<CrawlSource[]>([])
const [loading, setLoading] = useState(true)
const [showForm, setShowForm] = useState(false)
const [formName, setFormName] = useState('')
const [formPath, setFormPath] = useState('')
const [testResult, setTestResult] = useState<Record<string, string>>({})
const loadSources = useCallback(async () => {
setLoading(true)
try {
const data = await api('sources')
setSources(data || [])
} catch { /* ignore */ }
setLoading(false)
}, [])
useEffect(() => { loadSources() }, [loadSources])
const handleCreate = async () => {
if (!formName || !formPath) return
await api('sources', {
method: 'POST',
body: JSON.stringify({ name: formName, path: formPath }),
})
setFormName('')
setFormPath('')
setShowForm(false)
loadSources()
}
const handleDelete = async (id: string) => {
await api(`sources/${id}`, { method: 'DELETE' })
loadSources()
}
const handleToggle = async (source: CrawlSource) => {
await api(`sources/${source.id}`, {
method: 'PUT',
body: JSON.stringify({ enabled: !source.enabled }),
})
loadSources()
}
const handleTest = async (id: string) => {
setTestResult(prev => ({ ...prev, [id]: 'testing...' }))
const result = await api(`sources/${id}/test`, { method: 'POST' })
setTestResult(prev => ({ ...prev, [id]: result?.message || 'Fehler' }))
}
return (
<div className="space-y-6">
<div className="flex justify-between items-center">
<h2 className="text-lg font-semibold text-gray-900">Crawl-Quellen</h2>
<button
onClick={() => setShowForm(!showForm)}
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700"
>
+ Neue Quelle
</button>
</div>
{showForm && (
<div className="bg-white rounded-xl border border-gray-200 p-6 space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Name</label>
<input
value={formName}
onChange={e => setFormName(e.target.value)}
placeholder="z.B. Compliance-Ordner"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
/>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Pfad (relativ zu /data/crawl)</label>
<input
value={formPath}
onChange={e => setFormPath(e.target.value)}
placeholder="z.B. compliance-docs"
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm focus:ring-2 focus:ring-purple-500"
/>
</div>
<div className="flex gap-2">
<button onClick={handleCreate} className="px-4 py-2 bg-green-600 text-white text-sm rounded-lg hover:bg-green-700">
Erstellen
</button>
<button onClick={() => setShowForm(false)} className="px-4 py-2 text-gray-600 text-sm hover:text-gray-800">
Abbrechen
</button>
</div>
</div>
)}
{loading ? (
<div className="text-center py-12 text-gray-500">Laden...</div>
) : sources.length === 0 ? (
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
<p className="text-lg font-medium">Keine Quellen konfiguriert</p>
<p className="text-sm mt-1">Erstellen Sie eine Crawl-Quelle um Dokumente zu scannen.</p>
</div>
) : (
<div className="space-y-3">
{sources.map(s => (
<div key={s.id} className="bg-white rounded-xl border border-gray-200 p-5 flex items-center gap-4">
<div className={`w-3 h-3 rounded-full ${s.enabled ? 'bg-green-500' : 'bg-gray-300'}`} />
<div className="flex-1 min-w-0">
<div className="font-medium text-gray-900">{s.name}</div>
<div className="text-sm text-gray-500 truncate">{s.path}</div>
<div className="text-xs text-gray-400 mt-1">
Tiefe: {s.max_depth} | Formate: {(typeof s.file_extensions === 'string' ? JSON.parse(s.file_extensions) : s.file_extensions).join(', ')}
</div>
</div>
{testResult[s.id] && (
<span className="text-xs text-gray-500 bg-gray-50 px-2 py-1 rounded">{testResult[s.id]}</span>
)}
<button onClick={() => handleTest(s.id)} className="text-sm text-blue-600 hover:text-blue-800">Testen</button>
<button onClick={() => handleToggle(s)} className="text-sm text-gray-600 hover:text-gray-800">
{s.enabled ? 'Deaktivieren' : 'Aktivieren'}
</button>
<button onClick={() => handleDelete(s.id)} className="text-sm text-red-600 hover:text-red-800">Loeschen</button>
</div>
))}
</div>
)}
</div>
)
}
// =============================================================================
// TAB: CRAWL-JOBS
// =============================================================================
function JobsTab() {
const [jobs, setJobs] = useState<CrawlJob[]>([])
const [sources, setSources] = useState<CrawlSource[]>([])
const [selectedSource, setSelectedSource] = useState('')
const [jobType, setJobType] = useState<'full' | 'delta'>('full')
const [loading, setLoading] = useState(true)
const loadData = useCallback(async () => {
setLoading(true)
try {
const [j, s] = await Promise.all([api('jobs'), api('sources')])
setJobs(j || [])
setSources(s || [])
if (!selectedSource && s?.length > 0) setSelectedSource(s[0].id)
} catch { /* ignore */ }
setLoading(false)
}, [selectedSource])
useEffect(() => { loadData() }, [loadData])
// Auto-refresh running jobs
useEffect(() => {
const hasRunning = jobs.some(j => j.status === 'running' || j.status === 'pending')
if (!hasRunning) return
const interval = setInterval(loadData, 3000)
return () => clearInterval(interval)
}, [jobs, loadData])
const handleTrigger = async () => {
if (!selectedSource) return
await api('jobs', {
method: 'POST',
body: JSON.stringify({ source_id: selectedSource, job_type: jobType }),
})
loadData()
}
const handleCancel = async (id: string) => {
await api(`jobs/${id}/cancel`, { method: 'POST' })
loadData()
}
const statusColor = (s: string) => {
switch (s) {
case 'completed': return 'bg-green-100 text-green-700'
case 'running': return 'bg-blue-100 text-blue-700'
case 'pending': return 'bg-yellow-100 text-yellow-700'
case 'failed': return 'bg-red-100 text-red-700'
case 'cancelled': return 'bg-gray-100 text-gray-600'
default: return 'bg-gray-100 text-gray-700'
}
}
return (
<div className="space-y-6">
{/* Trigger form */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="font-medium text-gray-900 mb-4">Neuen Crawl starten</h3>
<div className="flex gap-4 items-end">
<div className="flex-1">
<label className="block text-sm font-medium text-gray-700 mb-1">Quelle</label>
<select
value={selectedSource}
onChange={e => setSelectedSource(e.target.value)}
className="w-full px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
{sources.map(s => <option key={s.id} value={s.id}>{s.name}</option>)}
</select>
</div>
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">Typ</label>
<select
value={jobType}
onChange={e => setJobType(e.target.value as 'full' | 'delta')}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="full">Voll-Scan</option>
<option value="delta">Delta-Scan</option>
</select>
</div>
<button
onClick={handleTrigger}
disabled={!selectedSource}
className="px-6 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
Crawl starten
</button>
</div>
</div>
{/* Job list */}
{loading ? (
<div className="text-center py-8 text-gray-500">Laden...</div>
) : jobs.length === 0 ? (
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
Noch keine Crawl-Jobs ausgefuehrt.
</div>
) : (
<div className="space-y-3">
{jobs.map(job => (
<div key={job.id} className="bg-white rounded-xl border border-gray-200 p-5">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<span className={`px-2 py-1 text-xs font-medium rounded ${statusColor(job.status)}`}>
{job.status}
</span>
<span className="text-sm font-medium text-gray-900">{job.source_name || 'Quelle'}</span>
<span className="text-xs text-gray-400">{job.job_type === 'delta' ? 'Delta' : 'Voll'}</span>
</div>
<div className="flex items-center gap-2">
{(job.status === 'running' || job.status === 'pending') && (
<button onClick={() => handleCancel(job.id)} className="text-xs text-red-600 hover:text-red-800">
Abbrechen
</button>
)}
<span className="text-xs text-gray-400">
{new Date(job.created_at).toLocaleString('de-DE')}
</span>
</div>
</div>
{/* Progress */}
{job.status === 'running' && job.files_found > 0 && (
<div className="mb-3">
<div className="w-full h-2 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-purple-600 rounded-full transition-all"
style={{ width: `${(job.files_processed / job.files_found) * 100}%` }}
/>
</div>
<div className="text-xs text-gray-500 mt-1">
{job.files_processed} / {job.files_found} Dateien verarbeitet
</div>
</div>
)}
{/* Stats */}
<div className="grid grid-cols-6 gap-2 text-center">
<div className="bg-gray-50 rounded-lg p-2">
<div className="text-lg font-bold text-gray-900">{job.files_found}</div>
<div className="text-xs text-gray-500">Gefunden</div>
</div>
<div className="bg-gray-50 rounded-lg p-2">
<div className="text-lg font-bold text-gray-900">{job.files_processed}</div>
<div className="text-xs text-gray-500">Verarbeitet</div>
</div>
<div className="bg-green-50 rounded-lg p-2">
<div className="text-lg font-bold text-green-700">{job.files_new}</div>
<div className="text-xs text-green-600">Neu</div>
</div>
<div className="bg-blue-50 rounded-lg p-2">
<div className="text-lg font-bold text-blue-700">{job.files_changed}</div>
<div className="text-xs text-blue-600">Geaendert</div>
</div>
<div className="bg-gray-50 rounded-lg p-2">
<div className="text-lg font-bold text-gray-500">{job.files_skipped}</div>
<div className="text-xs text-gray-500">Uebersprungen</div>
</div>
<div className="bg-red-50 rounded-lg p-2">
<div className="text-lg font-bold text-red-700">{job.files_error}</div>
<div className="text-xs text-red-600">Fehler</div>
</div>
</div>
</div>
))}
</div>
)}
</div>
)
}
// =============================================================================
// TAB: DOKUMENTE
// =============================================================================
function DocumentsTab() {
const [docs, setDocs] = useState<CrawlDocument[]>([])
const [total, setTotal] = useState(0)
const [loading, setLoading] = useState(true)
const [filterClass, setFilterClass] = useState('')
const [archiving, setArchiving] = useState<Record<string, boolean>>({})
const loadDocs = useCallback(async () => {
setLoading(true)
try {
const params = filterClass ? `?classification=${filterClass}` : ''
const data = await api(`documents${params}`)
setDocs(data?.documents || [])
setTotal(data?.total || 0)
} catch { /* ignore */ }
setLoading(false)
}, [filterClass])
useEffect(() => { loadDocs() }, [loadDocs])
const handleReclassify = async (docId: string, newClass: string) => {
await api(`documents/${docId}/classify`, {
method: 'PUT',
body: JSON.stringify({ classification: newClass }),
})
loadDocs()
}
const handleArchive = async (docId: string) => {
setArchiving(prev => ({ ...prev, [docId]: true }))
try {
await api(`documents/${docId}/archive`, { method: 'POST' })
loadDocs()
} catch { /* ignore */ }
setArchiving(prev => ({ ...prev, [docId]: false }))
}
const formatSize = (bytes: number) => {
if (bytes < 1024) return `${bytes} B`
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
return `${(bytes / 1024 / 1024).toFixed(1)} MB`
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">{total} Dokumente</h2>
<select
value={filterClass}
onChange={e => setFilterClass(e.target.value)}
className="px-3 py-2 border border-gray-300 rounded-lg text-sm"
>
<option value="">Alle Kategorien</option>
{ALL_CLASSIFICATIONS.map(c => (
<option key={c} value={c}>{CLASSIFICATION_LABELS[c]?.label || c}</option>
))}
</select>
</div>
{loading ? (
<div className="text-center py-12 text-gray-500">Laden...</div>
) : docs.length === 0 ? (
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
Keine Dokumente gefunden. Starten Sie zuerst einen Crawl-Job.
</div>
) : (
<div className="bg-white rounded-xl border border-gray-200 overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50 text-gray-600">
<tr>
<th className="text-left px-4 py-3 font-medium">Datei</th>
<th className="text-left px-4 py-3 font-medium">Kategorie</th>
<th className="text-center px-4 py-3 font-medium">Konfidenz</th>
<th className="text-right px-4 py-3 font-medium">Groesse</th>
<th className="text-center px-4 py-3 font-medium">Archiv</th>
<th className="text-right px-4 py-3 font-medium">Aktionen</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{docs.map(doc => {
const cls = CLASSIFICATION_LABELS[doc.classification || ''] || CLASSIFICATION_LABELS['Sonstiges']
return (
<tr key={doc.id} className="hover:bg-gray-50">
<td className="px-4 py-3">
<div className="font-medium text-gray-900 truncate max-w-xs">{doc.file_name}</div>
<div className="text-xs text-gray-400">{doc.source_name}</div>
</td>
<td className="px-4 py-3">
<select
value={doc.classification || 'Sonstiges'}
onChange={e => handleReclassify(doc.id, e.target.value)}
className={`px-2 py-1 text-xs font-medium rounded border-0 ${cls.color}`}
>
{ALL_CLASSIFICATIONS.map(c => (
<option key={c} value={c}>{CLASSIFICATION_LABELS[c].label}</option>
))}
</select>
{doc.classification_corrected && (
<span className="ml-1 text-xs text-orange-500" title="Manuell korrigiert">*</span>
)}
</td>
<td className="px-4 py-3 text-center">
{doc.classification_confidence != null && (
<div className="inline-flex items-center gap-1">
<div className="w-12 h-1.5 bg-gray-200 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full"
style={{ width: `${doc.classification_confidence * 100}%` }}
/>
</div>
<span className="text-xs text-gray-500">
{(doc.classification_confidence * 100).toFixed(0)}%
</span>
</div>
)}
</td>
<td className="px-4 py-3 text-right text-gray-500">{formatSize(doc.file_size_bytes)}</td>
<td className="px-4 py-3 text-center">
{doc.archived ? (
<span className="text-green-600 text-xs font-medium" title={doc.ipfs_cid || ''}>IPFS</span>
) : (
<span className="text-gray-400 text-xs">-</span>
)}
</td>
<td className="px-4 py-3 text-right">
{!doc.archived && (
<button
onClick={() => handleArchive(doc.id)}
disabled={archiving[doc.id]}
className="text-xs text-purple-600 hover:text-purple-800 disabled:opacity-50"
>
{archiving[doc.id] ? 'Archiviert...' : 'Archivieren'}
</button>
)}
</td>
</tr>
)
})}
</tbody>
</table>
</div>
)}
</div>
)
}
// =============================================================================
// TAB: ONBOARDING-REPORT
// =============================================================================
function ReportTab() {
const [reports, setReports] = useState<OnboardingReport[]>([])
const [activeReport, setActiveReport] = useState<OnboardingReport | null>(null)
const [loading, setLoading] = useState(true)
const [generating, setGenerating] = useState(false)
const loadReports = useCallback(async () => {
setLoading(true)
try {
const data = await api('reports')
setReports(data || [])
if (data?.length > 0 && !activeReport) {
const detail = await api(`reports/${data[0].id}`)
setActiveReport(detail)
}
} catch { /* ignore */ }
setLoading(false)
}, [activeReport])
useEffect(() => { loadReports() }, [loadReports])
const handleGenerate = async () => {
setGenerating(true)
try {
const result = await api('reports/generate', {
method: 'POST',
body: JSON.stringify({}),
})
setActiveReport(result)
loadReports()
} catch { /* ignore */ }
setGenerating(false)
}
const handleSelectReport = async (id: string) => {
const detail = await api(`reports/${id}`)
setActiveReport(detail)
}
// Compliance score ring
const ComplianceRing = ({ score }: { score: number }) => {
const radius = 50
const circumference = 2 * Math.PI * radius
const offset = circumference - (score / 100) * circumference
const color = score >= 75 ? '#16a34a' : score >= 50 ? '#f59e0b' : '#dc2626'
return (
<div className="relative w-36 h-36">
<svg className="w-full h-full -rotate-90">
<circle cx="68" cy="68" r={radius} fill="none" stroke="#e5e7eb" strokeWidth="8" />
<circle
cx="68" cy="68" r={radius} fill="none"
stroke={color} strokeWidth="8"
strokeDasharray={circumference}
strokeDashoffset={offset}
strokeLinecap="round"
className="transition-all duration-1000"
/>
</svg>
<div className="absolute inset-0 flex flex-col items-center justify-center">
<span className="text-3xl font-bold" style={{ color }}>{score.toFixed(0)}%</span>
<span className="text-xs text-gray-500">Compliance</span>
</div>
</div>
)
}
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold text-gray-900">Onboarding-Report</h2>
<button
onClick={handleGenerate}
disabled={generating}
className="px-4 py-2 bg-purple-600 text-white text-sm font-medium rounded-lg hover:bg-purple-700 disabled:opacity-50"
>
{generating ? 'Generiere...' : 'Neuen Report erstellen'}
</button>
</div>
{/* Report selector */}
{reports.length > 1 && (
<div className="flex gap-2 overflow-x-auto pb-2">
{reports.map(r => (
<button
key={r.id}
onClick={() => handleSelectReport(r.id)}
className={`px-3 py-1.5 text-xs rounded-lg border whitespace-nowrap ${
activeReport?.id === r.id
? 'bg-purple-50 border-purple-300 text-purple-700'
: 'bg-white border-gray-200 text-gray-600 hover:bg-gray-50'
}`}
>
{new Date(r.created_at).toLocaleString('de-DE')} {r.compliance_score.toFixed(0)}%
</button>
))}
</div>
)}
{loading ? (
<div className="text-center py-12 text-gray-500">Laden...</div>
) : !activeReport ? (
<div className="text-center py-12 text-gray-500 bg-white rounded-xl border border-gray-200">
<p className="text-lg font-medium">Kein Report vorhanden</p>
<p className="text-sm mt-1">Fuehren Sie zuerst einen Crawl durch und generieren Sie dann einen Report.</p>
</div>
) : (
<div className="space-y-6">
{/* Score + Stats */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<div className="flex items-center gap-8">
<ComplianceRing score={activeReport.compliance_score} />
<div className="flex-1 grid grid-cols-3 gap-4">
<div className="text-center p-4 bg-gray-50 rounded-xl">
<div className="text-3xl font-bold text-gray-900">{activeReport.total_documents_found}</div>
<div className="text-sm text-gray-500">Dokumente gefunden</div>
</div>
<div className="text-center p-4 bg-gray-50 rounded-xl">
<div className="text-3xl font-bold text-gray-900">
{Object.keys(activeReport.classification_breakdown || {}).length}
</div>
<div className="text-sm text-gray-500">Kategorien abgedeckt</div>
</div>
<div className="text-center p-4 bg-gray-50 rounded-xl">
<div className="text-3xl font-bold text-red-600">
{(activeReport.gaps || []).length}
</div>
<div className="text-sm text-gray-500">Luecken identifiziert</div>
</div>
</div>
</div>
</div>
{/* Classification breakdown */}
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="font-medium text-gray-900 mb-4">Dokumenten-Verteilung</h3>
<div className="flex flex-wrap gap-2">
{Object.entries(activeReport.classification_breakdown || {}).map(([cat, count]) => {
const cls = CLASSIFICATION_LABELS[cat] || CLASSIFICATION_LABELS['Sonstiges']
return (
<span key={cat} className={`px-3 py-1.5 text-sm font-medium rounded-lg ${cls.color}`}>
{cls.label}: {count as number}
</span>
)
})}
{Object.keys(activeReport.classification_breakdown || {}).length === 0 && (
<span className="text-gray-400 text-sm">Keine Dokumente klassifiziert</span>
)}
</div>
</div>
{/* Gap summary */}
{activeReport.gap_summary && (
<div className="grid grid-cols-3 gap-4">
<div className="text-center p-4 bg-red-50 rounded-xl border border-red-100">
<div className="text-3xl font-bold text-red-600">{activeReport.gap_summary.critical}</div>
<div className="text-sm text-red-600 font-medium">Kritisch</div>
</div>
<div className="text-center p-4 bg-orange-50 rounded-xl border border-orange-100">
<div className="text-3xl font-bold text-orange-600">{activeReport.gap_summary.high}</div>
<div className="text-sm text-orange-600 font-medium">Hoch</div>
</div>
<div className="text-center p-4 bg-yellow-50 rounded-xl border border-yellow-100">
<div className="text-3xl font-bold text-yellow-600">{activeReport.gap_summary.medium}</div>
<div className="text-sm text-yellow-600 font-medium">Mittel</div>
</div>
</div>
)}
{/* Gap details */}
{(activeReport.gaps || []).length > 0 && (
<div className="bg-white rounded-xl border border-gray-200 p-6">
<h3 className="font-medium text-gray-900 mb-4">Compliance-Luecken</h3>
<div className="space-y-3">
{activeReport.gaps.map((gap) => (
<div
key={gap.id}
className={`p-4 rounded-lg border-l-4 ${
gap.severity === 'CRITICAL'
? 'bg-red-50 border-red-500'
: gap.severity === 'HIGH'
? 'bg-orange-50 border-orange-500'
: 'bg-yellow-50 border-yellow-500'
}`}
>
<div className="flex items-start justify-between">
<div>
<div className="font-medium text-gray-900">{gap.category}</div>
<p className="text-sm text-gray-600 mt-1">{gap.description}</p>
</div>
<span className={`px-2 py-1 text-xs font-medium rounded ${
gap.severity === 'CRITICAL' ? 'bg-red-100 text-red-700'
: gap.severity === 'HIGH' ? 'bg-orange-100 text-orange-700'
: 'bg-yellow-100 text-yellow-700'
}`}>
{gap.severity}
</span>
</div>
<div className="mt-2 text-xs text-gray-500">
Regulierung: {gap.regulation} | Aktion: {gap.requiredAction}
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
</div>
)
}
// =============================================================================
// MAIN PAGE
// =============================================================================
type Tab = 'sources' | 'jobs' | 'documents' | 'report'
export default function DocumentCrawlerPage() {
const [activeTab, setActiveTab] = useState<Tab>('sources')
const tabs: { id: Tab; label: string }[] = [
{ id: 'sources', label: 'Quellen' },
{ id: 'jobs', label: 'Crawl-Jobs' },
{ id: 'documents', label: 'Dokumente' },
{ id: 'report', label: 'Onboarding-Report' },
]
return (
<div className="max-w-6xl mx-auto space-y-6">
{/* Header */}
<div>
<h1 className="text-2xl font-bold text-gray-900">Document Crawler & Auto-Onboarding</h1>
<p className="mt-1 text-gray-500">
Automatisches Scannen von Dateisystemen, KI-Klassifizierung, IPFS-Archivierung und Compliance Gap-Analyse.
</p>
</div>
{/* Tabs */}
<div className="border-b border-gray-200">
<nav className="flex gap-6">
{tabs.map(tab => (
<button
key={tab.id}
onClick={() => setActiveTab(tab.id)}
className={`pb-3 text-sm font-medium border-b-2 transition-colors ${
activeTab === tab.id
? 'border-purple-600 text-purple-600'
: 'border-transparent text-gray-500 hover:text-gray-700'
}`}
>
{tab.label}
</button>
))}
</nav>
</div>
{/* Tab content */}
{activeTab === 'sources' && <SourcesTab />}
{activeTab === 'jobs' && <JobsTab />}
{activeTab === 'documents' && <DocumentsTab />}
{activeTab === 'report' && <ReportTab />}
</div>
)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,879 +0,0 @@
'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

@@ -1,225 +0,0 @@
/**
* 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

@@ -1,184 +0,0 @@
/**
* 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

@@ -1,168 +0,0 @@
/**
* 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

@@ -1,188 +0,0 @@
/**
* 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

@@ -1,136 +0,0 @@
/**
* 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

@@ -1,235 +0,0 @@
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

@@ -1,114 +0,0 @@
/**
* Document Crawler API Proxy - Catch-all route
* Proxies all /api/sdk/v1/crawler/* requests to document-crawler service (port 8098)
*/
import { NextRequest, NextResponse } from 'next/server'
const CRAWLER_BACKEND_URL = process.env.CRAWLER_API_URL || 'http://document-crawler:8098'
async function proxyRequest(
request: NextRequest,
pathSegments: string[] | undefined,
method: string
) {
const pathStr = pathSegments?.join('/') || ''
const searchParams = request.nextUrl.searchParams.toString()
const basePath = `${CRAWLER_BACKEND_URL}/api/v1/crawler`
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 non-GET requests
if (method !== 'GET' && method !== 'DELETE') {
try {
const body = await request.json()
fetchOptions.body = JSON.stringify(body)
} catch {
// No body or non-JSON body
}
}
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 }
)
}
// Handle 204 No Content
if (response.status === 204) {
return new NextResponse(null, { status: 204 })
}
const data = await response.json()
return NextResponse.json(data)
} catch (error) {
console.error('Document Crawler API proxy error:', error)
return NextResponse.json(
{ error: 'Verbindung zum Document Crawler 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

@@ -1,52 +0,0 @@
/**
* 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

@@ -1,77 +0,0 @@
/**
* 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

@@ -1,214 +0,0 @@
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

@@ -1,109 +0,0 @@
/**
* 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

@@ -1,130 +0,0 @@
/**
* 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

@@ -1,255 +0,0 @@
/**
* 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

@@ -1,369 +0,0 @@
/**
* 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

@@ -1,215 +0,0 @@
/**
* 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

@@ -1,256 +0,0 @@
/**
* 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

@@ -1,186 +0,0 @@
/**
* 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

@@ -1,88 +0,0 @@
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

@@ -1,150 +0,0 @@
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

@@ -1,309 +0,0 @@
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

@@ -1,137 +0,0 @@
/**
* 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

@@ -1,74 +0,0 @@
/**
* 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

@@ -1,111 +0,0 @@
/**
* 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

@@ -1,75 +0,0 @@
/**
* 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

@@ -1,111 +0,0 @@
/**
* 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

@@ -1,345 +0,0 @@
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

@@ -1,107 +0,0 @@
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

@@ -1,128 +0,0 @@
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

@@ -1,121 +0,0 @@
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

@@ -1,153 +0,0 @@
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

@@ -1,155 +0,0 @@
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

@@ -1,245 +0,0 @@
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

@@ -1,205 +0,0 @@
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

@@ -1,250 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,40 +0,0 @@
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

@@ -1,197 +0,0 @@
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

@@ -1,88 +0,0 @@
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

@@ -1,28 +0,0 @@
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

@@ -1,75 +0,0 @@
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

@@ -1,44 +0,0 @@
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

@@ -1,118 +0,0 @@
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

@@ -1,43 +0,0 @@
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

@@ -1,70 +0,0 @@
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

@@ -1,84 +0,0 @@
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

@@ -1,82 +0,0 @@
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

@@ -1,135 +0,0 @@
/**
* 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

@@ -1,147 +0,0 @@
/**
* 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

@@ -1,324 +0,0 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { exportToPDF, exportToZIP, downloadExport } from '../export'
import type { SDKState } from '../types'
// Mock jsPDF as a class
vi.mock('jspdf', () => {
return {
default: class MockJsPDF {
internal = {
pageSize: { getWidth: () => 210, getHeight: () => 297 },
}
setFillColor = vi.fn().mockReturnThis()
setDrawColor = vi.fn().mockReturnThis()
setTextColor = vi.fn().mockReturnThis()
setFontSize = vi.fn().mockReturnThis()
setFont = vi.fn().mockReturnThis()
setLineWidth = vi.fn().mockReturnThis()
text = vi.fn().mockReturnThis()
line = vi.fn().mockReturnThis()
rect = vi.fn().mockReturnThis()
roundedRect = vi.fn().mockReturnThis()
circle = vi.fn().mockReturnThis()
addPage = vi.fn().mockReturnThis()
setPage = vi.fn().mockReturnThis()
getNumberOfPages = vi.fn(() => 5)
splitTextToSize = vi.fn((text: string) => [text])
output = vi.fn(() => new Blob(['mock-pdf'], { type: 'application/pdf' }))
},
}
})
// Mock JSZip as a class
vi.mock('jszip', () => {
return {
default: class MockJSZip {
private mockFolder = {
file: vi.fn().mockReturnThis(),
folder: vi.fn(() => this.mockFolder),
}
folder = vi.fn(() => this.mockFolder)
generateAsync = vi.fn(() => Promise.resolve(new Blob(['mock-zip'], { type: 'application/zip' })))
},
}
})
const createMockState = (overrides: Partial<SDKState> = {}): SDKState => ({
version: '1.0.0',
lastModified: new Date('2024-01-15'),
tenantId: 'test-tenant',
userId: 'test-user',
subscription: 'PROFESSIONAL',
currentPhase: 1,
currentStep: 'use-case-workshop',
completedSteps: ['use-case-workshop', 'screening'],
checkpoints: {
'CP-UC': {
checkpointId: 'CP-UC',
passed: true,
validatedAt: new Date(),
validatedBy: 'SYSTEM',
errors: [],
warnings: [],
},
},
useCases: [
{
id: 'uc-1',
name: 'Test Use Case',
description: 'A test use case for testing',
category: 'Marketing',
stepsCompleted: 3,
steps: [
{ id: 's1', name: 'Step 1', completed: true, data: {} },
{ id: 's2', name: 'Step 2', completed: true, data: {} },
{ id: 's3', name: 'Step 3', completed: true, data: {} },
],
assessmentResult: null,
createdAt: new Date(),
updatedAt: new Date(),
},
],
activeUseCase: 'uc-1',
screening: null,
modules: [],
requirements: [],
controls: [
{
id: 'ctrl-1',
name: 'Test Control',
description: 'A test control',
type: 'TECHNICAL',
category: 'Access Control',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: [],
owner: 'Test Owner',
dueDate: null,
},
],
evidence: [],
checklist: [],
risks: [
{
id: 'risk-1',
title: 'Test Risk',
description: 'A test risk',
category: 'Security',
likelihood: 3,
impact: 4,
severity: 'HIGH',
inherentRiskScore: 12,
residualRiskScore: 6,
status: 'MITIGATED',
mitigation: [
{
id: 'mit-1',
description: 'Test mitigation',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 80,
controlId: 'ctrl-1',
},
],
owner: 'Risk Owner',
relatedControls: ['ctrl-1'],
relatedRequirements: [],
},
],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
sbom: null,
securityIssues: [],
securityBacklog: [],
commandBarHistory: [],
recentSearches: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
...overrides,
})
describe('exportToPDF', () => {
it('should return a Blob', async () => {
const state = createMockState()
const result = await exportToPDF(state)
expect(result).toBeInstanceOf(Blob)
})
it('should create a PDF with the correct type', async () => {
const state = createMockState()
const result = await exportToPDF(state)
expect(result.type).toBe('application/pdf')
})
it('should handle empty state', async () => {
const emptyState = createMockState({
useCases: [],
risks: [],
controls: [],
completedSteps: [],
})
const result = await exportToPDF(emptyState)
expect(result).toBeInstanceOf(Blob)
})
it('should handle state with multiple risks of different severities', async () => {
const state = createMockState({
risks: [
{
id: 'risk-1',
title: 'Critical Risk',
description: 'Critical',
category: 'Security',
likelihood: 5,
impact: 5,
severity: 'CRITICAL',
inherentRiskScore: 25,
residualRiskScore: 15,
status: 'IDENTIFIED',
mitigation: [],
owner: null,
relatedControls: [],
relatedRequirements: [],
},
{
id: 'risk-2',
title: 'Low Risk',
description: 'Low',
category: 'Operational',
likelihood: 1,
impact: 1,
severity: 'LOW',
inherentRiskScore: 1,
residualRiskScore: 1,
status: 'ACCEPTED',
mitigation: [],
owner: null,
relatedControls: [],
relatedRequirements: [],
},
],
})
const result = await exportToPDF(state)
expect(result).toBeInstanceOf(Blob)
})
})
describe('exportToZIP', () => {
it('should return a Blob', async () => {
const state = createMockState()
const result = await exportToZIP(state)
expect(result).toBeInstanceOf(Blob)
})
it('should create a ZIP with the correct type', async () => {
const state = createMockState()
const result = await exportToZIP(state)
expect(result.type).toBe('application/zip')
})
it('should handle empty state', async () => {
const emptyState = createMockState({
useCases: [],
risks: [],
controls: [],
completedSteps: [],
})
const result = await exportToZIP(emptyState)
expect(result).toBeInstanceOf(Blob)
})
it('should respect includeEvidence option', async () => {
const state = createMockState()
const result = await exportToZIP(state, { includeEvidence: false })
expect(result).toBeInstanceOf(Blob)
})
it('should respect includeDocuments option', async () => {
const state = createMockState()
const result = await exportToZIP(state, { includeDocuments: false })
expect(result).toBeInstanceOf(Blob)
})
})
describe('downloadExport', () => {
let mockCreateElement: ReturnType<typeof vi.spyOn>
let mockAppendChild: ReturnType<typeof vi.spyOn>
let mockRemoveChild: ReturnType<typeof vi.spyOn>
let mockLink: { href: string; download: string; click: ReturnType<typeof vi.fn> }
beforeEach(() => {
mockLink = {
href: '',
download: '',
click: vi.fn(),
}
mockCreateElement = vi.spyOn(document, 'createElement').mockReturnValue(mockLink as unknown as HTMLElement)
mockAppendChild = vi.spyOn(document.body, 'appendChild').mockImplementation(() => mockLink as unknown as HTMLElement)
mockRemoveChild = vi.spyOn(document.body, 'removeChild').mockImplementation(() => mockLink as unknown as HTMLElement)
})
it('should download JSON format', async () => {
const state = createMockState()
await downloadExport(state, 'json')
expect(mockLink.download).toContain('.json')
expect(mockLink.click).toHaveBeenCalled()
})
it('should download PDF format', async () => {
const state = createMockState()
await downloadExport(state, 'pdf')
expect(mockLink.download).toContain('.pdf')
expect(mockLink.click).toHaveBeenCalled()
})
it('should download ZIP format', async () => {
const state = createMockState()
await downloadExport(state, 'zip')
expect(mockLink.download).toContain('.zip')
expect(mockLink.click).toHaveBeenCalled()
})
it('should include date in filename', async () => {
const state = createMockState()
await downloadExport(state, 'json')
// Check that filename contains a date pattern
expect(mockLink.download).toMatch(/ai-compliance-sdk-\d{4}-\d{2}-\d{2}\.json/)
})
it('should throw error for unknown format', async () => {
const state = createMockState()
await expect(downloadExport(state, 'unknown' as any)).rejects.toThrow('Unknown export format')
})
})

View File

@@ -1,250 +0,0 @@
import { describe, it, expect } from 'vitest'
import {
SDK_STEPS,
getStepById,
getStepByUrl,
getNextStep,
getPreviousStep,
getCompletionPercentage,
getPhaseCompletionPercentage,
type SDKState,
} from '../types'
describe('SDK_STEPS', () => {
it('should have steps defined for both phases', () => {
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1)
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2)
expect(phase1Steps.length).toBeGreaterThan(0)
expect(phase2Steps.length).toBeGreaterThan(0)
})
it('should have unique IDs for all steps', () => {
const ids = SDK_STEPS.map(s => s.id)
const uniqueIds = new Set(ids)
expect(uniqueIds.size).toBe(ids.length)
})
it('should have unique URLs for all steps', () => {
const urls = SDK_STEPS.map(s => s.url)
const uniqueUrls = new Set(urls)
expect(uniqueUrls.size).toBe(urls.length)
})
it('should have checkpoint IDs for all steps', () => {
SDK_STEPS.forEach(step => {
expect(step.checkpointId).toBeDefined()
expect(step.checkpointId.length).toBeGreaterThan(0)
})
})
})
describe('getStepById', () => {
it('should return the correct step for a valid ID', () => {
const step = getStepById('use-case-workshop')
expect(step).toBeDefined()
expect(step?.name).toBe('Use Case Workshop')
})
it('should return undefined for an invalid ID', () => {
const step = getStepById('invalid-step-id')
expect(step).toBeUndefined()
})
it('should find steps in Phase 2', () => {
const step = getStepById('dsfa')
expect(step).toBeDefined()
expect(step?.phase).toBe(2)
})
})
describe('getStepByUrl', () => {
it('should return the correct step for a valid URL', () => {
const step = getStepByUrl('/sdk/advisory-board')
expect(step).toBeDefined()
expect(step?.id).toBe('use-case-workshop')
})
it('should return undefined for an invalid URL', () => {
const step = getStepByUrl('/invalid/url')
expect(step).toBeUndefined()
})
it('should find Phase 2 steps by URL', () => {
const step = getStepByUrl('/sdk/dsfa')
expect(step).toBeDefined()
expect(step?.id).toBe('dsfa')
})
})
describe('getNextStep', () => {
it('should return the next step in sequence', () => {
const nextStep = getNextStep('use-case-workshop')
expect(nextStep).toBeDefined()
expect(nextStep?.id).toBe('screening')
})
it('should return undefined for the last step', () => {
const lastStep = SDK_STEPS[SDK_STEPS.length - 1]
const nextStep = getNextStep(lastStep.id)
expect(nextStep).toBeUndefined()
})
it('should handle transition between phases', () => {
const lastPhase1Step = SDK_STEPS.filter(s => s.phase === 1).pop()
expect(lastPhase1Step).toBeDefined()
const nextStep = getNextStep(lastPhase1Step!.id)
expect(nextStep?.phase).toBe(2)
})
})
describe('getPreviousStep', () => {
it('should return the previous step in sequence', () => {
const prevStep = getPreviousStep('screening')
expect(prevStep).toBeDefined()
expect(prevStep?.id).toBe('use-case-workshop')
})
it('should return undefined for the first step', () => {
const prevStep = getPreviousStep('use-case-workshop')
expect(prevStep).toBeUndefined()
})
})
describe('getCompletionPercentage', () => {
const createMockState = (completedSteps: string[]): SDKState => ({
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'test',
subscription: 'PROFESSIONAL',
currentPhase: 1,
currentStep: 'use-case-workshop',
completedSteps,
checkpoints: {},
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
sbom: null,
securityIssues: [],
securityBacklog: [],
commandBarHistory: [],
recentSearches: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
})
it('should return 0 for no completed steps', () => {
const state = createMockState([])
const percentage = getCompletionPercentage(state)
expect(percentage).toBe(0)
})
it('should return 100 for all completed steps', () => {
const allStepIds = SDK_STEPS.map(s => s.id)
const state = createMockState(allStepIds)
const percentage = getCompletionPercentage(state)
expect(percentage).toBe(100)
})
it('should calculate correct percentage for partial completion', () => {
const halfSteps = SDK_STEPS.slice(0, Math.floor(SDK_STEPS.length / 2)).map(s => s.id)
const state = createMockState(halfSteps)
const percentage = getCompletionPercentage(state)
expect(percentage).toBeGreaterThan(40)
expect(percentage).toBeLessThan(60)
})
})
describe('getPhaseCompletionPercentage', () => {
const createMockState = (completedSteps: string[]): SDKState => ({
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'test',
subscription: 'PROFESSIONAL',
currentPhase: 1,
currentStep: 'use-case-workshop',
completedSteps,
checkpoints: {},
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
sbom: null,
securityIssues: [],
securityBacklog: [],
commandBarHistory: [],
recentSearches: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
})
it('should return 0 for Phase 1 with no completed steps', () => {
const state = createMockState([])
const percentage = getPhaseCompletionPercentage(state, 1)
expect(percentage).toBe(0)
})
it('should return 100 for Phase 1 when all Phase 1 steps are complete', () => {
const phase1Steps = SDK_STEPS.filter(s => s.phase === 1).map(s => s.id)
const state = createMockState(phase1Steps)
const percentage = getPhaseCompletionPercentage(state, 1)
expect(percentage).toBe(100)
})
it('should not count Phase 2 steps in Phase 1 percentage', () => {
const phase2Steps = SDK_STEPS.filter(s => s.phase === 2).map(s => s.id)
const state = createMockState(phase2Steps)
const percentage = getPhaseCompletionPercentage(state, 1)
expect(percentage).toBe(0)
})
})

File diff suppressed because it is too large Load Diff

View File

@@ -1,722 +0,0 @@
import type { ScopeProfilingAnswer, ComplianceDepthLevel, ScopeDocumentType } from './compliance-scope-types'
export interface GoldenTest {
id: string
name: string
description: string
answers: ScopeProfilingAnswer[]
expectedLevel: ComplianceDepthLevel | null // null for prefill tests
expectedMinDocuments?: ScopeDocumentType[]
expectedHardTriggerIds?: string[]
expectedDsfaRequired?: boolean
tags: string[]
}
export const GOLDEN_TESTS: GoldenTest[] = [
// GT-01: 2-Person Freelancer, nur B2B, DE-Hosting → L1
{
id: 'GT-01',
name: '2-Person Freelancer B2B',
description: 'Kleinstes Setup ohne besondere Risiken',
answers: [
{ questionId: 'org_employee_count', value: '2' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'consulting' },
{ questionId: 'data_health', value: false },
{ questionId: 'data_genetic', value: false },
{ questionId: 'data_biometric', value: false },
{ questionId: 'data_racial_ethnic', value: false },
{ questionId: 'data_political_opinion', value: false },
{ questionId: 'data_religious', value: false },
{ questionId: 'data_union_membership', value: false },
{ questionId: 'data_sexual_orientation', value: false },
{ questionId: 'data_criminal', value: false },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
{ questionId: 'process_has_incident_plan', value: true },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<100' },
],
expectedLevel: 'L1',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
expectedHardTriggerIds: [],
expectedDsfaRequired: false,
tags: ['baseline', 'freelancer', 'b2b'],
},
// GT-02: Solo IT-Berater → L1
{
id: 'GT-02',
name: 'Solo IT-Berater',
description: 'Einzelperson, minimale Datenverarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'it_services' },
{ questionId: 'data_health', value: false },
{ questionId: 'data_genetic', value: false },
{ questionId: 'data_biometric', value: false },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['baseline', 'solo', 'minimal'],
},
// GT-03: 5-Person Agentur, Website, kein Tracking → L1
{
id: 'GT-03',
name: '5-Person Agentur ohne Tracking',
description: 'Kleine Agentur, einfache Website ohne Analytics',
answers: [
{ questionId: 'org_employee_count', value: '5' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'marketing' },
{ questionId: 'tech_has_website', value: true },
{ questionId: 'tech_has_tracking', value: false },
{ questionId: 'data_volume', value: '1000-10000' },
{ questionId: 'org_customer_count', value: '100-1000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L1',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER'],
tags: ['baseline', 'agency', 'simple'],
},
// GT-04: 30-Person SaaS B2B, EU-Cloud → L2 (scale trigger)
{
id: 'GT-04',
name: '30-Person SaaS B2B',
description: 'Scale-Trigger durch Mitarbeiterzahl',
answers: [
{ questionId: 'org_employee_count', value: '30' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'software' },
{ questionId: 'tech_has_cloud', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: false },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER'],
tags: ['scale', 'saas', 'growth'],
},
// GT-05: 50-Person Handel B2C, Webshop → L2 (B2C+Webshop)
{
id: 'GT-05',
name: '50-Person E-Commerce B2C',
description: 'B2C mit Webshop erhöht Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '50' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_has_webshop', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'org_customer_count', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L2',
expectedHardTriggerIds: ['HT-H01'],
expectedMinDocuments: ['VVT', 'TOM', 'AVV', 'COOKIE_BANNER', 'EINWILLIGUNG'],
tags: ['b2c', 'webshop', 'retail'],
},
// GT-06: 80-Person Dienstleister, Cloud → L2 (scale)
{
id: 'GT-06',
name: '80-Person Dienstleister',
description: 'Größerer Betrieb mit Cloud-Services',
answers: [
{ questionId: 'org_employee_count', value: '80' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'professional_services' },
{ questionId: 'tech_has_cloud', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'AVV'],
tags: ['scale', 'services'],
},
// GT-07: 20-Person Startup mit GA4 Tracking → L2 (tracking)
{
id: 'GT-07',
name: 'Startup mit Google Analytics',
description: 'Tracking-Tools erhöhen Compliance-Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '20' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'technology' },
{ questionId: 'tech_has_website', value: true },
{ questionId: 'tech_has_tracking', value: true },
{ questionId: 'tech_tracking_tools', value: 'google_analytics' },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L2',
expectedMinDocuments: ['VVT', 'TOM', 'COOKIE_BANNER', 'EINWILLIGUNG'],
tags: ['tracking', 'analytics', 'startup'],
},
// GT-08: Kita-App (Minderjaehrige) → L3 (HT-B01)
{
id: 'GT-08',
name: 'Kita-App für Eltern',
description: 'Datenverarbeitung von Minderjährigen unter 16',
answers: [
{ questionId: 'org_employee_count', value: '15' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'data_volume', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-B01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'EINWILLIGUNG', 'AVV'],
tags: ['hard-trigger', 'minors', 'education'],
},
// GT-09: Krankenhaus-Software → L3 (HT-A01)
{
id: 'GT-09',
name: 'Krankenhaus-Verwaltungssoftware',
description: 'Gesundheitsdaten Art. 9 DSGVO',
answers: [
{ questionId: 'org_employee_count', value: '200' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10-50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'health', 'art9'],
},
// GT-10: HR-Scoring-Plattform → L3 (HT-C01)
{
id: 'GT-10',
name: 'HR-Scoring für Bewerbungen',
description: 'Automatisierte Entscheidungen im HR-Bereich',
answers: [
{ questionId: 'org_employee_count', value: '40' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'hr_tech' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'profiling' },
{ questionId: 'tech_adm_impact', value: 'employment' },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-C01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'adm', 'profiling'],
},
// GT-11: Fintech Kreditscoring → L3 (HT-H05 + C01)
{
id: 'GT-11',
name: 'Fintech Kreditscoring',
description: 'Finanzsektor mit automatisierten Entscheidungen',
answers: [
{ questionId: 'org_employee_count', value: '120' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'finance' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'scoring' },
{ questionId: 'tech_adm_impact', value: 'credit' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H05', 'HT-C01'],
expectedDsfaRequired: true,
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV'],
tags: ['hard-trigger', 'finance', 'scoring'],
},
// GT-12: Bildungsplattform Minderjaehrige → L3 (HT-B01)
{
id: 'GT-12',
name: 'Online-Lernplattform für Schüler',
description: 'Bildungssektor mit minderjährigen Nutzern',
answers: [
{ questionId: 'org_employee_count', value: '35' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'tech_has_tracking', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-B01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'education', 'minors'],
},
// GT-13: Datenbroker → L3 (HT-H02)
{
id: 'GT-13',
name: 'Datenbroker / Adresshandel',
description: 'Geschäftsmodell basiert auf Datenhandel',
answers: [
{ questionId: 'org_employee_count', value: '25' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'data_broker' },
{ questionId: 'data_is_core_business', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '100-1000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'data-broker'],
},
// GT-14: Video + ADM → L3 (HT-D05)
{
id: 'GT-14',
name: 'Videoüberwachung mit Gesichtserkennung',
description: 'Biometrische Daten mit automatisierter Verarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '60' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'security' },
{ questionId: 'data_biometric', value: true },
{ questionId: 'tech_has_video_surveillance', value: true },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-D05'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'biometric', 'video'],
},
// GT-15: 500-MA Konzern ohne Zert → L3 (HT-G04)
{
id: 'GT-15',
name: 'Großunternehmen ohne Zertifizierung',
description: 'Scale-Trigger durch Unternehmensgröße',
answers: [
{ questionId: 'org_employee_count', value: '500' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'manufacturing' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '>100000' },
{ questionId: 'cert_has_iso27001', value: false },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-G04'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'scale', 'enterprise'],
},
// GT-16: ISO 27001 Anbieter → L4 (HT-F01)
{
id: 'GT-16',
name: 'ISO 27001 zertifizierter Cloud-Provider',
description: 'Zertifizierung erfordert höchste Compliance',
answers: [
{ questionId: 'org_employee_count', value: '150' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27001', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F01'],
expectedMinDocuments: ['VVT', 'TOM', 'DSFA', 'AVV', 'CERT_ISO27001'],
tags: ['hard-trigger', 'certification', 'iso'],
},
// GT-17: TISAX Automobilzulieferer → L4 (HT-F04)
{
id: 'GT-17',
name: 'TISAX-zertifizierter Automobilzulieferer',
description: 'Automotive-Branche mit TISAX-Anforderungen',
answers: [
{ questionId: 'org_employee_count', value: '300' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'automotive' },
{ questionId: 'cert_has_tisax', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10-50' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F04'],
tags: ['hard-trigger', 'certification', 'tisax'],
},
// GT-18: ISO 27701 Cloud-Provider → L4 (HT-F02)
{
id: 'GT-18',
name: 'ISO 27701 Privacy-zertifiziert',
description: 'Privacy-spezifische Zertifizierung',
answers: [
{ questionId: 'org_employee_count', value: '200' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27701', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F02'],
tags: ['hard-trigger', 'certification', 'privacy'],
},
// GT-19: Grosskonzern + Art.9 + >1M DS → L4 (HT-G05)
{
id: 'GT-19',
name: 'Konzern mit sensiblen Massendaten',
description: 'Kombination aus Scale und Art. 9 Daten',
answers: [
{ questionId: 'org_employee_count', value: '2000' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'insurance' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '>100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-G05'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'scale', 'art9'],
},
// GT-20: Nur B2C Webshop → L2 (HT-H01)
{
id: 'GT-20',
name: 'Reiner B2C Webshop',
description: 'B2C-Trigger ohne weitere Risiken',
answers: [
{ questionId: 'org_employee_count', value: '12' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_has_webshop', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'org_customer_count', value: '1000-10000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L2',
expectedHardTriggerIds: ['HT-H01'],
tags: ['b2c', 'webshop'],
},
// GT-21: Keine Daten, keine MA → L1
{
id: 'GT-21',
name: 'Minimale Datenverarbeitung',
description: 'Absolute Baseline ohne Risiken',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'consulting' },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'tech_has_website', value: false },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['baseline', 'minimal'],
},
// GT-22: Alle Art.9 Kategorien → L3 (HT-A09)
{
id: 'GT-22',
name: 'Alle Art. 9 Kategorien',
description: 'Multiple sensible Datenkategorien',
answers: [
{ questionId: 'org_employee_count', value: '50' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'research' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_genetic', value: true },
{ questionId: 'data_biometric', value: true },
{ questionId: 'data_racial_ethnic', value: true },
{ questionId: 'data_political_opinion', value: true },
{ questionId: 'data_religious', value: true },
{ questionId: 'data_union_membership', value: true },
{ questionId: 'data_sexual_orientation', value: true },
{ questionId: 'data_criminal', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A09'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'art9', 'multiple-categories'],
},
// GT-23: Drittland + Art.9 → L3 (HT-E04)
{
id: 'GT-23',
name: 'Drittlandtransfer mit Art. 9 Daten',
description: 'Kombination aus Drittland und sensiblen Daten',
answers: [
{ questionId: 'org_employee_count', value: '45' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'us' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'tech_has_third_country_transfer', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-E04'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'third-country', 'art9'],
},
// GT-24: Minderjaehrige + Art.9 → L4 (HT-B02)
{
id: 'GT-24',
name: 'Minderjährige mit Gesundheitsdaten',
description: 'Kombination aus vulnerabler Gruppe und Art. 9',
answers: [
{ questionId: 'org_employee_count', value: '30' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-B02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'minors', 'health', 'combined-risk'],
},
// GT-25: KI autonome Entscheidungen → L3 (HT-C02)
{
id: 'GT-25',
name: 'KI mit autonomen Entscheidungen',
description: 'AI Act relevante autonome Systeme',
answers: [
{ questionId: 'org_employee_count', value: '70' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'ai_services' },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'tech_adm_type', value: 'autonomous_decision' },
{ questionId: 'tech_has_ai', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-C02'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'ai', 'adm'],
},
// GT-26: Multiple Zertifizierungen → L4 (HT-F01-05)
{
id: 'GT-26',
name: 'Multiple Zertifizierungen',
description: 'Mehrere Zertifizierungen kombiniert',
answers: [
{ questionId: 'org_employee_count', value: '250' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'cloud_services' },
{ questionId: 'cert_has_iso27001', value: true },
{ questionId: 'cert_has_iso27701', value: true },
{ questionId: 'cert_has_soc2', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-F01', 'HT-F02', 'HT-F03'],
tags: ['hard-trigger', 'certification', 'multiple'],
},
// GT-27: Oeffentlicher Sektor + Gesundheit → L3 (HT-H07 + A01)
{
id: 'GT-27',
name: 'Öffentlicher Sektor mit Gesundheitsdaten',
description: 'Behörde mit Art. 9 Datenverarbeitung',
answers: [
{ questionId: 'org_employee_count', value: '120' },
{ questionId: 'org_business_model', value: 'b2g' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'public_sector' },
{ questionId: 'org_is_public_sector', value: true },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-H07', 'HT-A01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'public-sector', 'health'],
},
// GT-28: Bildung + KI + Minderjaehrige → L4 (HT-B03)
{
id: 'GT-28',
name: 'EdTech mit KI für Minderjährige',
description: 'Triple-Risiko: Bildung, KI, vulnerable Gruppe',
answers: [
{ questionId: 'org_employee_count', value: '55' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'education' },
{ questionId: 'data_subjects_minors', value: true },
{ questionId: 'data_subjects_minors_age', value: '<16' },
{ questionId: 'tech_has_ai', value: true },
{ questionId: 'tech_has_adm', value: true },
{ questionId: 'data_volume', value: '100000-1000000' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L4',
expectedHardTriggerIds: ['HT-B03'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'education', 'ai', 'minors', 'triple-risk'],
},
// GT-29: Freelancer mit 1 Art.9 → L3 (hard trigger override despite low score)
{
id: 'GT-29',
name: 'Freelancer mit Gesundheitsdaten',
description: 'Hard Trigger überschreibt niedrige Score-Bewertung',
answers: [
{ questionId: 'org_employee_count', value: '1' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'de' },
{ questionId: 'org_industry', value: 'healthcare' },
{ questionId: 'data_health', value: true },
{ questionId: 'data_volume', value: '<1000' },
{ questionId: 'org_customer_count', value: '<50' },
{ questionId: 'process_has_vvt', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-A01'],
expectedDsfaRequired: true,
tags: ['hard-trigger', 'override', 'art9', 'freelancer'],
},
// GT-30: Enterprise, alle Prozesse vorhanden → L3 (good process maturity)
{
id: 'GT-30',
name: 'Enterprise mit reifer Prozesslandschaft',
description: 'Große Organisation mit allen Compliance-Prozessen',
answers: [
{ questionId: 'org_employee_count', value: '450' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'tech_hosting_location', value: 'eu' },
{ questionId: 'org_industry', value: 'manufacturing' },
{ questionId: 'data_volume', value: '>1000000' },
{ questionId: 'org_customer_count', value: '10000-100000' },
{ questionId: 'process_has_vvt', value: true },
{ questionId: 'process_has_tom', value: true },
{ questionId: 'process_has_dsfa', value: true },
{ questionId: 'process_has_incident_plan', value: true },
{ questionId: 'process_has_dsb', value: true },
{ questionId: 'process_has_training', value: true },
],
expectedLevel: 'L3',
expectedHardTriggerIds: ['HT-G04'],
tags: ['enterprise', 'mature', 'all-processes'],
},
// GT-31: SMB, nur 1 Block beantwortet → L1 (graceful degradation)
{
id: 'GT-31',
name: 'Unvollständige Profilerstellung',
description: 'Test für graceful degradation bei unvollständigen Antworten',
answers: [
{ questionId: 'org_employee_count', value: '8' },
{ questionId: 'org_business_model', value: 'b2b' },
{ questionId: 'org_industry', value: 'consulting' },
// Nur Block 1 (Organization) beantwortet, Rest fehlt
],
expectedLevel: 'L1',
expectedHardTriggerIds: [],
tags: ['incomplete', 'degradation', 'edge-case'],
},
// GT-32: CompanyProfile Prefill Konsistenz → null (prefill test, no expected level)
{
id: 'GT-32',
name: 'CompanyProfile Prefill Test',
description: 'Prüft ob CompanyProfile-Daten korrekt in ScopeProfile übernommen werden',
answers: [
{ questionId: 'org_employee_count', value: '25' },
{ questionId: 'org_business_model', value: 'b2c' },
{ questionId: 'org_industry', value: 'retail' },
{ questionId: 'tech_hosting_location', value: 'eu' },
// Diese Werte sollten mit CompanyProfile-Prefill übereinstimmen
],
expectedLevel: null,
tags: ['prefill', 'integration', 'consistency'],
},
]

View File

@@ -1,821 +0,0 @@
import type {
ScopeQuestionBlock,
ScopeQuestionBlockId,
ScopeProfilingQuestion,
ScopeProfilingAnswer,
ComplianceScopeState,
} from './compliance-scope-types'
import type { CompanyProfile } from './types'
/**
* Block 1: Organisation & Reife
*/
const BLOCK_1_ORGANISATION: ScopeQuestionBlock = {
id: 'organisation',
title: 'Organisation & Reife',
description: 'Grundlegende Informationen zu Ihrer Organisation und Compliance-Zielen',
order: 1,
questions: [
{
id: 'org_employee_count',
type: 'number',
question: 'Wie viele Mitarbeiter hat Ihre Organisation?',
helpText: 'Geben Sie die Gesamtzahl aller Beschäftigten an (inkl. Teilzeit, Minijobs)',
required: true,
scoreWeights: { risk: 5, complexity: 8, assurance: 6 },
mapsToCompanyProfile: 'employeeCount',
},
{
id: 'org_customer_count',
type: 'single',
question: 'Wie viele Kunden/Nutzer betreuen Sie?',
helpText: 'Schätzen Sie die Anzahl aktiver Kunden oder Nutzer',
required: true,
options: [
{ value: '<100', label: 'Weniger als 100' },
{ value: '100-1000', label: '100 bis 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000+', label: 'Mehr als 100.000' },
],
scoreWeights: { risk: 6, complexity: 7, assurance: 6 },
},
{
id: 'org_annual_revenue',
type: 'single',
question: 'Wie hoch ist Ihr jährlicher Umsatz?',
helpText: 'Wählen Sie die zutreffende Umsatzklasse',
required: true,
options: [
{ value: '<2Mio', label: 'Unter 2 Mio. EUR' },
{ value: '2-10Mio', label: '2 bis 10 Mio. EUR' },
{ value: '10-50Mio', label: '10 bis 50 Mio. EUR' },
{ value: '>50Mio', label: 'Über 50 Mio. EUR' },
],
scoreWeights: { risk: 4, complexity: 6, assurance: 7 },
mapsToCompanyProfile: 'annualRevenue',
},
{
id: 'org_cert_target',
type: 'multi',
question: 'Welche Zertifizierungen streben Sie an oder besitzen Sie bereits?',
helpText: 'Mehrfachauswahl möglich. Zertifizierungen erhöhen den Assurance-Bedarf',
required: false,
options: [
{ value: 'ISO27001', label: 'ISO 27001 (Informationssicherheit)' },
{ value: 'ISO27701', label: 'ISO 27701 (Datenschutz-Erweiterung)' },
{ value: 'TISAX', label: 'TISAX (Automotive)' },
{ value: 'SOC2', label: 'SOC 2 (US-Standard)' },
{ value: 'BSI-Grundschutz', label: 'BSI IT-Grundschutz' },
{ value: 'Keine', label: 'Keine Zertifizierung geplant' },
],
scoreWeights: { risk: 3, complexity: 5, assurance: 10 },
},
{
id: 'org_industry',
type: 'single',
question: 'In welcher Branche sind Sie tätig?',
helpText: 'Ihre Branche beeinflusst Risikobewertung und regulatorische Anforderungen',
required: true,
options: [
{ value: 'it_software', label: 'IT & Software' },
{ value: 'healthcare', label: 'Gesundheitswesen' },
{ value: 'education', label: 'Bildung & Forschung' },
{ value: 'finance', label: 'Finanzdienstleistungen' },
{ value: 'retail', label: 'Einzelhandel & E-Commerce' },
{ value: 'manufacturing', label: 'Produktion & Fertigung' },
{ value: 'consulting', label: 'Beratung & Dienstleistungen' },
{ value: 'public', label: 'Öffentliche Verwaltung' },
{ value: 'other', label: 'Sonstige' },
],
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
mapsToCompanyProfile: 'industry',
mapsToVVTQuestion: 'org_industry',
mapsToLFQuestion: 'org-branche',
},
{
id: 'org_business_model',
type: 'single',
question: 'Was ist Ihr primäres Geschäftsmodell?',
helpText: 'B2C-Modelle haben höhere Datenschutzanforderungen',
required: true,
options: [
{ value: 'b2b', label: 'B2B (Business-to-Business)' },
{ value: 'b2c', label: 'B2C (Business-to-Consumer)' },
{ value: 'both', label: 'B2B und B2C gemischt' },
{ value: 'b2g', label: 'B2G (Business-to-Government)' },
],
scoreWeights: { risk: 6, complexity: 5, assurance: 5 },
mapsToCompanyProfile: 'businessModel',
mapsToVVTQuestion: 'org_b2b_b2c',
mapsToLFQuestion: 'org-geschaeftsmodell',
},
{
id: 'org_has_dsb',
type: 'boolean',
question: 'Haben Sie einen Datenschutzbeauftragten bestellt?',
helpText: 'Ein DSB ist bei mehr als 20 Personen mit regelmäßiger Datenverarbeitung Pflicht',
required: true,
scoreWeights: { risk: 5, complexity: 3, assurance: 6 },
},
],
}
/**
* Block 2: Daten & Betroffene
*/
const BLOCK_2_DATA: ScopeQuestionBlock = {
id: 'data',
title: 'Daten & Betroffene',
description: 'Art und Umfang der verarbeiteten personenbezogenen Daten',
order: 2,
questions: [
{
id: 'data_minors',
type: 'boolean',
question: 'Verarbeiten Sie Daten von Minderjährigen?',
helpText: 'Besondere Schutzpflichten für unter 16-Jährige (bzw. 13-Jährige bei Online-Diensten)',
required: true,
scoreWeights: { risk: 10, complexity: 5, assurance: 7 },
mapsToVVTQuestion: 'data_minors',
},
{
id: 'data_art9',
type: 'multi',
question: 'Verarbeiten Sie besondere Kategorien personenbezogener Daten (Art. 9 DSGVO)?',
helpText: 'Diese Daten unterliegen erhöhten Schutzanforderungen',
required: true,
options: [
{ value: 'gesundheit', label: 'Gesundheitsdaten' },
{ value: 'biometrie', label: 'Biometrische Daten (z.B. Fingerabdruck, Gesichtserkennung)' },
{ value: 'genetik', label: 'Genetische Daten' },
{ value: 'politisch', label: 'Politische Meinungen' },
{ value: 'religion', label: 'Religiöse/weltanschauliche Überzeugungen' },
{ value: 'gewerkschaft', label: 'Gewerkschaftszugehörigkeit' },
{ value: 'sexualleben', label: 'Sexualleben/sexuelle Orientierung' },
{ value: 'strafrechtlich', label: 'Strafrechtliche Verurteilungen/Straftaten' },
{ value: 'ethnisch', label: 'Ethnische Herkunft' },
],
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
mapsToVVTQuestion: 'data_health',
},
{
id: 'data_hr',
type: 'boolean',
question: 'Verarbeiten Sie Personaldaten (HR)?',
helpText: 'Bewerberdaten, Gehälter, Leistungsbeurteilungen etc.',
required: true,
scoreWeights: { risk: 6, complexity: 4, assurance: 5 },
mapsToVVTQuestion: 'dept_hr',
mapsToLFQuestion: 'data-hr',
},
{
id: 'data_communication',
type: 'boolean',
question: 'Verarbeiten Sie Kommunikationsdaten (E-Mail, Chat, Telefonie)?',
helpText: 'Inhalte oder Metadaten von Kommunikationsvorgängen',
required: true,
scoreWeights: { risk: 7, complexity: 5, assurance: 6 },
},
{
id: 'data_financial',
type: 'boolean',
question: 'Verarbeiten Sie Finanzdaten (Konten, Zahlungen)?',
helpText: 'Bankdaten, Kreditkartendaten, Buchhaltungsdaten',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
mapsToVVTQuestion: 'dept_finance',
mapsToLFQuestion: 'data-buchhaltung',
},
{
id: 'data_volume',
type: 'single',
question: 'Wie viele Personendatensätze verarbeiten Sie insgesamt?',
helpText: 'Schätzen Sie die Gesamtzahl betroffener Personen',
required: true,
options: [
{ value: '<1000', label: 'Unter 1.000' },
{ value: '1000-10000', label: '1.000 bis 10.000' },
{ value: '10000-100000', label: '10.000 bis 100.000' },
{ value: '100000-1000000', label: '100.000 bis 1 Mio.' },
{ value: '>1000000', label: 'Über 1 Mio.' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
],
}
/**
* Block 3: Verarbeitung & Zweck
*/
const BLOCK_3_PROCESSING: ScopeQuestionBlock = {
id: 'processing',
title: 'Verarbeitung & Zweck',
description: 'Wie und wofür werden personenbezogene Daten verarbeitet?',
order: 3,
questions: [
{
id: 'proc_tracking',
type: 'boolean',
question: 'Setzen Sie Tracking oder Profiling ein?',
helpText: 'Web-Analytics, Werbe-Tracking, Nutzungsprofile etc.',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'proc_adm_scoring',
type: 'boolean',
question: 'Treffen Sie automatisierte Entscheidungen (Art. 22 DSGVO)?',
helpText: 'Scoring, Bonitätsprüfung, automatische Ablehnung ohne menschliche Beteiligung',
required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
},
{
id: 'proc_ai_usage',
type: 'multi',
question: 'Setzen Sie KI-Systeme ein?',
helpText: 'KI-Einsatz kann zusätzliche Anforderungen (EU AI Act) auslösen',
required: true,
options: [
{ value: 'keine', label: 'Keine KI im Einsatz' },
{ value: 'chatbot', label: 'Chatbots/Virtuelle Assistenten' },
{ value: 'scoring', label: 'Scoring/Risikobewertung' },
{ value: 'profiling', label: 'Profiling/Verhaltensvorhersage' },
{ value: 'generativ', label: 'Generative KI (Text, Bild, Code)' },
{ value: 'autonom', label: 'Autonome Systeme/Entscheidungen' },
],
scoreWeights: { risk: 8, complexity: 9, assurance: 7 },
},
{
id: 'proc_data_combination',
type: 'boolean',
question: 'Führen Sie Daten aus verschiedenen Quellen zusammen?',
helpText: 'Data Matching, Anreicherung aus externen Quellen',
required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 6 },
},
{
id: 'proc_employee_monitoring',
type: 'boolean',
question: 'Überwachen Sie Mitarbeiter (Zeiterfassung, Standort, IT-Nutzung)?',
helpText: 'Beschäftigtendatenschutz nach § 26 BDSG',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 7 },
},
{
id: 'proc_video_surveillance',
type: 'boolean',
question: 'Setzen Sie Videoüberwachung ein?',
helpText: 'Kameras in Büros, Produktionsstätten, Verkaufsräumen etc.',
required: true,
scoreWeights: { risk: 8, complexity: 5, assurance: 7 },
mapsToVVTQuestion: 'special_video_surveillance',
mapsToLFQuestion: 'data-video',
},
],
}
/**
* Block 4: Technik/Hosting/Transfers
*/
const BLOCK_4_TECH: ScopeQuestionBlock = {
id: 'tech',
title: 'Technik, Hosting & Transfers',
description: 'Technische Infrastruktur und Datenübermittlung',
order: 4,
questions: [
{
id: 'tech_hosting_location',
type: 'single',
question: 'Wo werden Ihre Daten primär gehostet?',
helpText: 'Standort bestimmt anwendbares Datenschutzrecht',
required: true,
options: [
{ value: 'de', label: 'Deutschland' },
{ value: 'eu', label: 'EU (ohne Deutschland)' },
{ value: 'ewr', label: 'EWR (z.B. Norwegen, Island)' },
{ value: 'us_adequacy', label: 'USA (mit Angemessenheitsbeschluss/DPF)' },
{ value: 'drittland', label: 'Drittland ohne Angemessenheitsbeschluss' },
],
scoreWeights: { risk: 7, complexity: 6, assurance: 7 },
},
{
id: 'tech_subprocessors',
type: 'boolean',
question: 'Nutzen Sie Auftragsverarbeiter (externe Dienstleister)?',
helpText: 'Cloud-Anbieter, Hosting, E-Mail-Service, CRM etc. erfordert AVV nach Art. 28 DSGVO',
required: true,
scoreWeights: { risk: 6, complexity: 7, assurance: 7 },
},
{
id: 'tech_third_country',
type: 'boolean',
question: 'Übermitteln Sie Daten in Drittländer?',
helpText: 'Transfer außerhalb EU/EWR erfordert Schutzmaßnahmen (SCC, BCR etc.)',
required: true,
scoreWeights: { risk: 9, complexity: 8, assurance: 8 },
mapsToVVTQuestion: 'transfer_cloud_us',
},
{
id: 'tech_encryption_rest',
type: 'boolean',
question: 'Sind Daten im Ruhezustand verschlüsselt (at rest)?',
helpText: 'Datenbank-, Dateisystem- oder Volume-Verschlüsselung',
required: true,
scoreWeights: { risk: -5, complexity: 3, assurance: 7 },
},
{
id: 'tech_encryption_transit',
type: 'boolean',
question: 'Sind Daten bei Übertragung verschlüsselt (in transit)?',
helpText: 'TLS/SSL für alle Verbindungen',
required: true,
scoreWeights: { risk: -5, complexity: 2, assurance: 7 },
},
{
id: 'tech_cloud_providers',
type: 'multi',
question: 'Welche Cloud-Anbieter nutzen Sie?',
helpText: 'Mehrfachauswahl möglich',
required: false,
options: [
{ value: 'aws', label: 'Amazon Web Services (AWS)' },
{ value: 'azure', label: 'Microsoft Azure' },
{ value: 'gcp', label: 'Google Cloud Platform (GCP)' },
{ value: 'hetzner', label: 'Hetzner' },
{ value: 'ionos', label: 'IONOS' },
{ value: 'ovh', label: 'OVH' },
{ value: 'andere', label: 'Andere Anbieter' },
{ value: 'keine', label: 'Keine Cloud-Nutzung (On-Premise)' },
],
scoreWeights: { risk: 5, complexity: 6, assurance: 6 },
},
],
}
/**
* Block 5: Rechte & Prozesse
*/
const BLOCK_5_PROCESSES: ScopeQuestionBlock = {
id: 'processes',
title: 'Rechte & Prozesse',
description: 'Etablierte Datenschutz- und Sicherheitsprozesse',
order: 5,
questions: [
{
id: 'proc_dsar_process',
type: 'boolean',
question: 'Haben Sie einen Prozess für Betroffenenrechte (DSAR)?',
helpText: 'Auskunft, Löschung, Berichtigung, Widerspruch etc. Art. 15-22 DSGVO',
required: true,
scoreWeights: { risk: 6, complexity: 5, assurance: 8 },
},
{
id: 'proc_deletion_concept',
type: 'boolean',
question: 'Haben Sie ein Löschkonzept?',
helpText: 'Definierte Löschfristen und automatisierte Löschroutinen',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 8 },
},
{
id: 'proc_incident_response',
type: 'boolean',
question: 'Haben Sie einen Notfallplan für Datenschutzvorfälle?',
helpText: 'Incident Response Plan, 72h-Meldepflicht an Aufsichtsbehörde (Art. 33 DSGVO)',
required: true,
scoreWeights: { risk: 8, complexity: 6, assurance: 9 },
},
{
id: 'proc_regular_audits',
type: 'boolean',
question: 'Führen Sie regelmäßige Datenschutz-Audits durch?',
helpText: 'Interne oder externe Prüfungen mindestens jährlich',
required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 9 },
},
{
id: 'proc_training',
type: 'boolean',
question: 'Schulen Sie Ihre Mitarbeiter im Datenschutz?',
helpText: 'Awareness-Trainings, Onboarding, jährliche Auffrischung',
required: true,
scoreWeights: { risk: 6, complexity: 3, assurance: 7 },
},
],
}
/**
* Block 6: Produktkontext
*/
const BLOCK_6_PRODUCT: ScopeQuestionBlock = {
id: 'product',
title: 'Produktkontext',
description: 'Spezifische Merkmale Ihrer Produkte und Services',
order: 6,
questions: [
{
id: 'prod_type',
type: 'multi',
question: 'Welche Art von Produkten/Services bieten Sie an?',
helpText: 'Mehrfachauswahl möglich',
required: true,
options: [
{ value: 'webapp', label: 'Web-Anwendung' },
{ value: 'mobile', label: 'Mobile App (iOS/Android)' },
{ value: 'saas', label: 'SaaS-Plattform' },
{ value: 'onpremise', label: 'On-Premise Software' },
{ value: 'api', label: 'API/Schnittstellen' },
{ value: 'iot', label: 'IoT/Hardware' },
{ value: 'beratung', label: 'Beratungsleistungen' },
{ value: 'handel', label: 'Handel/Vertrieb' },
],
scoreWeights: { risk: 5, complexity: 6, assurance: 5 },
},
{
id: 'prod_cookies_consent',
type: 'boolean',
question: 'Benötigen Sie Cookie-Consent (Tracking-Cookies)?',
helpText: 'Nicht-essenzielle Cookies erfordern opt-in Einwilligung',
required: true,
scoreWeights: { risk: 5, complexity: 4, assurance: 6 },
},
{
id: 'prod_webshop',
type: 'boolean',
question: 'Betreiben Sie einen Online-Shop?',
helpText: 'E-Commerce mit Zahlungsabwicklung, Bestellverwaltung',
required: true,
scoreWeights: { risk: 7, complexity: 6, assurance: 6 },
},
{
id: 'prod_api_external',
type: 'boolean',
question: 'Bieten Sie externe APIs an (Daten-Weitergabe an Dritte)?',
helpText: 'Programmierschnittstellen für Partner, Entwickler etc.',
required: true,
scoreWeights: { risk: 7, complexity: 7, assurance: 7 },
},
{
id: 'prod_data_broker',
type: 'boolean',
question: 'Handeln Sie mit Daten (Data Brokerage, Adresshandel)?',
helpText: 'Verkauf oder Vermittlung personenbezogener Daten',
required: true,
scoreWeights: { risk: 10, complexity: 8, assurance: 9 },
},
],
}
/**
* All question blocks in order
*/
export const SCOPE_QUESTION_BLOCKS: ScopeQuestionBlock[] = [
BLOCK_1_ORGANISATION,
BLOCK_2_DATA,
BLOCK_3_PROCESSING,
BLOCK_4_TECH,
BLOCK_5_PROCESSES,
BLOCK_6_PRODUCT,
]
/**
* Prefill scope answers from CompanyProfile
*/
export function prefillFromCompanyProfile(
profile: CompanyProfile
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// employeeCount
if (profile.employeeCount != null) {
answers.push({
questionId: 'org_employee_count',
value: profile.employeeCount,
})
}
// annualRevenue
if (profile.annualRevenue) {
answers.push({
questionId: 'org_annual_revenue',
value: profile.annualRevenue,
})
}
// industry
if (profile.industry) {
answers.push({
questionId: 'org_industry',
value: profile.industry,
})
}
// businessModel
if (profile.businessModel) {
answers.push({
questionId: 'org_business_model',
value: profile.businessModel,
})
}
// dpoName -> org_has_dsb
if (profile.dpoName && profile.dpoName.trim() !== '') {
answers.push({
questionId: 'org_has_dsb',
value: true,
})
}
// usesAI -> proc_ai_usage
if (profile.usesAI === true) {
// We don't know which specific AI type, so just mark as "generativ" as a default
answers.push({
questionId: 'proc_ai_usage',
value: ['generativ'],
})
} else if (profile.usesAI === false) {
answers.push({
questionId: 'proc_ai_usage',
value: ['keine'],
})
}
// offerings -> prod_type mapping
if (profile.offerings && profile.offerings.length > 0) {
const prodTypes: string[] = []
const offeringsLower = profile.offerings.map((o) => o.toLowerCase())
if (offeringsLower.some((o) => o.includes('webapp') || o.includes('web'))) {
prodTypes.push('webapp')
}
if (
offeringsLower.some((o) => o.includes('mobile') || o.includes('app'))
) {
prodTypes.push('mobile')
}
if (offeringsLower.some((o) => o.includes('saas') || o.includes('cloud'))) {
prodTypes.push('saas')
}
if (
offeringsLower.some(
(o) => o.includes('onpremise') || o.includes('on-premise')
)
) {
prodTypes.push('onpremise')
}
if (offeringsLower.some((o) => o.includes('api'))) {
prodTypes.push('api')
}
if (offeringsLower.some((o) => o.includes('iot') || o.includes('hardware'))) {
prodTypes.push('iot')
}
if (
offeringsLower.some(
(o) => o.includes('beratung') || o.includes('consulting')
)
) {
prodTypes.push('beratung')
}
if (
offeringsLower.some(
(o) => o.includes('handel') || o.includes('shop') || o.includes('commerce')
)
) {
prodTypes.push('handel')
}
if (prodTypes.length > 0) {
answers.push({
questionId: 'prod_type',
value: prodTypes,
})
}
}
return answers
}
/**
* Prefill scope answers from VVT profiling answers
*/
export function prefillFromVVTAnswers(
vvtAnswers: Record<string, unknown>
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// Build reverse mapping: VVT question -> Scope question
const reverseMap: Record<string, string> = {}
for (const block of SCOPE_QUESTION_BLOCKS) {
for (const q of block.questions) {
if (q.mapsToVVTQuestion) {
reverseMap[q.mapsToVVTQuestion] = q.id
}
}
}
// Map VVT answers to scope answers
for (const [vvtQuestionId, vvtValue] of Object.entries(vvtAnswers)) {
const scopeQuestionId = reverseMap[vvtQuestionId]
if (scopeQuestionId) {
answers.push({
questionId: scopeQuestionId,
value: vvtValue,
})
}
}
return answers
}
/**
* Prefill scope answers from Loeschfristen profiling answers
*/
export function prefillFromLoeschfristenAnswers(
lfAnswers: Array<{ questionId: string; value: unknown }>
): ScopeProfilingAnswer[] {
const answers: ScopeProfilingAnswer[] = []
// Build reverse mapping: LF question -> Scope question
const reverseMap: Record<string, string> = {}
for (const block of SCOPE_QUESTION_BLOCKS) {
for (const q of block.questions) {
if (q.mapsToLFQuestion) {
reverseMap[q.mapsToLFQuestion] = q.id
}
}
}
// Map LF answers to scope answers
for (const lfAnswer of lfAnswers) {
const scopeQuestionId = reverseMap[lfAnswer.questionId]
if (scopeQuestionId) {
answers.push({
questionId: scopeQuestionId,
value: lfAnswer.value,
})
}
}
return answers
}
/**
* Export scope answers in VVT format
*/
export function exportToVVTAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, unknown> {
const vvtAnswers: Record<string, unknown> = {}
for (const answer of scopeAnswers) {
// Find the question
let question: ScopeProfilingQuestion | undefined
for (const block of SCOPE_QUESTION_BLOCKS) {
question = block.questions.find((q) => q.id === answer.questionId)
if (question) break
}
if (question?.mapsToVVTQuestion) {
vvtAnswers[question.mapsToVVTQuestion] = answer.value
}
}
return vvtAnswers
}
/**
* Export scope answers in Loeschfristen format
*/
export function exportToLoeschfristenAnswers(
scopeAnswers: ScopeProfilingAnswer[]
): Array<{ questionId: string; value: unknown }> {
const lfAnswers: Array<{ questionId: string; value: unknown }> = []
for (const answer of scopeAnswers) {
// Find the question
let question: ScopeProfilingQuestion | undefined
for (const block of SCOPE_QUESTION_BLOCKS) {
question = block.questions.find((q) => q.id === answer.questionId)
if (question) break
}
if (question?.mapsToLFQuestion) {
lfAnswers.push({
questionId: question.mapsToLFQuestion,
value: answer.value,
})
}
}
return lfAnswers
}
/**
* Export scope answers for TOM generator
*/
export function exportToTOMProfile(
scopeAnswers: ScopeProfilingAnswer[]
): Record<string, unknown> {
const tomProfile: Record<string, unknown> = {}
// Get answer values
const getVal = (qId: string) => getAnswerValue(scopeAnswers, qId)
// Map relevant scope answers to TOM profile fields
tomProfile.industry = getVal('org_industry')
tomProfile.employeeCount = getVal('org_employee_count')
tomProfile.hasDataMinors = getVal('data_minors')
tomProfile.hasSpecialCategories = Array.isArray(getVal('data_art9'))
? (getVal('data_art9') as string[]).length > 0
: false
tomProfile.hasAutomatedDecisions = getVal('proc_adm_scoring')
tomProfile.usesAI = Array.isArray(getVal('proc_ai_usage'))
? !(getVal('proc_ai_usage') as string[]).includes('keine')
: false
tomProfile.hasThirdCountryTransfer = getVal('tech_third_country')
tomProfile.hasEncryptionRest = getVal('tech_encryption_rest')
tomProfile.hasEncryptionTransit = getVal('tech_encryption_transit')
tomProfile.hasIncidentResponse = getVal('proc_incident_response')
tomProfile.hasDeletionConcept = getVal('proc_deletion_concept')
tomProfile.hasRegularAudits = getVal('proc_regular_audits')
tomProfile.hasTraining = getVal('proc_training')
return tomProfile
}
/**
* Check if a block is complete (all required questions answered)
*/
export function isBlockComplete(
answers: ScopeProfilingAnswer[],
blockId: ScopeQuestionBlockId
): boolean {
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
if (!block) return false
const requiredQuestions = block.questions.filter((q) => q.required)
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
return requiredQuestions.every((q) => answeredQuestionIds.has(q.id))
}
/**
* Get progress for a specific block (0-100)
*/
export function getBlockProgress(
answers: ScopeProfilingAnswer[],
blockId: ScopeQuestionBlockId
): number {
const block = SCOPE_QUESTION_BLOCKS.find((b) => b.id === blockId)
if (!block) return 0
const requiredQuestions = block.questions.filter((q) => q.required)
if (requiredQuestions.length === 0) return 100
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
const answeredCount = requiredQuestions.filter((q) =>
answeredQuestionIds.has(q.id)
).length
return Math.round((answeredCount / requiredQuestions.length) * 100)
}
/**
* Get total progress across all blocks (0-100)
*/
export function getTotalProgress(answers: ScopeProfilingAnswer[]): number {
let totalRequired = 0
let totalAnswered = 0
const answeredQuestionIds = new Set(answers.map((a) => a.questionId))
for (const block of SCOPE_QUESTION_BLOCKS) {
const requiredQuestions = block.questions.filter((q) => q.required)
totalRequired += requiredQuestions.length
totalAnswered += requiredQuestions.filter((q) =>
answeredQuestionIds.has(q.id)
).length
}
if (totalRequired === 0) return 100
return Math.round((totalAnswered / totalRequired) * 100)
}
/**
* Get answer value for a specific question
*/
export function getAnswerValue(
answers: ScopeProfilingAnswer[],
questionId: string
): unknown {
const answer = answers.find((a) => a.questionId === questionId)
return answer?.value
}
/**
* Get all questions as a flat array
*/
export function getAllQuestions(): ScopeProfilingQuestion[] {
return SCOPE_QUESTION_BLOCKS.flatMap((block) => block.questions)
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,210 +0,0 @@
/**
* Demo Controls for AI Compliance SDK
*/
import { Control } from '../types'
export const DEMO_CONTROLS: Control[] = [
// Zugangskontrolle
{
id: 'demo-ctrl-1',
name: 'Multi-Faktor-Authentifizierung',
description: 'Alle Systemzugriffe erfordern mindestens zwei unabhängige Authentifizierungsfaktoren (Wissen + Besitz).',
type: 'TECHNICAL',
category: 'Zugangskontrolle',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-1'],
owner: 'IT-Sicherheit',
dueDate: null,
},
{
id: 'demo-ctrl-2',
name: 'Rollenbasiertes Berechtigungskonzept',
description: 'Zugriffsrechte werden nach dem Least-Privilege-Prinzip anhand definierter Rollen vergeben und regelmäßig überprüft.',
type: 'ORGANIZATIONAL',
category: 'Zugangskontrolle',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-2'],
owner: 'IT-Sicherheit',
dueDate: null,
},
// Verfügbarkeit
{
id: 'demo-ctrl-3',
name: 'Automatisiertes Backup-System',
description: 'Tägliche inkrementelle Backups und wöchentliche Vollbackups aller kritischen Daten mit Verschlüsselung.',
type: 'TECHNICAL',
category: 'Verfügbarkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-3'],
owner: 'IT-Betrieb',
dueDate: null,
},
{
id: 'demo-ctrl-4',
name: 'Georedundante Datenspeicherung',
description: 'Kritische Daten werden synchron in zwei geographisch getrennten Rechenzentren gespeichert.',
type: 'TECHNICAL',
category: 'Verfügbarkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-4'],
owner: 'IT-Betrieb',
dueDate: null,
},
// KI-Fairness
{
id: 'demo-ctrl-5',
name: 'Bias-Monitoring',
description: 'Kontinuierliche Überwachung der KI-Modelle auf systematische Verzerrungen anhand definierter Fairness-Metriken.',
type: 'TECHNICAL',
category: 'KI-Governance',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-5'],
owner: 'Data Science Lead',
dueDate: null,
},
{
id: 'demo-ctrl-6',
name: 'Human-in-the-Loop',
description: 'Kritische automatisierte Entscheidungen werden vor Umsetzung durch qualifizierte Mitarbeiter überprüft.',
type: 'ORGANIZATIONAL',
category: 'KI-Governance',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-6'],
owner: 'Fachbereich HR',
dueDate: null,
},
// Transparenz
{
id: 'demo-ctrl-7',
name: 'Explainable AI Komponenten',
description: 'Einsatz von SHAP/LIME zur Erklärung von KI-Entscheidungen für nachvollziehbare Begründungen.',
type: 'TECHNICAL',
category: 'Transparenz',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-7'],
owner: 'Data Science Lead',
dueDate: null,
},
{
id: 'demo-ctrl-8',
name: 'Verständliche Datenschutzinformationen',
description: 'Betroffene erhalten klare, verständliche Informationen über die Verarbeitung ihrer Daten gemäß Art. 13-14 DSGVO.',
type: 'ORGANIZATIONAL',
category: 'Transparenz',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-8'],
owner: 'DSB',
dueDate: null,
},
// Datensparsamkeit
{
id: 'demo-ctrl-9',
name: 'Zweckbindungskontrollen',
description: 'Technische Maßnahmen stellen sicher, dass Daten nur für definierte Zwecke verarbeitet werden.',
type: 'TECHNICAL',
category: 'Datensparsamkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-9'],
owner: 'IT-Sicherheit',
dueDate: null,
},
{
id: 'demo-ctrl-10',
name: 'Anonymisierungs-Pipeline',
description: 'Automatisierte Anonymisierung von Daten für Analysen, wo keine Personenbezug erforderlich ist.',
type: 'TECHNICAL',
category: 'Datensparsamkeit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-10'],
owner: 'Data Engineering',
dueDate: null,
},
// KI-Sicherheit
{
id: 'demo-ctrl-11',
name: 'Input-Validierung',
description: 'Strenge Validierung aller Eingabedaten zur Verhinderung von Adversarial Attacks auf KI-Modelle.',
type: 'TECHNICAL',
category: 'KI-Sicherheit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-11'],
owner: 'Data Science Lead',
dueDate: null,
},
{
id: 'demo-ctrl-12',
name: 'Model Performance Monitoring',
description: 'Kontinuierliche Überwachung der Modell-Performance mit automatischen Alerts bei Abweichungen.',
type: 'TECHNICAL',
category: 'KI-Sicherheit',
implementationStatus: 'PARTIAL',
effectiveness: 'MEDIUM',
evidence: [],
owner: 'Data Science Lead',
dueDate: new Date('2026-03-31'),
},
// Datenlebenszyklus
{
id: 'demo-ctrl-13',
name: 'Automatisierte Löschroutinen',
description: 'Technische Umsetzung der Aufbewahrungsfristen mit automatischer Löschung nach Fristablauf.',
type: 'TECHNICAL',
category: 'Datenlebenszyklus',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-13'],
owner: 'IT-Betrieb',
dueDate: null,
},
{
id: 'demo-ctrl-14',
name: 'Löschprotokoll-Review',
description: 'Quartalsweise Überprüfung der Löschprotokolle durch den DSB.',
type: 'ORGANIZATIONAL',
category: 'Datenlebenszyklus',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'MEDIUM',
evidence: ['demo-evi-14'],
owner: 'DSB',
dueDate: null,
},
// Audit
{
id: 'demo-ctrl-15',
name: 'Umfassendes Audit-Logging',
description: 'Alle sicherheitsrelevanten Ereignisse werden manipulationssicher protokolliert und 10 Jahre aufbewahrt.',
type: 'TECHNICAL',
category: 'Audit',
implementationStatus: 'IMPLEMENTED',
effectiveness: 'HIGH',
evidence: ['demo-evi-15'],
owner: 'IT-Sicherheit',
dueDate: null,
},
]
export function getDemoControls(): Control[] {
return DEMO_CONTROLS.map(ctrl => ({
...ctrl,
dueDate: ctrl.dueDate ? new Date(ctrl.dueDate) : null,
}))
}

View File

@@ -1,224 +0,0 @@
/**
* Demo DSFA for AI Compliance SDK
*/
import { DSFA, DSFASection, DSFAApproval } from '../types'
export const DEMO_DSFA: DSFA = {
id: 'demo-dsfa-1',
status: 'IN_REVIEW',
version: 2,
sections: [
{
id: 'dsfa-sec-1',
title: 'Systematische Beschreibung der Verarbeitungsvorgänge',
content: `## 1. Verarbeitungsbeschreibung
### 1.1 Gegenstand der Verarbeitung
Die geplante KI-gestützte Kundenanalyse verarbeitet personenbezogene Daten von Kunden und Interessenten zur Optimierung von Marketingmaßnahmen und Personalisierung von Angeboten.
### 1.2 Verarbeitungszwecke
- Kundensegmentierung basierend auf Kaufverhalten
- Churn-Prediction zur Kundenbindung
- Personalisierte Produktempfehlungen
- Optimierung von Marketing-Kampagnen
### 1.3 Kategorien personenbezogener Daten
- **Stammdaten**: Name, Adresse, E-Mail, Telefon
- **Transaktionsdaten**: Käufe, Bestellungen, Retouren
- **Nutzungsdaten**: Clickstreams, Seitenaufrufe, Verweildauer
- **Demographische Daten**: Alter, Geschlecht, PLZ-Region
### 1.4 Kategorien betroffener Personen
- Bestandskunden (ca. 250.000 aktive Kunden)
- Registrierte Interessenten (ca. 100.000)
- Newsletter-Abonnenten (ca. 180.000)
### 1.5 Rechtsgrundlage
**Primär**: Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)
**Sekundär**: Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweiterte Profiling-Maßnahmen)
Das berechtigte Interesse liegt in der Verbesserung des Kundenerlebnisses und der Effizienzsteigerung des Marketings.`,
status: 'COMPLETED',
order: 1,
},
{
id: 'dsfa-sec-2',
title: 'Bewertung der Notwendigkeit und Verhältnismäßigkeit',
content: `## 2. Notwendigkeit und Verhältnismäßigkeit
### 2.1 Notwendigkeit der Verarbeitung
Die Verarbeitung ist notwendig, um:
- Kunden individuell relevante Angebote zu unterbreiten
- Abwanderungsgefährdete Kunden frühzeitig zu identifizieren
- Marketing-Budget effizienter einzusetzen
- Wettbewerbsfähigkeit zu erhalten
### 2.2 Verhältnismäßigkeitsprüfung
**Alternative Methoden geprüft:**
1. **Manuelle Analyse**: Nicht praktikabel bei 250.000+ Kunden
2. **Regelbasierte Systeme**: Zu ungenau, führt zu höherem Datenverbrauch
3. **Aggregierte Analysen**: Keine ausreichende Personalisierung möglich
**Ergebnis**: Die KI-gestützte Analyse stellt die mildeste effektive Maßnahme dar.
### 2.3 Datensparsamkeit
- Nur für den Zweck notwendige Daten werden verarbeitet
- Sensitive Kategorien (Art. 9 DSGVO) werden ausgeschlossen
- Automatische Löschung nach definierten Fristen
### 2.4 Interessenabwägung
| Interesse des Verantwortlichen | Interesse der Betroffenen |
|-------------------------------|---------------------------|
| Effizientes Marketing | Privatsphäre |
| Kundenbindung | Keine unerwünschte Profilbildung |
| Umsatzsteigerung | Transparenz über Verarbeitung |
**Ausgleichende Maßnahmen:**
- Umfassende Informationen nach Art. 13/14 DSGVO
- Einfacher Opt-out für Profiling
- Human-Review bei kritischen Entscheidungen`,
status: 'COMPLETED',
order: 2,
},
{
id: 'dsfa-sec-3',
title: 'Risikobewertung',
content: `## 3. Risiken für Rechte und Freiheiten
### 3.1 Identifizierte Risiken
| # | Risiko | Eintritt | Schwere | Gesamt |
|---|--------|----------|---------|--------|
| R1 | Unbefugter Zugriff auf Profildaten | Mittel | Hoch | HOCH |
| R2 | Diskriminierende Entscheidungen durch Bias | Mittel | Hoch | HOCH |
| R3 | Unzulässige Profilbildung | Mittel | Mittel | MITTEL |
| R4 | Fehlende Nachvollziehbarkeit | Hoch | Mittel | MITTEL |
| R5 | Übermäßige Datensammlung | Niedrig | Mittel | NIEDRIG |
### 3.2 Detailanalyse kritischer Risiken
**R1 - Unbefugter Zugriff**
- Quelle: Externe Angreifer, Insider-Bedrohung
- Auswirkung: Identitätsdiebstahl, Reputationsschaden
- Betroffene: Alle Kunden
**R2 - Diskriminierende Entscheidungen**
- Quelle: Historische Verzerrungen in Trainingsdaten
- Auswirkung: Benachteiligung bestimmter Gruppen
- Betroffene: Potentiell alle, besonders geschützte Gruppen`,
status: 'COMPLETED',
order: 3,
},
{
id: 'dsfa-sec-4',
title: 'Maßnahmen zur Risikominderung',
content: `## 4. Abhilfemaßnahmen
### 4.1 Technische Maßnahmen
| Maßnahme | Risiko | Status | Wirksamkeit |
|----------|--------|--------|-------------|
| Multi-Faktor-Authentifizierung | R1 | ✅ Umgesetzt | Hoch |
| Verschlüsselung (AES-256) | R1 | ✅ Umgesetzt | Hoch |
| Bias-Monitoring | R2 | ✅ Umgesetzt | Mittel |
| Explainable AI | R4 | ✅ Umgesetzt | Mittel |
| Zweckbindungskontrollen | R3 | ✅ Umgesetzt | Hoch |
| Audit-Logging | R1, R4 | ✅ Umgesetzt | Hoch |
### 4.2 Organisatorische Maßnahmen
| Maßnahme | Risiko | Status | Wirksamkeit |
|----------|--------|--------|-------------|
| Rollenbasierte Zugriffskontrolle | R1 | ✅ Umgesetzt | Hoch |
| Human-in-the-Loop | R2 | ✅ Umgesetzt | Hoch |
| Datenschutz-Schulungen | R1, R3 | ✅ Umgesetzt | Mittel |
| Regelmäßige Audits | Alle | ⏳ Geplant | Hoch |
### 4.3 Restrisikobewertung
Nach Implementierung aller Maßnahmen:
- **R1**: HOCH → MITTEL (akzeptabel)
- **R2**: HOCH → MITTEL (akzeptabel)
- **R3**: MITTEL → NIEDRIG (akzeptabel)
- **R4**: MITTEL → NIEDRIG (akzeptabel)
- **R5**: NIEDRIG → NIEDRIG (akzeptabel)`,
status: 'COMPLETED',
order: 4,
},
{
id: 'dsfa-sec-5',
title: 'Stellungnahme des Datenschutzbeauftragten',
content: `## 5. Stellungnahme DSB
### 5.1 Bewertung
Der Datenschutzbeauftragte hat die DSFA geprüft und kommt zu folgender Einschätzung:
**Positiv:**
- Umfassende Risikoanalyse durchgeführt
- Technische Schutzmaßnahmen dem Stand der Technik entsprechend
- Transparenzpflichten angemessen berücksichtigt
- Interessenabwägung nachvollziehbar dokumentiert
**Verbesserungspotenzial:**
- Regelmäßige Überprüfung der Bias-Metriken sollte quartalsweise erfolgen
- Informationen für Betroffene könnten noch verständlicher formuliert werden
- Löschkonzept sollte um automatische Überprüfungsmechanismen ergänzt werden
### 5.2 Empfehlung
Der DSB empfiehlt die **Genehmigung** der Verarbeitungstätigkeit unter der Voraussetzung, dass:
1. Die identifizierten Verbesserungsmaßnahmen innerhalb von 3 Monaten umgesetzt werden
2. Eine jährliche Überprüfung der DSFA erfolgt
3. Bei wesentlichen Änderungen eine Aktualisierung vorgenommen wird
---
*Datum: 2026-01-28*
*Unterschrift: [DSB]*`,
status: 'COMPLETED',
order: 5,
},
],
approvals: [
{
id: 'dsfa-appr-1',
approver: 'Dr. Thomas Schmidt',
role: 'Datenschutzbeauftragter',
status: 'APPROVED',
comment: 'Unter den genannten Voraussetzungen genehmigt.',
approvedAt: new Date('2026-01-28'),
},
{
id: 'dsfa-appr-2',
approver: 'Maria Weber',
role: 'CISO',
status: 'APPROVED',
comment: 'Technische Maßnahmen sind angemessen.',
approvedAt: new Date('2026-01-29'),
},
{
id: 'dsfa-appr-3',
approver: 'Michael Bauer',
role: 'Geschäftsführung',
status: 'PENDING',
comment: null,
approvedAt: null,
},
],
createdAt: new Date('2026-01-15'),
updatedAt: new Date('2026-02-01'),
}
export function getDemoDSFA(): DSFA {
return {
...DEMO_DSFA,
approvals: DEMO_DSFA.approvals.map(a => ({
...a,
approvedAt: a.approvedAt ? new Date(a.approvedAt) : null,
})),
createdAt: new Date(DEMO_DSFA.createdAt),
updatedAt: new Date(DEMO_DSFA.updatedAt),
}
}

View File

@@ -1,556 +0,0 @@
/**
* Demo Data Seeding for AI Compliance SDK
*
* IMPORTANT: Demo data is NOT hardcoded in the frontend.
* This module provides seed data that gets stored via the API,
* exactly like real customer data would be stored.
*
* The seedDemoData() function writes data through the API,
* and the data is then loaded from the database like any other data.
*/
import { SDKState } from '../types'
import { getSDKApiClient } from '../api-client'
// Seed data imports (these are templates, not runtime data)
import { getDemoUseCases, DEMO_USE_CASES } from './use-cases'
import { getDemoRisks, DEMO_RISKS } from './risks'
import { getDemoControls, DEMO_CONTROLS } from './controls'
import { getDemoDSFA, DEMO_DSFA } from './dsfa'
import { getDemoTOMs, DEMO_TOMS } from './toms'
import { getDemoProcessingActivities, getDemoRetentionPolicies, DEMO_PROCESSING_ACTIVITIES, DEMO_RETENTION_POLICIES } from './vvt'
// Re-export for direct access to seed templates (for testing/development)
export {
getDemoUseCases,
getDemoRisks,
getDemoControls,
getDemoDSFA,
getDemoTOMs,
getDemoProcessingActivities,
getDemoRetentionPolicies,
// Raw data exports
DEMO_USE_CASES,
DEMO_RISKS,
DEMO_CONTROLS,
DEMO_DSFA,
DEMO_TOMS,
DEMO_PROCESSING_ACTIVITIES,
DEMO_RETENTION_POLICIES,
}
/**
* Generate a complete demo state object
* This is used as seed data for the API, not as runtime data
*/
export function generateDemoState(tenantId: string, userId: string): Partial<SDKState> {
const now = new Date()
return {
// Metadata
version: '1.0.0',
lastModified: now,
// Tenant & User
tenantId,
userId,
subscription: 'PROFESSIONAL',
// Customer Type
customerType: 'new',
// Company Profile (Demo: TechStart GmbH - SaaS-Startup aus Berlin)
companyProfile: {
companyName: 'TechStart GmbH',
legalForm: 'gmbh',
industry: 'Technologie / IT',
foundedYear: 2022,
businessModel: 'B2B_B2C',
offerings: ['app_web', 'software_saas', 'services_consulting'],
companySize: 'small',
employeeCount: '10-49',
annualRevenue: '2-10 Mio',
headquartersCountry: 'DE',
headquartersCity: 'Berlin',
hasInternationalLocations: false,
internationalCountries: [],
targetMarkets: ['germany_only', 'dach'],
primaryJurisdiction: 'DE',
isDataController: true,
isDataProcessor: true,
usesAI: true,
aiUseCases: ['KI-gestützte Kundenberatung', 'Automatisierte Dokumentenanalyse'],
dpoName: 'Max Mustermann',
dpoEmail: 'dsb@techstart.de',
legalContactName: null,
legalContactEmail: null,
isComplete: true,
completedAt: new Date('2026-01-14'),
},
// Progress - showing a realistic partially completed workflow
currentPhase: 2,
currentStep: 'tom',
completedSteps: [
'company-profile',
'use-case-assessment',
'screening',
'modules',
'requirements',
'controls',
'evidence',
'audit-checklist',
'risks',
'ai-act',
'obligations',
'dsfa',
],
checkpoints: {
'CP-PROF': { checkpointId: 'CP-PROF', passed: true, validatedAt: new Date('2026-01-14'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-UC': { checkpointId: 'CP-UC', passed: true, validatedAt: new Date('2026-01-15'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-SCAN': { checkpointId: 'CP-SCAN', passed: true, validatedAt: new Date('2026-01-16'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-MOD': { checkpointId: 'CP-MOD', passed: true, validatedAt: new Date('2026-01-17'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-REQ': { checkpointId: 'CP-REQ', passed: true, validatedAt: new Date('2026-01-18'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-CTRL': { checkpointId: 'CP-CTRL', passed: true, validatedAt: new Date('2026-01-19'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-EVI': { checkpointId: 'CP-EVI', passed: true, validatedAt: new Date('2026-01-20'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-CHK': { checkpointId: 'CP-CHK', passed: true, validatedAt: new Date('2026-01-21'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-RISK': { checkpointId: 'CP-RISK', passed: true, validatedAt: new Date('2026-01-22'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-AI': { checkpointId: 'CP-AI', passed: true, validatedAt: new Date('2026-01-25'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-OBL': { checkpointId: 'CP-OBL', passed: true, validatedAt: new Date('2026-01-27'), validatedBy: 'demo-user', errors: [], warnings: [] },
'CP-DSFA': { checkpointId: 'CP-DSFA', passed: true, validatedAt: new Date('2026-01-30'), validatedBy: 'DSB', errors: [], warnings: [] },
},
// Phase 1 Data
useCases: getDemoUseCases(),
activeUseCase: 'demo-uc-1',
screening: {
id: 'demo-scan-1',
status: 'COMPLETED',
startedAt: new Date('2026-01-16T09:00:00'),
completedAt: new Date('2026-01-16T09:15:00'),
sbom: {
format: 'CycloneDX',
version: '1.4',
components: [
{
name: 'tensorflow',
version: '2.15.0',
type: 'library',
purl: 'pkg:pypi/tensorflow@2.15.0',
licenses: ['Apache-2.0'],
vulnerabilities: [],
},
{
name: 'scikit-learn',
version: '1.4.0',
type: 'library',
purl: 'pkg:pypi/scikit-learn@1.4.0',
licenses: ['BSD-3-Clause'],
vulnerabilities: [],
},
{
name: 'pandas',
version: '2.2.0',
type: 'library',
purl: 'pkg:pypi/pandas@2.2.0',
licenses: ['BSD-3-Clause'],
vulnerabilities: [],
},
],
dependencies: [],
generatedAt: new Date('2026-01-16T09:10:00'),
},
securityScan: {
totalIssues: 3,
critical: 0,
high: 1,
medium: 1,
low: 1,
issues: [
{
id: 'sec-issue-1',
severity: 'HIGH',
title: 'Outdated cryptography library',
description: 'The cryptography library version 41.0.0 has known vulnerabilities',
cve: 'CVE-2024-1234',
cvss: 7.5,
affectedComponent: 'cryptography',
remediation: 'Upgrade to cryptography >= 42.0.0',
status: 'RESOLVED',
},
{
id: 'sec-issue-2',
severity: 'MEDIUM',
title: 'Insecure default configuration',
description: 'Debug mode enabled in production configuration',
cve: null,
cvss: 5.3,
affectedComponent: 'app-config',
remediation: 'Set DEBUG=false in production',
status: 'RESOLVED',
},
{
id: 'sec-issue-3',
severity: 'LOW',
title: 'Missing security headers',
description: 'X-Content-Type-Options header not set',
cve: null,
cvss: 3.1,
affectedComponent: 'web-server',
remediation: 'Add security headers middleware',
status: 'RESOLVED',
},
],
},
error: null,
},
modules: [
{
id: 'demo-mod-1',
name: 'Kundendaten-Modul',
description: 'Verarbeitung von Kundendaten für Marketing und Analyse',
regulations: ['DSGVO', 'TTDSG'],
criticality: 'HIGH',
processesPersonalData: true,
hasAIComponents: true,
},
{
id: 'demo-mod-2',
name: 'HR-Modul',
description: 'Bewerbermanagement und Personalverwaltung',
regulations: ['DSGVO', 'AGG', 'AI Act'],
criticality: 'HIGH',
processesPersonalData: true,
hasAIComponents: true,
},
{
id: 'demo-mod-3',
name: 'Support-Modul',
description: 'Kundenservice und Chatbot-System',
regulations: ['DSGVO', 'AI Act'],
criticality: 'MEDIUM',
processesPersonalData: true,
hasAIComponents: true,
},
],
requirements: [
{
id: 'demo-req-1',
regulation: 'DSGVO',
article: 'Art. 5',
title: 'Grundsätze der Verarbeitung',
description: 'Einhaltung der Grundsätze für die Verarbeitung personenbezogener Daten',
criticality: 'CRITICAL',
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-1', 'demo-ctrl-2', 'demo-ctrl-9'],
},
{
id: 'demo-req-2',
regulation: 'DSGVO',
article: 'Art. 32',
title: 'Sicherheit der Verarbeitung',
description: 'Geeignete technische und organisatorische Maßnahmen',
criticality: 'CRITICAL',
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-1', 'demo-ctrl-3', 'demo-ctrl-4'],
},
{
id: 'demo-req-3',
regulation: 'DSGVO',
article: 'Art. 25',
title: 'Datenschutz durch Technikgestaltung',
description: 'Privacy by Design und Privacy by Default',
criticality: 'HIGH',
applicableModules: ['demo-mod-1', 'demo-mod-2'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-9', 'demo-ctrl-10'],
},
{
id: 'demo-req-4',
regulation: 'AI Act',
article: 'Art. 13',
title: 'Transparenz',
description: 'Transparenzanforderungen für KI-Systeme',
criticality: 'HIGH',
applicableModules: ['demo-mod-1', 'demo-mod-2', 'demo-mod-3'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-7', 'demo-ctrl-8'],
},
{
id: 'demo-req-5',
regulation: 'AI Act',
article: 'Art. 9',
title: 'Risikomanagement',
description: 'Risikomanagementsystem für Hochrisiko-KI',
criticality: 'HIGH',
applicableModules: ['demo-mod-2'],
status: 'IMPLEMENTED',
controls: ['demo-ctrl-5', 'demo-ctrl-6', 'demo-ctrl-11', 'demo-ctrl-12'],
},
],
controls: getDemoControls(),
evidence: [
{
id: 'demo-evi-1',
controlId: 'demo-ctrl-1',
type: 'SCREENSHOT',
name: 'MFA-Konfiguration Azure AD',
description: 'Screenshot der MFA-Einstellungen im Azure AD Admin Portal',
fileUrl: null,
validFrom: new Date('2026-01-01'),
validUntil: new Date('2027-01-01'),
uploadedBy: 'IT-Security',
uploadedAt: new Date('2026-01-10'),
},
{
id: 'demo-evi-2',
controlId: 'demo-ctrl-2',
type: 'DOCUMENT',
name: 'Berechtigungskonzept v2.1',
description: 'Dokumentiertes Berechtigungskonzept mit Rollenmatrix',
fileUrl: null,
validFrom: new Date('2026-01-01'),
validUntil: null,
uploadedBy: 'IT-Security',
uploadedAt: new Date('2026-01-05'),
},
{
id: 'demo-evi-5',
controlId: 'demo-ctrl-5',
type: 'AUDIT_REPORT',
name: 'Bias-Audit Q1/2026',
description: 'Externer Audit-Bericht zur Fairness des KI-Modells',
fileUrl: null,
validFrom: new Date('2026-01-15'),
validUntil: new Date('2026-04-15'),
uploadedBy: 'Data Science Lead',
uploadedAt: new Date('2026-01-20'),
},
],
checklist: [
{
id: 'demo-chk-1',
requirementId: 'demo-req-1',
title: 'Rechtmäßigkeit der Verarbeitung geprüft',
description: 'Dokumentierte Prüfung der Rechtsgrundlagen',
status: 'PASSED',
notes: 'Geprüft durch DSB',
verifiedBy: 'DSB',
verifiedAt: new Date('2026-01-20'),
},
{
id: 'demo-chk-2',
requirementId: 'demo-req-2',
title: 'TOMs dokumentiert und umgesetzt',
description: 'Technische und organisatorische Maßnahmen',
status: 'PASSED',
notes: 'Alle TOMs implementiert',
verifiedBy: 'CISO',
verifiedAt: new Date('2026-01-21'),
},
],
risks: getDemoRisks(),
// Phase 2 Data
aiActClassification: {
riskCategory: 'HIGH',
systemType: 'Beschäftigungsbezogenes KI-System (Art. 6 Abs. 2 AI Act)',
obligations: [
{
id: 'demo-ai-obl-1',
article: 'Art. 9',
title: 'Risikomanagementsystem',
description: 'Einrichtung eines KI-Risikomanagementsystems',
deadline: new Date('2026-08-01'),
status: 'IN_PROGRESS',
},
{
id: 'demo-ai-obl-2',
article: 'Art. 10',
title: 'Daten-Governance',
description: 'Anforderungen an Trainingsdaten',
deadline: new Date('2026-08-01'),
status: 'COMPLETED',
},
{
id: 'demo-ai-obl-3',
article: 'Art. 13',
title: 'Transparenz',
description: 'Dokumentation für Nutzer',
deadline: new Date('2026-08-01'),
status: 'COMPLETED',
},
],
assessmentDate: new Date('2026-01-25'),
assessedBy: 'Compliance Team',
justification: 'Das System fällt unter Art. 6 Abs. 2 lit. a AI Act (Einstellung und Auswahl von Personen).',
},
obligations: [
{
id: 'demo-obl-1',
regulation: 'DSGVO',
article: 'Art. 30',
title: 'Verarbeitungsverzeichnis',
description: 'Führung eines Verzeichnisses der Verarbeitungstätigkeiten',
deadline: null,
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
status: 'COMPLETED',
responsible: 'DSB',
},
{
id: 'demo-obl-2',
regulation: 'DSGVO',
article: 'Art. 35',
title: 'Datenschutz-Folgenabschätzung',
description: 'Durchführung einer DSFA für Hochrisiko-Verarbeitungen',
deadline: null,
penalty: 'Bis zu 10 Mio. EUR oder 2% des Jahresumsatzes',
status: 'COMPLETED',
responsible: 'DSB',
},
{
id: 'demo-obl-3',
regulation: 'AI Act',
article: 'Art. 49',
title: 'CE-Kennzeichnung',
description: 'CE-Kennzeichnung für Hochrisiko-KI-Systeme',
deadline: new Date('2026-08-01'),
penalty: 'Bis zu 35 Mio. EUR oder 7% des Jahresumsatzes',
status: 'PENDING',
responsible: 'Compliance',
},
],
dsfa: getDemoDSFA(),
toms: getDemoTOMs(),
retentionPolicies: getDemoRetentionPolicies(),
vvt: getDemoProcessingActivities(),
// Documents, Cookie Banner, etc. - partially filled
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
// Security
sbom: null,
securityIssues: [],
securityBacklog: [],
// UI State
commandBarHistory: [],
recentSearches: ['DSGVO Art. 5', 'Bias-Monitoring', 'TOM Verschlüsselung'],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
}
}
/**
* Seed demo data into the database via API
* This ensures demo data is stored exactly like real customer data
*/
export async function seedDemoData(
tenantId: string = 'demo-tenant',
userId: string = 'demo-user',
apiBaseUrl?: string
): Promise<{ success: boolean; message: string }> {
try {
const apiClient = getSDKApiClient(tenantId)
// Generate the demo state
const demoState = generateDemoState(tenantId, userId) as SDKState
// Save via the same API that real data uses
await apiClient.saveState(demoState)
return {
success: true,
message: `Demo data successfully seeded for tenant ${tenantId}`,
}
} catch (error) {
console.error('Failed to seed demo data:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error during seeding',
}
}
}
/**
* Check if demo data exists for a tenant
*/
export async function hasDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
try {
const apiClient = getSDKApiClient(tenantId)
const response = await apiClient.getState()
// Check if we have any use cases (indicating data exists)
return response !== null && response.state && Array.isArray(response.state.useCases) && response.state.useCases.length > 0
} catch {
return false
}
}
/**
* Clear demo data for a tenant
*/
export async function clearDemoData(tenantId: string = 'demo-tenant'): Promise<boolean> {
try {
const apiClient = getSDKApiClient(tenantId)
await apiClient.deleteState()
return true
} catch {
return false
}
}
/**
* Seed demo data via direct API call (for use outside of React context)
* This is useful for server-side seeding or CLI tools
*/
export async function seedDemoDataDirect(
baseUrl: string,
tenantId: string = 'demo-tenant',
userId: string = 'demo-user'
): Promise<{ success: boolean; message: string }> {
try {
const demoState = generateDemoState(tenantId, userId)
const response = await fetch(`${baseUrl}/api/sdk/v1/state`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
tenantId,
userId,
state: demoState,
}),
})
if (!response.ok) {
const error = await response.json().catch(() => ({ message: 'Unknown error' }))
throw new Error(error.message || `HTTP ${response.status}`)
}
return {
success: true,
message: `Demo data successfully seeded for tenant ${tenantId}`,
}
} catch (error) {
console.error('Failed to seed demo data:', error)
return {
success: false,
message: error instanceof Error ? error.message : 'Unknown error during seeding',
}
}
}

View File

@@ -1,268 +0,0 @@
/**
* Demo Risks for AI Compliance SDK
*/
import { Risk, RiskMitigation } from '../types'
export const DEMO_RISKS: Risk[] = [
{
id: 'demo-risk-1',
title: 'Unbefugter Zugriff auf personenbezogene Daten',
description: 'Risiko des unbefugten Zugriffs auf Kundendaten durch externe Angreifer oder interne Mitarbeiter ohne entsprechende Berechtigung.',
category: 'Datensicherheit',
likelihood: 3,
impact: 5,
severity: 'CRITICAL',
inherentRiskScore: 15,
residualRiskScore: 6,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-1a',
description: 'Implementierung von Multi-Faktor-Authentifizierung für alle Systemzugriffe',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-1',
},
{
id: 'demo-mit-1b',
description: 'Rollenbasiertes Zugriffskonzept mit Least-Privilege-Prinzip',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-2',
},
],
owner: 'CISO',
relatedControls: ['demo-ctrl-1', 'demo-ctrl-2'],
relatedRequirements: ['demo-req-1', 'demo-req-2'],
},
{
id: 'demo-risk-2',
title: 'KI-Bias bei automatisierten Entscheidungen',
description: 'Das KI-System könnte systematische Verzerrungen aufweisen, die zu diskriminierenden Entscheidungen führen, insbesondere bei der Bewerbungsvorauswahl.',
category: 'KI-Ethik',
likelihood: 4,
impact: 4,
severity: 'HIGH',
inherentRiskScore: 16,
residualRiskScore: 8,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-2a',
description: 'Regelmäßiges Bias-Monitoring mit Fairness-Metriken',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-5',
},
{
id: 'demo-mit-2b',
description: 'Human-in-the-Loop bei kritischen Entscheidungen',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 25,
controlId: 'demo-ctrl-6',
},
],
owner: 'Data Science Lead',
relatedControls: ['demo-ctrl-5', 'demo-ctrl-6'],
relatedRequirements: ['demo-req-5', 'demo-req-6'],
},
{
id: 'demo-risk-3',
title: 'Datenverlust durch Systemausfall',
description: 'Verlust von Kundendaten und KI-Modellen durch Hardware-Defekte, Softwarefehler oder Naturkatastrophen.',
category: 'Verfügbarkeit',
likelihood: 2,
impact: 5,
severity: 'HIGH',
inherentRiskScore: 10,
residualRiskScore: 3,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-3a',
description: 'Tägliche inkrementelle und wöchentliche Vollbackups',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-3',
},
{
id: 'demo-mit-3b',
description: 'Georedundante Datenspeicherung in zwei Rechenzentren',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 35,
controlId: 'demo-ctrl-4',
},
],
owner: 'IT-Leiter',
relatedControls: ['demo-ctrl-3', 'demo-ctrl-4'],
relatedRequirements: ['demo-req-3'],
},
{
id: 'demo-risk-4',
title: 'Unzureichende Transparenz bei KI-Entscheidungen',
description: 'Betroffene verstehen nicht, wie KI-Entscheidungen zustande kommen, was zu Beschwerden und regulatorischen Problemen führen kann.',
category: 'Transparenz',
likelihood: 4,
impact: 3,
severity: 'MEDIUM',
inherentRiskScore: 12,
residualRiskScore: 4,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-4a',
description: 'Explainable AI Komponenten für nachvollziehbare Entscheidungen',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-7',
},
{
id: 'demo-mit-4b',
description: 'Verständliche Informationen für Betroffene gem. Art. 13-14 DSGVO',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-8',
},
],
owner: 'DSB',
relatedControls: ['demo-ctrl-7', 'demo-ctrl-8'],
relatedRequirements: ['demo-req-4'],
},
{
id: 'demo-risk-5',
title: 'Unerlaubte Profilbildung',
description: 'Durch die Zusammenführung verschiedener Datenquellen könnte eine unzulässige umfassende Profilbildung von Personen entstehen.',
category: 'Datenschutz',
likelihood: 3,
impact: 4,
severity: 'HIGH',
inherentRiskScore: 12,
residualRiskScore: 6,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-5a',
description: 'Strenge Zweckbindung der Datenverarbeitung',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 25,
controlId: 'demo-ctrl-9',
},
{
id: 'demo-mit-5b',
description: 'Datensparsamkeit durch Aggregation und Anonymisierung',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-10',
},
],
owner: 'DSB',
relatedControls: ['demo-ctrl-9', 'demo-ctrl-10'],
relatedRequirements: ['demo-req-7', 'demo-req-8'],
},
{
id: 'demo-risk-6',
title: 'Mangelnde Modell-Robustheit',
description: 'KI-Modelle könnten durch Adversarial Attacks oder veränderte Inputdaten manipuliert werden und falsche Ergebnisse liefern.',
category: 'KI-Sicherheit',
likelihood: 2,
impact: 4,
severity: 'MEDIUM',
inherentRiskScore: 8,
residualRiskScore: 4,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-6a',
description: 'Input-Validierung und Anomalie-Erkennung',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 30,
controlId: 'demo-ctrl-11',
},
{
id: 'demo-mit-6b',
description: 'Regelmäßige Modell-Retraining und Performance-Monitoring',
type: 'MITIGATE',
status: 'IN_PROGRESS',
effectiveness: 20,
controlId: 'demo-ctrl-12',
},
],
owner: 'Data Science Lead',
relatedControls: ['demo-ctrl-11', 'demo-ctrl-12'],
relatedRequirements: ['demo-req-9'],
},
{
id: 'demo-risk-7',
title: 'Verstoß gegen Aufbewahrungsfristen',
description: 'Daten werden länger als zulässig gespeichert oder zu früh gelöscht, was zu Compliance-Verstößen führt.',
category: 'Datenschutz',
likelihood: 3,
impact: 3,
severity: 'MEDIUM',
inherentRiskScore: 9,
residualRiskScore: 3,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-7a',
description: 'Automatisierte Löschroutinen mit Retention-Policy-Enforcement',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 40,
controlId: 'demo-ctrl-13',
},
{
id: 'demo-mit-7b',
description: 'Quartalsmäßige Überprüfung der Löschprotokolle',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 25,
controlId: 'demo-ctrl-14',
},
],
owner: 'DSB',
relatedControls: ['demo-ctrl-13', 'demo-ctrl-14'],
relatedRequirements: ['demo-req-10'],
},
{
id: 'demo-risk-8',
title: 'Fehlende Nachvollziehbarkeit im Audit',
description: 'Bei Prüfungen können Verarbeitungsvorgänge nicht lückenlos nachvollzogen werden.',
category: 'Compliance',
likelihood: 2,
impact: 3,
severity: 'MEDIUM',
inherentRiskScore: 6,
residualRiskScore: 2,
status: 'MITIGATED',
mitigation: [
{
id: 'demo-mit-8a',
description: 'Umfassendes Audit-Logging aller Verarbeitungsvorgänge',
type: 'MITIGATE',
status: 'COMPLETED',
effectiveness: 50,
controlId: 'demo-ctrl-15',
},
],
owner: 'IT-Leiter',
relatedControls: ['demo-ctrl-15'],
relatedRequirements: ['demo-req-11'],
},
]
export function getDemoRisks(): Risk[] {
return DEMO_RISKS
}

View File

@@ -1,296 +0,0 @@
/**
* Demo TOMs (Technical & Organizational Measures) for AI Compliance SDK
* These are seed data structures - actual data is stored in database
*/
import { TOM } from '../types'
export const DEMO_TOMS: TOM[] = [
// Zugangskontrolle
{
id: 'demo-tom-1',
category: 'Zugangskontrolle',
name: 'Physische Zutrittskontrolle',
description: 'Elektronische Zugangskontrollsysteme mit personenbezogenen Zutrittskarten für alle Serverräume und Rechenzentren. Protokollierung aller Zutritte.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Facility Management',
implementationDate: new Date('2025-06-01'),
reviewDate: new Date('2026-06-01'),
evidence: ['demo-evi-tom-1'],
},
{
id: 'demo-tom-2',
category: 'Zugangskontrolle',
name: 'Besuchermanagement',
description: 'Registrierung aller Besucher mit Identitätsprüfung, Ausgabe von Besucherausweisen und permanente Begleitung in sicherheitsrelevanten Bereichen.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'MEDIUM',
responsiblePerson: 'Empfang/Security',
implementationDate: new Date('2025-03-15'),
reviewDate: new Date('2026-03-15'),
evidence: ['demo-evi-tom-2'],
},
// Zugriffskontrolle
{
id: 'demo-tom-3',
category: 'Zugriffskontrolle',
name: 'Identity & Access Management (IAM)',
description: 'Zentrales IAM-System mit automatischer Provisionierung, Deprovisionierung und regelmäßiger Rezertifizierung aller Benutzerkonten.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-3'],
},
{
id: 'demo-tom-4',
category: 'Zugriffskontrolle',
name: 'Privileged Access Management (PAM)',
description: 'Spezielles Management für administrative Zugänge mit Session-Recording, automatischer Passwortrotation und Just-in-Time-Berechtigungen.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-04-01'),
reviewDate: new Date('2026-04-01'),
evidence: ['demo-evi-tom-4'],
},
{
id: 'demo-tom-5',
category: 'Zugriffskontrolle',
name: 'Berechtigungskonzept-Review',
description: 'Halbjährliche Überprüfung aller Berechtigungen durch die jeweiligen Fachbereichsleiter mit dokumentierter Rezertifizierung.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Fachbereichsleiter',
implementationDate: new Date('2025-02-01'),
reviewDate: new Date('2026-02-01'),
evidence: ['demo-evi-tom-5'],
},
// Verschlüsselung
{
id: 'demo-tom-6',
category: 'Verschlüsselung',
name: 'Datenverschlüsselung at Rest',
description: 'AES-256 Verschlüsselung aller personenbezogenen Daten in Datenbanken und Dateisystemen. Key Management über HSM.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-15'),
reviewDate: new Date('2026-01-15'),
evidence: ['demo-evi-tom-6'],
},
{
id: 'demo-tom-7',
category: 'Verschlüsselung',
name: 'Transportverschlüsselung',
description: 'TLS 1.3 für alle externen Verbindungen, mTLS für interne Service-Kommunikation. Regelmäßige Überprüfung der Cipher Suites.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-7'],
},
// Pseudonymisierung
{
id: 'demo-tom-8',
category: 'Pseudonymisierung',
name: 'Pseudonymisierungs-Pipeline',
description: 'Automatisierte Pseudonymisierung von Daten vor der Verarbeitung in Analytics-Systemen. Reversible Zuordnung nur durch autorisierten Prozess.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Data Engineering',
implementationDate: new Date('2025-05-01'),
reviewDate: new Date('2026-05-01'),
evidence: ['demo-evi-tom-8'],
},
// Integrität
{
id: 'demo-tom-9',
category: 'Integrität',
name: 'Datenintegritätsprüfung',
description: 'Checksummen-Validierung bei allen Datentransfers, Hash-Verifikation gespeicherter Daten, automatische Alerts bei Abweichungen.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-03-01'),
reviewDate: new Date('2026-03-01'),
evidence: ['demo-evi-tom-9'],
},
{
id: 'demo-tom-10',
category: 'Integrität',
name: 'Change Management',
description: 'Dokumentierter Change-Prozess mit Vier-Augen-Prinzip für alle Änderungen an produktiven Systemen. CAB-Freigabe für kritische Changes.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Leitung',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-10'],
},
// Verfügbarkeit
{
id: 'demo-tom-11',
category: 'Verfügbarkeit',
name: 'Disaster Recovery Plan',
description: 'Dokumentierter und getesteter DR-Plan mit RTO <4h und RPO <1h. Jährliche DR-Tests mit Dokumentation.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Leitung',
implementationDate: new Date('2025-02-01'),
reviewDate: new Date('2026-02-01'),
evidence: ['demo-evi-tom-11'],
},
{
id: 'demo-tom-12',
category: 'Verfügbarkeit',
name: 'High Availability Cluster',
description: 'Aktiv-Aktiv-Cluster für alle kritischen Systeme mit automatischem Failover. 99,9% Verfügbarkeits-SLA.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-12'],
},
// Belastbarkeit
{
id: 'demo-tom-13',
category: 'Belastbarkeit',
name: 'Load Balancing & Auto-Scaling',
description: 'Dynamische Skalierung basierend auf Last-Metriken. Load Balancer mit Health Checks und automatischer Traffic-Umleitung.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-04-01'),
reviewDate: new Date('2026-04-01'),
evidence: ['demo-evi-tom-13'],
},
{
id: 'demo-tom-14',
category: 'Belastbarkeit',
name: 'DDoS-Schutz',
description: 'Cloudbasierter DDoS-Schutz mit automatischer Traffic-Filterung. Kapazität für 10x Normal-Traffic.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-14'],
},
// Wiederherstellbarkeit
{
id: 'demo-tom-15',
category: 'Wiederherstellbarkeit',
name: 'Backup-Strategie',
description: '3-2-1 Backup-Strategie: 3 Kopien, 2 verschiedene Medien, 1 Offsite. Tägliche inkrementelle, wöchentliche Vollbackups.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'CRITICAL',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-01-01'),
reviewDate: new Date('2026-01-01'),
evidence: ['demo-evi-tom-15'],
},
{
id: 'demo-tom-16',
category: 'Wiederherstellbarkeit',
name: 'Restore-Tests',
description: 'Monatliche Restore-Tests mit zufällig ausgewählten Daten. Dokumentation der Recovery-Zeit und Vollständigkeit.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Betrieb',
implementationDate: new Date('2025-02-01'),
reviewDate: new Date('2026-02-01'),
evidence: ['demo-evi-tom-16'],
},
// Überprüfung & Bewertung
{
id: 'demo-tom-17',
category: 'Überprüfung & Bewertung',
name: 'Penetration Tests',
description: 'Jährliche externe Penetration Tests durch zertifizierte Dienstleister. Zusätzliche Tests nach größeren Änderungen.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'IT-Sicherheit',
implementationDate: new Date('2025-03-01'),
reviewDate: new Date('2026-03-01'),
evidence: ['demo-evi-tom-17'],
},
{
id: 'demo-tom-18',
category: 'Überprüfung & Bewertung',
name: 'Security Awareness Training',
description: 'Verpflichtendes Security-Training für alle Mitarbeiter bei Einstellung und jährlich. Phishing-Simulationen quartalsweise.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'MEDIUM',
responsiblePerson: 'HR / IT-Sicherheit',
implementationDate: new Date('2025-01-15'),
reviewDate: new Date('2026-01-15'),
evidence: ['demo-evi-tom-18'],
},
// KI-spezifische TOMs
{
id: 'demo-tom-19',
category: 'KI-Governance',
name: 'Model Governance Framework',
description: 'Dokumentierter Prozess für Entwicklung, Test, Deployment und Monitoring von KI-Modellen. Model Cards für alle produktiven Modelle.',
type: 'ORGANIZATIONAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Data Science Lead',
implementationDate: new Date('2025-06-01'),
reviewDate: new Date('2026-06-01'),
evidence: ['demo-evi-tom-19'],
},
{
id: 'demo-tom-20',
category: 'KI-Governance',
name: 'Bias Detection & Monitoring',
description: 'Automatisiertes Monitoring der Modell-Outputs auf Bias. Alerting bei signifikanten Abweichungen von Fairness-Metriken.',
type: 'TECHNICAL',
implementationStatus: 'IMPLEMENTED',
priority: 'HIGH',
responsiblePerson: 'Data Science Lead',
implementationDate: new Date('2025-07-01'),
reviewDate: new Date('2026-07-01'),
evidence: ['demo-evi-tom-20'],
},
]
export function getDemoTOMs(): TOM[] {
return DEMO_TOMS.map(tom => ({
...tom,
implementationDate: tom.implementationDate ? new Date(tom.implementationDate) : null,
reviewDate: tom.reviewDate ? new Date(tom.reviewDate) : null,
}))
}

View File

@@ -1,85 +0,0 @@
/**
* Demo Use Cases for AI Compliance SDK
*/
import { UseCaseAssessment, AssessmentResult } from '../types'
export const DEMO_USE_CASES: UseCaseAssessment[] = [
{
id: 'demo-uc-1',
name: 'KI-gestützte Kundenanalyse',
description: 'Analyse von Kundenverhalten und Präferenzen mittels Machine Learning zur Personalisierung von Angeboten und Verbesserung des Customer Lifetime Value. Das System verarbeitet Transaktionsdaten, Clickstreams und demographische Informationen.',
category: 'Marketing',
stepsCompleted: 5,
steps: [
{ id: 'uc1-step-1', name: 'Grunddaten', completed: true, data: { type: 'customer-analytics', department: 'Marketing' } },
{ id: 'uc1-step-2', name: 'Datenquellen', completed: true, data: { sources: ['CRM', 'Webshop', 'Newsletter'] } },
{ id: 'uc1-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['Clustering', 'Recommender', 'Churn-Prediction'] } },
{ id: 'uc1-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
{ id: 'uc1-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
],
assessmentResult: {
riskLevel: 'HIGH',
applicableRegulations: ['DSGVO', 'AI Act', 'TTDSG'],
recommendedControls: ['Einwilligungsmanagement', 'Profilbildungstransparenz', 'Opt-out-Mechanismus'],
dsfaRequired: true,
aiActClassification: 'LIMITED',
},
createdAt: new Date('2026-01-15'),
updatedAt: new Date('2026-02-01'),
},
{
id: 'demo-uc-2',
name: 'Automatisierte Bewerbungsvorauswahl',
description: 'KI-System zur Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse, Qualifikationsabgleich und Erfahrungsbewertung. Ziel ist die Effizienzsteigerung im Recruiting-Prozess bei gleichzeitiger Gewährleistung von Fairness.',
category: 'HR',
stepsCompleted: 5,
steps: [
{ id: 'uc2-step-1', name: 'Grunddaten', completed: true, data: { type: 'hr-screening', department: 'Personal' } },
{ id: 'uc2-step-2', name: 'Datenquellen', completed: true, data: { sources: ['Bewerbungsportal', 'LinkedIn', 'XING'] } },
{ id: 'uc2-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['NLP', 'Matching', 'Scoring'] } },
{ id: 'uc2-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Bewerber'] } },
{ id: 'uc2-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'HIGH' } },
],
assessmentResult: {
riskLevel: 'HIGH',
applicableRegulations: ['DSGVO', 'AI Act', 'AGG'],
recommendedControls: ['Bias-Monitoring', 'Human-in-the-Loop', 'Transparenzpflichten'],
dsfaRequired: true,
aiActClassification: 'HIGH',
},
createdAt: new Date('2026-01-20'),
updatedAt: new Date('2026-02-02'),
},
{
id: 'demo-uc-3',
name: 'Chatbot für Kundenservice',
description: 'Konversationeller KI-Assistent für die automatisierte Beantwortung von Kundenanfragen im First-Level-Support. Basiert auf Large Language Models mit firmeneigenem Wissen.',
category: 'Kundenservice',
stepsCompleted: 5,
steps: [
{ id: 'uc3-step-1', name: 'Grunddaten', completed: true, data: { type: 'chatbot', department: 'Support' } },
{ id: 'uc3-step-2', name: 'Datenquellen', completed: true, data: { sources: ['FAQ', 'Wissensdatenbank', 'Ticketsystem'] } },
{ id: 'uc3-step-3', name: 'KI-Komponenten', completed: true, data: { algorithms: ['LLM', 'RAG', 'Intent-Classification'] } },
{ id: 'uc3-step-4', name: 'Betroffene', completed: true, data: { subjects: ['Kunden', 'Interessenten'] } },
{ id: 'uc3-step-5', name: 'Risikobewertung', completed: true, data: { riskLevel: 'MEDIUM' } },
],
assessmentResult: {
riskLevel: 'MEDIUM',
applicableRegulations: ['DSGVO', 'AI Act'],
recommendedControls: ['KI-Kennzeichnung', 'Übergabe an Menschen', 'Datensparsamkeit'],
dsfaRequired: false,
aiActClassification: 'LIMITED',
},
createdAt: new Date('2026-01-25'),
updatedAt: new Date('2026-02-03'),
},
]
export function getDemoUseCases(): UseCaseAssessment[] {
return DEMO_USE_CASES.map(uc => ({
...uc,
createdAt: new Date(uc.createdAt),
updatedAt: new Date(uc.updatedAt),
}))
}

View File

@@ -1,316 +0,0 @@
/**
* Demo VVT (Verarbeitungsverzeichnis / Processing Activities Register) for AI Compliance SDK
* Art. 30 DSGVO - These are seed data structures - actual data is stored in database
*/
import { ProcessingActivity, RetentionPolicy } from '../types'
export const DEMO_PROCESSING_ACTIVITIES: ProcessingActivity[] = [
{
id: 'demo-pa-1',
name: 'KI-gestützte Kundenanalyse',
purpose: 'Analyse von Kundenverhalten und Präferenzen zur Personalisierung von Angeboten, Churn-Prediction und Marketing-Optimierung',
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse) / Art. 6 Abs. 1 lit. a DSGVO (Einwilligung für erweitertes Profiling)',
dataCategories: [
'Stammdaten (Name, Adresse, E-Mail, Telefon)',
'Transaktionsdaten (Käufe, Bestellungen, Retouren)',
'Nutzungsdaten (Clickstreams, Seitenaufrufe, Verweildauer)',
'Demographische Daten (Alter, Geschlecht, PLZ-Region)',
],
dataSubjects: [
'Bestandskunden (ca. 250.000 aktive)',
'Registrierte Interessenten (ca. 100.000)',
'Newsletter-Abonnenten (ca. 180.000)',
],
recipients: [
'Interne Fachabteilungen (Marketing, Vertrieb)',
'E-Mail-Marketing-Dienstleister (AV-Vertrag vorhanden)',
'Cloud-Infrastruktur-Anbieter (AV-Vertrag vorhanden)',
],
thirdCountryTransfers: false,
retentionPeriod: '3 Jahre nach letzter Aktivität, danach Anonymisierung',
technicalMeasures: [
'AES-256 Verschlüsselung',
'Pseudonymisierung',
'Zugriffskontrolle mit MFA',
'Audit-Logging',
],
organizationalMeasures: [
'Rollenbasiertes Berechtigungskonzept',
'Verpflichtung auf Datengeheimnis',
'Regelmäßige Datenschutzschulungen',
'Dokumentierte Prozesse',
],
},
{
id: 'demo-pa-2',
name: 'Automatisierte Bewerbungsvorauswahl',
purpose: 'KI-gestützte Vorauswahl von Bewerbungen basierend auf Lebenslauf-Analyse und Qualifikationsabgleich zur Effizienzsteigerung im Recruiting',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (vorvertragliche Maßnahmen) / § 26 BDSG (Beschäftigungsverhältnis)',
dataCategories: [
'Bewerberdaten (Name, Kontakt, Geburtsdatum)',
'Qualifikationen (Ausbildung, Berufserfahrung, Zertifikate)',
'Lebenslaufdaten (Werdegang, Fähigkeiten)',
'Bewerbungsschreiben',
],
dataSubjects: [
'Bewerber auf offene Stellen',
'Initiativbewerber',
],
recipients: [
'HR-Abteilung',
'Fachabteilungsleiter (nur finale Kandidaten)',
'Betriebsrat (Einsichtnahme möglich)',
],
thirdCountryTransfers: false,
retentionPeriod: '6 Monate nach Abschluss des Bewerbungsverfahrens (bei Ablehnung), länger nur mit Einwilligung für Talentpool',
technicalMeasures: [
'Verschlüsselte Speicherung',
'Zugangsbeschränkung auf HR',
'Automatische Löschroutinen',
'Bias-Monitoring',
],
organizationalMeasures: [
'Human-in-the-Loop für finale Entscheidungen',
'Dokumentierte KI-Entscheidungskriterien',
'Transparente Information an Bewerber',
'Regelmäßige Fairness-Audits',
],
},
{
id: 'demo-pa-3',
name: 'Kundenservice-Chatbot',
purpose: 'Automatisierte Beantwortung von Kundenanfragen im First-Level-Support mittels KI-gestütztem Dialogsystem',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Vertragserfüllung) / Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse)',
dataCategories: [
'Kundenstammdaten (zur Identifikation)',
'Kommunikationsinhalte (Chat-Verläufe)',
'Technische Daten (Session-ID, Zeitstempel)',
'Serviceanfragen und deren Lösungen',
],
dataSubjects: [
'Kunden mit aktiven Verträgen',
'Interessenten mit Anfragen',
],
recipients: [
'Kundenservice-Team (bei Eskalation)',
'Cloud-Anbieter (Hosting, AV-Vertrag)',
],
thirdCountryTransfers: false,
retentionPeriod: '2 Jahre für Chat-Verläufe, danach Anonymisierung für Training',
technicalMeasures: [
'TLS-Verschlüsselung',
'Keine Speicherung sensitiver Daten im Chat',
'Automatische PII-Erkennung und Maskierung',
],
organizationalMeasures: [
'Klare KI-Kennzeichnung gegenüber Kunden',
'Jederzeit Übergabe an Menschen möglich',
'Schulung des Eskalations-Teams',
],
},
{
id: 'demo-pa-4',
name: 'Mitarbeiterverwaltung',
purpose: 'Verwaltung von Personalstammdaten, Gehaltsabrechnung, Zeiterfassung und Personalentwicklung',
legalBasis: 'Art. 6 Abs. 1 lit. b DSGVO (Arbeitsvertrag) / § 26 BDSG (Beschäftigungsverhältnis) / gesetzliche Pflichten (Steuer, SV)',
dataCategories: [
'Personalstammdaten (Name, Adresse, Geburtsdatum, SV-Nr.)',
'Vertragsdaten (Arbeitsvertrag, Gehalt, Arbeitszeit)',
'Zeiterfassungsdaten',
'Leistungsbeurteilungen',
'Bankverbindung',
],
dataSubjects: [
'Aktive Mitarbeiter',
'Ehemalige Mitarbeiter (Archiv)',
],
recipients: [
'HR-Abteilung',
'Lohnbuchhaltung / Steuerberater',
'Sozialversicherungsträger',
'Finanzamt',
],
thirdCountryTransfers: false,
retentionPeriod: '10 Jahre nach Ausscheiden (steuerliche Aufbewahrungspflichten)',
technicalMeasures: [
'Verschlüsselte Speicherung',
'Strenge Zugriffskontrolle',
'Getrennte Systeme für verschiedene Datenkategorien',
],
organizationalMeasures: [
'Need-to-know-Prinzip',
'Dokumentierte Prozesse',
'Betriebsvereinbarung zur Datenverarbeitung',
],
},
{
id: 'demo-pa-5',
name: 'Website-Analyse und Marketing',
purpose: 'Analyse des Nutzerverhaltens auf der Website zur Optimierung der User Experience und für personalisierte Marketing-Maßnahmen',
legalBasis: 'Art. 6 Abs. 1 lit. a DSGVO (Einwilligung via Cookie-Banner)',
dataCategories: [
'Pseudonymisierte Nutzungsdaten',
'Cookie-IDs und Tracking-Identifier',
'Geräteinformationen',
'Interaktionsdaten (Klicks, Scrollverhalten)',
],
dataSubjects: [
'Website-Besucher (nur mit Einwilligung)',
],
recipients: [
'Marketing-Team',
'Analytics-Anbieter (AV-Vertrag)',
'Advertising-Partner (nur mit erweiterter Einwilligung)',
],
thirdCountryTransfers: true,
retentionPeriod: '13 Monate für Analytics-Daten, Cookie-Laufzeit max. 12 Monate',
technicalMeasures: [
'IP-Anonymisierung',
'Secure Cookies',
'Consent-Management-System',
],
organizationalMeasures: [
'Transparente Cookie-Richtlinie',
'Einfacher Widerruf möglich',
'Regelmäßige Cookie-Audits',
],
},
{
id: 'demo-pa-6',
name: 'Videoüberwachung',
purpose: 'Schutz von Eigentum und Personen, Prävention und Aufklärung von Straftaten in Geschäftsräumen',
legalBasis: 'Art. 6 Abs. 1 lit. f DSGVO (berechtigtes Interesse an Sicherheit)',
dataCategories: [
'Videoaufnahmen',
'Zeitstempel',
'Aufnahmeort',
],
dataSubjects: [
'Mitarbeiter in überwachten Bereichen',
'Besucher und Kunden',
'Lieferanten',
],
recipients: [
'Sicherheitspersonal',
'Geschäftsleitung (bei Vorfällen)',
'Strafverfolgungsbehörden (auf Anforderung)',
],
thirdCountryTransfers: false,
retentionPeriod: '72 Stunden, bei Vorfällen bis zur Abschluss der Untersuchung',
technicalMeasures: [
'Verschlüsselte Speicherung',
'Automatische Löschung nach Fristablauf',
'Eingeschränkter Zugriff',
],
organizationalMeasures: [
'Beschilderung der überwachten Bereiche',
'Betriebsvereinbarung mit Betriebsrat',
'Dokumentiertes Einsichtsprotokoll',
],
},
]
export const DEMO_RETENTION_POLICIES: RetentionPolicy[] = [
{
id: 'demo-ret-1',
dataCategory: 'Kundenstammdaten',
description: 'Grundlegende Daten zur Kundenidentifikation (Name, Adresse, Kontaktdaten)',
legalBasis: 'Handels- und steuerrechtliche Aufbewahrungspflichten (§ 257 HGB, § 147 AO)',
retentionPeriod: '10 Jahre nach Vertragsende',
deletionMethod: 'Sichere Löschung mit Protokollierung, bei Papier: Aktenvernichtung DIN 66399',
exceptions: [
'Laufende Rechtsstreitigkeiten',
'Offene Forderungen',
],
},
{
id: 'demo-ret-2',
dataCategory: 'Transaktionsdaten',
description: 'Bestellungen, Rechnungen, Zahlungen, Lieferungen',
legalBasis: '§ 257 HGB, § 147 AO (handels- und steuerrechtliche Aufbewahrung)',
retentionPeriod: '10 Jahre ab Ende des Geschäftsjahres',
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
exceptions: [
'Garantiefälle (bis Ende der Garantiezeit)',
'Prüfungen durch Finanzbehörden',
],
},
{
id: 'demo-ret-3',
dataCategory: 'Bewerberdaten',
description: 'Lebenslauf, Anschreiben, Zeugnisse, Korrespondenz',
legalBasis: 'AGG (Diskriminierungsschutz) / § 26 BDSG',
retentionPeriod: '6 Monate nach Abschluss des Verfahrens',
deletionMethod: 'Sichere Löschung, bei Papier: Aktenvernichtung',
exceptions: [
'Aufnahme in Talentpool (mit Einwilligung): 2 Jahre',
'Diskriminierungsklagen: bis Abschluss',
],
},
{
id: 'demo-ret-4',
dataCategory: 'Personalakten',
description: 'Arbeitsverträge, Gehaltsabrechnungen, Beurteilungen, Abmahnungen',
legalBasis: '§ 257 HGB, § 147 AO, Sozialversicherungsrecht',
retentionPeriod: '10 Jahre nach Ausscheiden (teilweise 30 Jahre für Rentenansprüche)',
deletionMethod: 'Sichere Löschung mit Dokumentation',
exceptions: [
'Arbeitsrechtliche Streitigkeiten',
'Rentenversicherungsnachweise (lebenslang empfohlen)',
],
},
{
id: 'demo-ret-5',
dataCategory: 'Marketing-Profile',
description: 'Analysedaten, Segmentierungen, Präferenzen, Kaufhistorie',
legalBasis: 'Einwilligung (Art. 6 Abs. 1 lit. a DSGVO)',
retentionPeriod: '3 Jahre nach letzter Aktivität, dann Anonymisierung',
deletionMethod: 'Pseudonymisierung → Anonymisierung → Löschung',
exceptions: [
'Widerruf der Einwilligung (sofortige Löschung)',
],
},
{
id: 'demo-ret-6',
dataCategory: 'Videoaufnahmen',
description: 'Aufnahmen der Sicherheitskameras',
legalBasis: 'Berechtigtes Interesse (Art. 6 Abs. 1 lit. f DSGVO)',
retentionPeriod: '72 Stunden',
deletionMethod: 'Automatisches Überschreiben',
exceptions: [
'Sicherheitsvorfälle (bis Abschluss der Untersuchung)',
'Anforderung durch Strafverfolgungsbehörden',
],
},
{
id: 'demo-ret-7',
dataCategory: 'KI-Trainingsdaten',
description: 'Anonymisierte Datensätze für Modell-Training',
legalBasis: 'Berechtigtes Interesse / ursprüngliche Zweckbindung (bei Kompatibilität)',
retentionPeriod: 'Solange Modell aktiv, danach Löschung mit Modell-Archivierung',
deletionMethod: 'Sichere Löschung bei Modell-Retirement',
exceptions: [
'Audit-Trail für Modell-Herkunft (anonymisierte Metadaten)',
],
},
{
id: 'demo-ret-8',
dataCategory: 'Audit-Logs',
description: 'Protokolle von Datenzugriffen und Systemereignissen',
legalBasis: 'Nachweispflichten DSGVO, Compliance-Anforderungen',
retentionPeriod: '10 Jahre',
deletionMethod: 'Automatisierte Löschung nach Fristablauf',
exceptions: [
'Laufende Untersuchungen oder Audits',
],
},
]
export function getDemoProcessingActivities(): ProcessingActivity[] {
return DEMO_PROCESSING_ACTIVITIES
}
export function getDemoRetentionPolicies(): RetentionPolicy[] {
return DEMO_RETENTION_POLICIES
}

View File

@@ -1,548 +0,0 @@
/**
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
* in den Dokumentengenerator.
*
* Diese Funktionen generieren DSGVO-konforme Textbausteine basierend auf
* den vom Benutzer ausgewählten Datenpunkten.
*/
import {
DataPoint,
DataPointCategory,
LegalBasis,
RetentionPeriod,
RiskLevel,
CATEGORY_METADATA,
LEGAL_BASIS_INFO,
RETENTION_PERIOD_INFO,
RISK_LEVEL_STYLING,
LocalizedText,
SupportedLanguage
} from '@/lib/sdk/einwilligungen/types'
// =============================================================================
// TYPES
// =============================================================================
/**
* Sprach-Option für alle Helper-Funktionen
*/
export type Language = SupportedLanguage
/**
* Generierte Platzhalter-Map für den Dokumentengenerator
*/
export interface DataPointPlaceholders {
'[DATENPUNKTE_COUNT]': string
'[DATENPUNKTE_LIST]': string
'[DATENPUNKTE_TABLE]': string
'[VERARBEITUNGSZWECKE]': string
'[RECHTSGRUNDLAGEN]': string
'[SPEICHERFRISTEN]': string
'[EMPFAENGER]': string
'[BESONDERE_KATEGORIEN]': string
'[DRITTLAND_TRANSFERS]': string
'[RISIKO_ZUSAMMENFASSUNG]': string
}
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
/**
* Extrahiert Text aus LocalizedText basierend auf Sprache
*/
function getText(text: LocalizedText, lang: Language): string {
return text[lang] || text.de
}
/**
* Generiert eine Markdown-Tabelle der Datenpunkte
*
* @param dataPoints - Liste der ausgewählten Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Markdown-Tabelle als String
*/
export function generateDataPointsTable(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
if (dataPoints.length === 0) {
return lang === 'de'
? '*Keine Datenpunkte ausgewählt.*'
: '*No data points selected.*'
}
const header = lang === 'de'
? '| Datenpunkt | Kategorie | Zweck | Rechtsgrundlage | Speicherfrist |'
: '| Data Point | Category | Purpose | Legal Basis | Retention Period |'
const separator = '|------------|-----------|-------|-----------------|---------------|'
const rows = dataPoints.map(dp => {
const category = CATEGORY_METADATA[dp.category]
const legalBasis = LEGAL_BASIS_INFO[dp.legalBasis]
const retention = RETENTION_PERIOD_INFO[dp.retentionPeriod]
const name = getText(dp.name, lang)
const categoryName = getText(category.name, lang)
const purpose = getText(dp.purpose, lang)
const legalBasisName = getText(legalBasis.name, lang)
const retentionLabel = getText(retention.label, lang)
// Truncate long texts for table readability
const truncatedPurpose = purpose.length > 50 ? purpose.slice(0, 47) + '...' : purpose
return `| ${name} | ${categoryName} | ${truncatedPurpose} | ${legalBasisName} | ${retentionLabel} |`
}).join('\n')
return `${header}\n${separator}\n${rows}`
}
/**
* Gruppiert Datenpunkte nach Speicherfrist
*
* @param dataPoints - Liste der Datenpunkte
* @returns Record mit Speicherfrist als Key und Datenpunkten als Value
*/
export function groupByRetention(
dataPoints: DataPoint[]
): Record<RetentionPeriod, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.retentionPeriod
if (!acc[key]) {
acc[key] = []
}
acc[key].push(dp)
return acc
}, {} as Record<RetentionPeriod, DataPoint[]>)
}
/**
* Gruppiert Datenpunkte nach Kategorie
*
* @param dataPoints - Liste der Datenpunkte
* @returns Record mit Kategorie als Key und Datenpunkten als Value
*/
export function groupByCategory(
dataPoints: DataPoint[]
): Record<DataPointCategory, DataPoint[]> {
return dataPoints.reduce((acc, dp) => {
const key = dp.category
if (!acc[key]) {
acc[key] = []
}
acc[key].push(dp)
return acc
}, {} as Record<DataPointCategory, DataPoint[]>)
}
/**
* Generiert DSGVO-konformen Abschnitt für besondere Kategorien (Art. 9 DSGVO)
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Markdown-Abschnitt als String (leer wenn keine Art. 9 Daten)
*/
export function generateSpecialCategorySection(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const special = dataPoints.filter(dp => dp.isSpecialCategory)
if (special.length === 0) {
return ''
}
if (lang === 'de') {
const items = special.map(dp =>
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
).join('\n')
return `## Verarbeitung besonderer Kategorien personenbezogener Daten (Art. 9 DSGVO)
Wir verarbeiten folgende besondere Kategorien personenbezogener Daten:
${items}
Die Verarbeitung erfolgt auf Grundlage Ihrer ausdrücklichen Einwilligung gemäß Art. 9 Abs. 2 lit. a DSGVO. Sie können Ihre Einwilligung jederzeit mit Wirkung für die Zukunft widerrufen.`
} else {
const items = special.map(dp =>
`- **${getText(dp.name, lang)}**: ${getText(dp.description, lang)}`
).join('\n')
return `## Processing of Special Categories of Personal Data (Art. 9 GDPR)
We process the following special categories of personal data:
${items}
Processing is based on your explicit consent pursuant to Art. 9(2)(a) GDPR. You may withdraw your consent at any time with effect for the future.`
}
}
/**
* Generiert Liste aller eindeutigen Verarbeitungszwecke
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Kommaseparierte Liste der Zwecke
*/
export function generatePurposesList(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const purposes = new Set<string>()
dataPoints.forEach(dp => {
purposes.add(getText(dp.purpose, lang))
})
return [...purposes].join(', ')
}
/**
* Generiert Liste aller verwendeten Rechtsgrundlagen
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Formatierte Liste der Rechtsgrundlagen
*/
export function generateLegalBasisList(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const bases = new Set<LegalBasis>()
dataPoints.forEach(dp => {
bases.add(dp.legalBasis)
})
return [...bases].map(basis => {
const info = LEGAL_BASIS_INFO[basis]
return `${info.article} (${getText(info.name, lang)})`
}).join(', ')
}
/**
* Generiert Liste aller Speicherfristen gruppiert
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Formatierte Liste der Speicherfristen mit zugehörigen Kategorien
*/
export function generateRetentionList(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const grouped = groupByRetention(dataPoints)
const entries: string[] = []
for (const [period, points] of Object.entries(grouped)) {
const retentionInfo = RETENTION_PERIOD_INFO[period as RetentionPeriod]
const categories = [...new Set(points.map(p => getText(CATEGORY_METADATA[p.category].name, lang)))]
entries.push(`${getText(retentionInfo.label, lang)}: ${categories.join(', ')}`)
}
return entries.join('; ')
}
/**
* Generiert Liste aller Empfänger/Drittparteien
*
* @param dataPoints - Liste der Datenpunkte
* @returns Kommaseparierte Liste der Empfänger
*/
export function generateRecipientsList(dataPoints: DataPoint[]): string {
const recipients = new Set<string>()
dataPoints.forEach(dp => {
dp.thirdPartyRecipients?.forEach(r => recipients.add(r))
})
if (recipients.size === 0) {
return ''
}
return [...recipients].join(', ')
}
/**
* Generiert Abschnitt für Drittland-Übermittlungen
*
* @param dataPoints - Liste der Datenpunkte mit thirdCountryTransfer === true
* @param lang - Sprache für die Ausgabe
* @returns Markdown-Abschnitt als String
*/
export function generateThirdCountrySection(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
// Note: We assume dataPoints have been filtered for thirdCountryTransfer
// The actual flag would need to be added to the DataPoint interface
// For now, we check if any thirdPartyRecipients suggest third country
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare']
const thirdCountryPoints = dataPoints.filter(dp =>
dp.thirdPartyRecipients?.some(r =>
thirdCountryIndicators.some(indicator =>
r.toLowerCase().includes(indicator.toLowerCase())
)
)
)
if (thirdCountryPoints.length === 0) {
return ''
}
const recipients = new Set<string>()
thirdCountryPoints.forEach(dp => {
dp.thirdPartyRecipients?.forEach(r => {
if (thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))) {
recipients.add(r)
}
})
})
if (lang === 'de') {
return `## Übermittlung in Drittländer
Wir übermitteln personenbezogene Daten an folgende Empfänger in Drittländern (außerhalb der EU/des EWR):
${[...recipients].map(r => `- ${r}`).join('\n')}
Die Übermittlung erfolgt auf Grundlage von Standardvertragsklauseln (Art. 46 Abs. 2 lit. c DSGVO) bzw. einem Angemessenheitsbeschluss der EU-Kommission (Art. 45 DSGVO).`
} else {
return `## Transfers to Third Countries
We transfer personal data to the following recipients in third countries (outside the EU/EEA):
${[...recipients].map(r => `- ${r}`).join('\n')}
The transfer is based on Standard Contractual Clauses (Art. 46(2)(c) GDPR) or an adequacy decision by the EU Commission (Art. 45 GDPR).`
}
}
/**
* Generiert Risiko-Zusammenfassung
*
* @param dataPoints - Liste der Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Formatierte Risiko-Zusammenfassung
*/
export function generateRiskSummary(
dataPoints: DataPoint[],
lang: Language = 'de'
): string {
const riskCounts: Record<RiskLevel, number> = {
LOW: 0,
MEDIUM: 0,
HIGH: 0
}
dataPoints.forEach(dp => {
riskCounts[dp.riskLevel]++
})
const parts = Object.entries(riskCounts)
.filter(([, count]) => count > 0)
.map(([level, count]) => {
const styling = RISK_LEVEL_STYLING[level as RiskLevel]
return `${count} ${getText(styling.label, lang).toLowerCase()}`
})
return parts.join(', ')
}
/**
* Generiert alle Platzhalter für den Dokumentengenerator
*
* @param dataPoints - Liste der ausgewählten Datenpunkte
* @param lang - Sprache für die Ausgabe
* @returns Objekt mit allen Platzhaltern
*/
export function generateAllPlaceholders(
dataPoints: DataPoint[],
lang: Language = 'de'
): DataPointPlaceholders {
return {
'[DATENPUNKTE_COUNT]': String(dataPoints.length),
'[DATENPUNKTE_LIST]': dataPoints.map(dp => getText(dp.name, lang)).join(', '),
'[DATENPUNKTE_TABLE]': generateDataPointsTable(dataPoints, lang),
'[VERARBEITUNGSZWECKE]': generatePurposesList(dataPoints, lang),
'[RECHTSGRUNDLAGEN]': generateLegalBasisList(dataPoints, lang),
'[SPEICHERFRISTEN]': generateRetentionList(dataPoints, lang),
'[EMPFAENGER]': generateRecipientsList(dataPoints),
'[BESONDERE_KATEGORIEN]': generateSpecialCategorySection(dataPoints, lang),
'[DRITTLAND_TRANSFERS]': generateThirdCountrySection(dataPoints, lang),
'[RISIKO_ZUSAMMENFASSUNG]': generateRiskSummary(dataPoints, lang)
}
}
// =============================================================================
// VALIDATION HELPERS
// =============================================================================
/**
* Validierungswarnung für den Dokumentengenerator
*/
export interface ValidationWarning {
type: 'error' | 'warning' | 'info'
code: string
message: string
suggestion: string
affectedDataPoints?: DataPoint[]
}
/**
* Prüft ob besondere Kategorien vorhanden sind aber kein entsprechender Abschnitt
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns ValidationWarning oder null
*/
export function checkSpecialCategoriesWarning(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning | null {
const specialCategories = dataPoints.filter(dp => dp.isSpecialCategory)
if (specialCategories.length === 0) {
return null
}
const hasSection = lang === 'de'
? documentContent.includes('Art. 9') || documentContent.includes('Artikel 9') || documentContent.includes('besondere Kategorie')
: documentContent.includes('Art. 9') || documentContent.includes('Article 9') || documentContent.includes('special categor')
if (!hasSection) {
return {
type: 'error',
code: 'MISSING_ART9_SECTION',
message: lang === 'de'
? `${specialCategories.length} besondere Datenkategorien (Art. 9 DSGVO) ausgewählt, aber kein entsprechender Abschnitt im Dokument gefunden.`
: `${specialCategories.length} special data categories (Art. 9 GDPR) selected, but no corresponding section found in document.`,
suggestion: lang === 'de'
? 'Fügen Sie einen Abschnitt zu besonderen Kategorien personenbezogener Daten hinzu oder verwenden Sie [BESONDERE_KATEGORIEN] als Platzhalter.'
: 'Add a section about special categories of personal data or use [BESONDERE_KATEGORIEN] as placeholder.',
affectedDataPoints: specialCategories
}
}
return null
}
/**
* Prüft ob Drittland-Übermittlungen vorhanden sind aber keine SCC erwähnt werden
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns ValidationWarning oder null
*/
export function checkThirdCountryWarning(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning | null {
const thirdCountryIndicators = ['Google', 'AWS', 'Microsoft', 'Meta', 'Facebook', 'Cloudflare', 'USA', 'US']
const thirdCountryPoints = dataPoints.filter(dp =>
dp.thirdPartyRecipients?.some(r =>
thirdCountryIndicators.some(i => r.toLowerCase().includes(i.toLowerCase()))
)
)
if (thirdCountryPoints.length === 0) {
return null
}
const hasSCCMention = lang === 'de'
? documentContent.includes('Standardvertragsklauseln') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
: documentContent.includes('Standard Contractual Clauses') || documentContent.includes('SCC') || documentContent.includes('Art. 46')
if (!hasSCCMention) {
return {
type: 'warning',
code: 'MISSING_SCC_SECTION',
message: lang === 'de'
? `Drittland-Übermittlung für ${thirdCountryPoints.length} Datenpunkte erkannt, aber keine Standardvertragsklauseln (SCC) erwähnt.`
: `Third country transfer detected for ${thirdCountryPoints.length} data points, but no Standard Contractual Clauses (SCC) mentioned.`,
suggestion: lang === 'de'
? 'Erwägen Sie die Aufnahme eines Abschnitts zu Drittland-Übermittlungen und Standardvertragsklauseln oder verwenden Sie [DRITTLAND_TRANSFERS] als Platzhalter.'
: 'Consider adding a section about third country transfers and Standard Contractual Clauses or use [DRITTLAND_TRANSFERS] as placeholder.',
affectedDataPoints: thirdCountryPoints
}
}
return null
}
/**
* Prüft ob Datenpunkte mit expliziter Einwilligung korrekt behandelt werden
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns ValidationWarning oder null
*/
export function checkExplicitConsentWarning(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning | null {
const explicitConsentPoints = dataPoints.filter(dp => dp.requiresExplicitConsent)
if (explicitConsentPoints.length === 0) {
return null
}
const hasConsentSection = lang === 'de'
? documentContent.includes('Einwilligung') || documentContent.includes('Widerruf') || documentContent.includes('Art. 7')
: documentContent.includes('consent') || documentContent.includes('withdraw') || documentContent.includes('Art. 7')
if (!hasConsentSection) {
return {
type: 'warning',
code: 'MISSING_CONSENT_SECTION',
message: lang === 'de'
? `${explicitConsentPoints.length} Datenpunkte erfordern ausdrückliche Einwilligung, aber kein Abschnitt zu Einwilligung/Widerruf gefunden.`
: `${explicitConsentPoints.length} data points require explicit consent, but no section about consent/withdrawal found.`,
suggestion: lang === 'de'
? 'Fügen Sie einen Abschnitt zum Widerrufsrecht hinzu.'
: 'Add a section about the right to withdraw consent.',
affectedDataPoints: explicitConsentPoints
}
}
return null
}
/**
* Führt alle Validierungsprüfungen durch
*
* @param dataPoints - Liste der Datenpunkte
* @param documentContent - Der generierte Dokumentinhalt
* @param lang - Sprache
* @returns Array aller Warnungen
*/
export function validateDocument(
dataPoints: DataPoint[],
documentContent: string,
lang: Language = 'de'
): ValidationWarning[] {
const warnings: ValidationWarning[] = []
const specialCatWarning = checkSpecialCategoriesWarning(dataPoints, documentContent, lang)
if (specialCatWarning) warnings.push(specialCatWarning)
const thirdCountryWarning = checkThirdCountryWarning(dataPoints, documentContent, lang)
if (thirdCountryWarning) warnings.push(thirdCountryWarning)
const consentWarning = checkExplicitConsentWarning(dataPoints, documentContent, lang)
if (consentWarning) warnings.push(consentWarning)
return warnings
}

View File

@@ -1,8 +0,0 @@
/**
* Document Generator Library
*
* Helper-Funktionen für die Integration von Einwilligungen-Datenpunkten
* in den Dokumentengenerator.
*/
export * from './datapoint-helpers'

View File

@@ -1,224 +0,0 @@
import { ConstraintEnforcer } from '../constraint-enforcer'
import type { ScopeDecision } from '../../compliance-scope-types'
describe('ConstraintEnforcer', () => {
const enforcer = new ConstraintEnforcer()
// Helper: minimal valid ScopeDecision
function makeDecision(overrides: Partial<ScopeDecision> = {}): ScopeDecision {
return {
id: 'test-decision',
determinedLevel: 'L2',
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
triggeredHardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '3h', triggeredBy: [] },
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
],
riskFlags: [],
gaps: [],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
...overrides,
} as ScopeDecision
}
describe('check - no decision', () => {
it('should allow basic documents (vvt, tom, dsi) without decision', () => {
const result = enforcer.check('vvt', null)
expect(result.allowed).toBe(true)
expect(result.adjustments.length).toBeGreaterThan(0)
expect(result.checkedRules).toContain('RULE-NO-DECISION')
})
it('should allow tom without decision', () => {
const result = enforcer.check('tom', null)
expect(result.allowed).toBe(true)
})
it('should allow dsi without decision', () => {
const result = enforcer.check('dsi', null)
expect(result.allowed).toBe(true)
})
it('should block non-basic documents without decision', () => {
const result = enforcer.check('dsfa', null)
expect(result.allowed).toBe(false)
expect(result.violations.length).toBeGreaterThan(0)
})
it('should block av_vertrag without decision', () => {
const result = enforcer.check('av_vertrag', null)
expect(result.allowed).toBe(false)
})
})
describe('check - RULE-DOC-REQUIRED', () => {
it('should allow required documents', () => {
const decision = makeDecision()
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
})
it('should warn but allow optional documents', () => {
const decision = makeDecision({
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
],
})
const result = enforcer.check('dsfa', decision)
expect(result.allowed).toBe(true) // Only warns, does not block
expect(result.adjustments.some(a => a.includes('nicht als Pflicht'))).toBe(true)
})
})
describe('check - RULE-DEPTH-MATCH', () => {
it('should block when requested depth exceeds determined level', () => {
const decision = makeDecision({ determinedLevel: 'L2' })
const result = enforcer.check('vvt', decision, 'L4')
expect(result.allowed).toBe(false)
expect(result.violations.some(v => v.includes('ueberschreitet'))).toBe(true)
})
it('should allow when requested depth matches level', () => {
const decision = makeDecision({ determinedLevel: 'L2' })
const result = enforcer.check('vvt', decision, 'L2')
expect(result.allowed).toBe(true)
})
it('should adjust when requested depth is below level', () => {
const decision = makeDecision({ determinedLevel: 'L3' })
const result = enforcer.check('vvt', decision, 'L1')
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('angehoben'))).toBe(true)
})
it('should allow without requested depth level', () => {
const decision = makeDecision({ determinedLevel: 'L3' })
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
})
})
describe('check - RULE-DSFA-ENFORCEMENT', () => {
it('should note when DSFA is not required but requested', () => {
const decision = makeDecision({ determinedLevel: 'L2' })
const result = enforcer.check('dsfa', decision)
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('nicht verpflichtend'))).toBe(true)
})
it('should allow DSFA when hard triggers require it', () => {
const decision = makeDecision({
determinedLevel: 'L3',
triggeredHardTriggers: [{
rule: {
id: 'HT-ART9',
label: 'Art. 9 Daten',
description: '',
conditionField: '',
conditionOperator: 'EQUALS' as const,
conditionValue: null,
minimumLevel: 'L3',
mandatoryDocuments: ['dsfa'],
dsfaRequired: true,
legalReference: 'Art. 35 DSGVO',
},
matchedValue: null,
explanation: 'Art. 9 Daten verarbeitet',
}],
})
const result = enforcer.check('dsfa', decision)
expect(result.allowed).toBe(true)
})
it('should warn about DSFA when drafting non-DSFA but DSFA is required', () => {
const decision = makeDecision({
determinedLevel: 'L3',
triggeredHardTriggers: [{
rule: {
id: 'HT-ART9',
label: 'Art. 9 Daten',
description: '',
conditionField: '',
conditionOperator: 'EQUALS' as const,
conditionValue: null,
minimumLevel: 'L3',
mandatoryDocuments: ['dsfa'],
dsfaRequired: true,
legalReference: 'Art. 35 DSGVO',
},
matchedValue: null,
explanation: '',
}],
requiredDocuments: [
{ documentType: 'dsfa', label: 'DSFA', required: true, depth: 'Vollstaendig', detailItems: [], estimatedEffort: '8h', triggeredBy: [] },
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h', triggeredBy: [] },
],
})
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('DSFA') && a.includes('verpflichtend'))).toBe(true)
})
})
describe('check - RULE-RISK-FLAGS', () => {
it('should note critical risk flags', () => {
const decision = makeDecision({
riskFlags: [
{ id: 'rf-1', severity: 'CRITICAL', title: 'Offene Art. 9 Verarbeitung', description: '', recommendation: 'DSFA durchfuehren' },
{ id: 'rf-2', severity: 'HIGH', title: 'Fehlende Verschluesselung', description: '', recommendation: 'TOM erstellen' },
{ id: 'rf-3', severity: 'LOW', title: 'Dokumentation unvollstaendig', description: '', recommendation: '' },
],
})
const result = enforcer.check('vvt', decision)
expect(result.allowed).toBe(true)
expect(result.adjustments.some(a => a.includes('2 kritische/hohe Risiko-Flags'))).toBe(true)
})
it('should not flag when no risk flags present', () => {
const decision = makeDecision({ riskFlags: [] })
const result = enforcer.check('vvt', decision)
expect(result.adjustments.every(a => !a.includes('Risiko-Flags'))).toBe(true)
})
})
describe('check - checkedRules tracking', () => {
it('should track all checked rules', () => {
const decision = makeDecision()
const result = enforcer.check('vvt', decision)
expect(result.checkedRules).toContain('RULE-DOC-REQUIRED')
expect(result.checkedRules).toContain('RULE-DEPTH-MATCH')
expect(result.checkedRules).toContain('RULE-DSFA-ENFORCEMENT')
expect(result.checkedRules).toContain('RULE-RISK-FLAGS')
expect(result.checkedRules).toContain('RULE-HARD-TRIGGER-CONSISTENCY')
})
})
describe('checkFromContext', () => {
it('should reconstruct decision from DraftContext and check', () => {
const context = {
decisions: {
level: 'L2' as const,
scores: { risk_score: 50, complexity_score: 50, assurance_need: 50, composite_score: 50 },
hardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt' as const, depth: 'Standard', detailItems: [] },
],
},
companyProfile: { name: 'Test GmbH', industry: 'IT', employeeCount: 50, businessModel: 'SaaS', isPublicSector: false },
constraints: {
depthRequirements: { required: true, depth: 'Standard', detailItems: [], estimatedEffort: '2h' },
riskFlags: [],
boundaries: [],
},
}
const result = enforcer.checkFromContext('vvt', context)
expect(result.allowed).toBe(true)
expect(result.checkedRules.length).toBeGreaterThan(0)
})
})
})

View File

@@ -1,153 +0,0 @@
import { IntentClassifier } from '../intent-classifier'
describe('IntentClassifier', () => {
const classifier = new IntentClassifier()
describe('classify - Draft mode', () => {
it.each([
['Erstelle ein VVT fuer unseren Hauptprozess', 'draft'],
['Generiere eine TOM-Dokumentation', 'draft'],
['Schreibe eine Datenschutzerklaerung', 'draft'],
['Verfasse einen Entwurf fuer das Loeschkonzept', 'draft'],
['Create a DSFA document', 'draft'],
['Draft a privacy policy for us', 'draft'],
['Neues VVT anlegen', 'draft'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.7)
})
})
describe('classify - Validate mode', () => {
it.each([
['Pruefe die Konsistenz meiner Dokumente', 'validate'],
['Ist mein VVT korrekt?', 'validate'],
['Validiere die TOM gegen das VVT', 'validate'],
['Check die Vollstaendigkeit', 'validate'],
['Stimmt das mit der DSFA ueberein?', 'validate'],
['Cross-Check VVT und TOM', 'validate'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.7)
})
})
describe('classify - Ask mode', () => {
it.each([
['Was fehlt noch in meinem Profil?', 'ask'],
['Zeige mir die Luecken', 'ask'],
['Welche Dokumente fehlen noch?', 'ask'],
['Was ist der naechste Schritt?', 'ask'],
['Welche Informationen brauche ich noch?', 'ask'],
])('"%s" should classify as %s', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
expect(result.confidence).toBeGreaterThan(0.6)
})
})
describe('classify - Explain mode (fallback)', () => {
it.each([
['Was ist DSGVO?', 'explain'],
['Erklaere mir Art. 30', 'explain'],
['Hallo', 'explain'],
['Danke fuer die Hilfe', 'explain'],
])('"%s" should classify as %s (fallback)', (input, expectedMode) => {
const result = classifier.classify(input)
expect(result.mode).toBe(expectedMode)
})
})
describe('classify - confidence thresholds', () => {
it('should have high confidence for clear draft intents', () => {
const result = classifier.classify('Erstelle ein neues VVT')
expect(result.confidence).toBeGreaterThanOrEqual(0.85)
})
it('should have lower confidence for ambiguous inputs', () => {
const result = classifier.classify('Hallo')
expect(result.confidence).toBeLessThan(0.6)
})
it('should boost confidence with document type detection', () => {
const withDoc = classifier.classify('Erstelle VVT')
const withoutDoc = classifier.classify('Erstelle etwas')
expect(withDoc.confidence).toBeGreaterThanOrEqual(withoutDoc.confidence)
})
it('should boost confidence with multiple pattern matches', () => {
const single = classifier.classify('Erstelle Dokument')
const multi = classifier.classify('Erstelle und generiere ein neues Dokument')
expect(multi.confidence).toBeGreaterThanOrEqual(single.confidence)
})
})
describe('detectDocumentType', () => {
it.each([
['VVT erstellen', 'vvt'],
['Verarbeitungsverzeichnis', 'vvt'],
['Art. 30 Dokumentation', 'vvt'],
['TOM definieren', 'tom'],
['technisch organisatorische Massnahmen', 'tom'],
['Art. 32 Massnahmen', 'tom'],
['DSFA durchfuehren', 'dsfa'],
['Datenschutz-Folgenabschaetzung', 'dsfa'],
['Art. 35 Pruefung', 'dsfa'],
['DPIA erstellen', 'dsfa'],
['Datenschutzerklaerung', 'dsi'],
['Privacy Policy', 'dsi'],
['Art. 13 Information', 'dsi'],
['Loeschfristen definieren', 'lf'],
['Loeschkonzept erstellen', 'lf'],
['Retention Policy', 'lf'],
['Auftragsverarbeitung', 'av_vertrag'],
['AVV erstellen', 'av_vertrag'],
['Art. 28 Vertrag', 'av_vertrag'],
['Einwilligung einholen', 'einwilligung'],
['Consent Management', 'einwilligung'],
['Cookie Banner', 'einwilligung'],
])('"%s" should detect document type %s', (input, expectedType) => {
const result = classifier.detectDocumentType(input)
expect(result).toBe(expectedType)
})
it('should return undefined for unrecognized types', () => {
expect(classifier.detectDocumentType('Hallo Welt')).toBeUndefined()
expect(classifier.detectDocumentType('Was kostet das?')).toBeUndefined()
})
})
describe('classify - Umlaut handling', () => {
it('should handle German umlauts correctly', () => {
// With actual umlauts (ä, ö, ü)
const result1 = classifier.classify('Prüfe die Vollständigkeit')
expect(result1.mode).toBe('validate')
// With ae/oe/ue substitution
const result2 = classifier.classify('Pruefe die Vollstaendigkeit')
expect(result2.mode).toBe('validate')
})
it('should handle ß correctly', () => {
const result = classifier.classify('Schließe Lücken')
// Should still detect via normalized patterns
expect(result).toBeDefined()
})
})
describe('classify - combined mode + document type', () => {
it('should detect both mode and document type', () => {
const result = classifier.classify('Erstelle ein VVT fuer unsere Firma')
expect(result.mode).toBe('draft')
expect(result.detectedDocumentType).toBe('vvt')
})
it('should detect validate + document type', () => {
const result = classifier.classify('Pruefe mein TOM auf Konsistenz')
expect(result.mode).toBe('validate')
expect(result.detectedDocumentType).toBe('tom')
})
})
})

View File

@@ -1,311 +0,0 @@
import { StateProjector } from '../state-projector'
import type { SDKState } from '../../types'
describe('StateProjector', () => {
const projector = new StateProjector()
// Helper: minimal SDKState
function makeState(overrides: Partial<SDKState> = {}): SDKState {
return {
version: '1.0.0',
lastModified: new Date(),
tenantId: 'test',
userId: 'user1',
subscription: 'PROFESSIONAL',
customerType: null,
companyProfile: null,
complianceScope: null,
currentPhase: 1,
currentStep: 'company-profile',
completedSteps: [],
checkpoints: {},
importedDocuments: [],
gapAnalysis: null,
useCases: [],
activeUseCase: null,
screening: null,
modules: [],
requirements: [],
controls: [],
evidence: [],
checklist: [],
risks: [],
aiActClassification: null,
obligations: [],
dsfa: null,
toms: [],
retentionPolicies: [],
vvt: [],
documents: [],
cookieBanner: null,
consents: [],
dsrConfig: null,
escalationWorkflows: [],
preferences: {
language: 'de',
theme: 'light',
compactMode: false,
showHints: true,
autoSave: true,
autoValidate: true,
allowParallelWork: true,
},
...overrides,
} as SDKState
}
function makeDecisionState(level: string = 'L2'): SDKState {
return makeState({
companyProfile: {
companyName: 'Test GmbH',
industry: 'IT-Dienstleistung',
employeeCount: 50,
businessModel: 'SaaS',
isPublicSector: false,
} as any,
complianceScope: {
decision: {
id: 'dec-1',
determinedLevel: level,
scores: { risk_score: 60, complexity_score: 50, assurance_need: 55, composite_score: 55 },
triggeredHardTriggers: [],
requiredDocuments: [
{ documentType: 'vvt', label: 'VVT', required: true, depth: 'Standard', detailItems: ['Bezeichnung', 'Zweck'], estimatedEffort: '2h', triggeredBy: [] },
{ documentType: 'tom', label: 'TOM', required: true, depth: 'Standard', detailItems: ['Verschluesselung'], estimatedEffort: '3h', triggeredBy: [] },
{ documentType: 'lf', label: 'LF', required: true, depth: 'Basis', detailItems: [], estimatedEffort: '1h', triggeredBy: [] },
],
riskFlags: [
{ id: 'rf-1', severity: 'MEDIUM', title: 'Cloud-Nutzung', description: '', recommendation: 'AVV pruefen' },
],
gaps: [
{ id: 'gap-1', severity: 'high', title: 'TOM fehlt', description: 'Keine TOM definiert', relatedDocuments: ['tom'] },
],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
answers: [],
} as any,
vvt: [{ id: 'vvt-1', name: 'Kundenverwaltung' }] as any[],
toms: [],
retentionPolicies: [],
})
}
describe('projectForDraft', () => {
it('should return a DraftContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result).toHaveProperty('decisions')
expect(result).toHaveProperty('companyProfile')
expect(result).toHaveProperty('constraints')
expect(result.decisions.level).toBe('L2')
})
it('should project company profile', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.companyProfile.name).toBe('Test GmbH')
expect(result.companyProfile.industry).toBe('IT-Dienstleistung')
expect(result.companyProfile.employeeCount).toBe(50)
})
it('should provide defaults when no company profile', () => {
const state = makeState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.companyProfile.name).toBe('Unbekannt')
expect(result.companyProfile.industry).toBe('Unbekannt')
expect(result.companyProfile.employeeCount).toBe(0)
})
it('should extract constraints and depth requirements', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.constraints.depthRequirements).toBeDefined()
expect(result.constraints.boundaries.length).toBeGreaterThan(0)
})
it('should extract risk flags', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.constraints.riskFlags.length).toBe(1)
expect(result.constraints.riskFlags[0].title).toBe('Cloud-Nutzung')
})
it('should include existing document data when available', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.existingDocumentData).toBeDefined()
expect((result.existingDocumentData as any).totalCount).toBe(1)
})
it('should return undefined existingDocumentData when none exists', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'tom')
expect(result.existingDocumentData).toBeUndefined()
})
it('should filter required documents', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.decisions.requiredDocuments.length).toBe(3)
expect(result.decisions.requiredDocuments.every(d => d.documentType)).toBe(true)
})
it('should handle empty state gracefully', () => {
const state = makeState()
const result = projector.projectForDraft(state, 'vvt')
expect(result.decisions.level).toBe('L1')
expect(result.decisions.hardTriggers).toEqual([])
expect(result.decisions.requiredDocuments).toEqual([])
})
})
describe('projectForAsk', () => {
it('should return a GapContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
expect(result).toHaveProperty('unansweredQuestions')
expect(result).toHaveProperty('gaps')
expect(result).toHaveProperty('missingDocuments')
})
it('should identify missing documents', () => {
const state = makeDecisionState()
// vvt exists, tom and lf are missing
const result = projector.projectForAsk(state)
expect(result.missingDocuments.some(d => d.documentType === 'tom')).toBe(true)
expect(result.missingDocuments.some(d => d.documentType === 'lf')).toBe(true)
})
it('should not list existing documents as missing', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
// vvt exists in state
expect(result.missingDocuments.some(d => d.documentType === 'vvt')).toBe(false)
})
it('should include gaps from scope decision', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
expect(result.gaps.length).toBe(1)
expect(result.gaps[0].title).toBe('TOM fehlt')
})
it('should handle empty state', () => {
const state = makeState()
const result = projector.projectForAsk(state)
expect(result.gaps).toEqual([])
expect(result.missingDocuments).toEqual([])
})
})
describe('projectForValidate', () => {
it('should return a ValidationContext with correct structure', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result).toHaveProperty('documents')
expect(result).toHaveProperty('crossReferences')
expect(result).toHaveProperty('scopeLevel')
expect(result).toHaveProperty('depthRequirements')
})
it('should include all requested document types', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.documents.length).toBe(2)
expect(result.documents.map(d => d.type)).toContain('vvt')
expect(result.documents.map(d => d.type)).toContain('tom')
})
it('should include cross-references', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result.crossReferences).toHaveProperty('vvtCategories')
expect(result.crossReferences).toHaveProperty('tomControls')
expect(result.crossReferences).toHaveProperty('retentionCategories')
expect(result.crossReferences.vvtCategories.length).toBe(1)
expect(result.crossReferences.vvtCategories[0]).toBe('Kundenverwaltung')
})
it('should include scope level', () => {
const state = makeDecisionState('L3')
const result = projector.projectForValidate(state, ['vvt'])
expect(result.scopeLevel).toBe('L3')
})
it('should include depth requirements per document type', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.depthRequirements).toHaveProperty('vvt')
expect(result.depthRequirements).toHaveProperty('tom')
})
it('should summarize documents', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom'])
expect(result.documents[0].contentSummary).toContain('1')
expect(result.documents[1].contentSummary).toContain('Keine TOM')
})
it('should handle empty state', () => {
const state = makeState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
expect(result.scopeLevel).toBe('L1')
expect(result.crossReferences.vvtCategories).toEqual([])
expect(result.crossReferences.tomControls).toEqual([])
})
})
describe('token budget estimation', () => {
it('projectForDraft should produce compact output', () => {
const state = makeDecisionState()
const result = projector.projectForDraft(state, 'vvt')
const json = JSON.stringify(result)
// Rough token estimation: ~4 chars per token
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(2000) // Budget is ~1500
})
it('projectForAsk should produce very compact output', () => {
const state = makeDecisionState()
const result = projector.projectForAsk(state)
const json = JSON.stringify(result)
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(1000) // Budget is ~600
})
it('projectForValidate should stay within budget', () => {
const state = makeDecisionState()
const result = projector.projectForValidate(state, ['vvt', 'tom', 'lf'])
const json = JSON.stringify(result)
const estimatedTokens = json.length / 4
expect(estimatedTokens).toBeLessThan(3000) // Budget is ~2000
})
})
})

View File

@@ -1,221 +0,0 @@
/**
* Constraint Enforcer - Hard Gate vor jedem Draft
*
* Stellt sicher, dass die Drafting Engine NIEMALS die deterministische
* Scope-Engine ueberschreibt. Prueft vor jedem Draft-Vorgang:
*
* 1. Ist der Dokumenttyp in requiredDocuments?
* 2. Passt die Draft-Tiefe zum Level?
* 3. Ist eine DSFA erforderlich (Hard Trigger)?
* 4. Werden Risiko-Flags beruecksichtigt?
*/
import type { ScopeDecision, ScopeDocumentType, ComplianceDepthLevel } from '../compliance-scope-types'
import { DOCUMENT_SCOPE_MATRIX, getDepthLevelNumeric } from '../compliance-scope-types'
import type { ConstraintCheckResult, DraftContext } from './types'
export class ConstraintEnforcer {
/**
* Prueft ob ein Draft fuer den gegebenen Dokumenttyp erlaubt ist.
* Dies ist ein HARD GATE - bei Violation wird der Draft blockiert.
*/
check(
documentType: ScopeDocumentType,
decision: ScopeDecision | null,
requestedDepthLevel?: ComplianceDepthLevel
): ConstraintCheckResult {
const violations: string[] = []
const adjustments: string[] = []
const checkedRules: string[] = []
// Wenn keine Decision vorhanden: Nur Basis-Drafts erlauben
if (!decision) {
checkedRules.push('RULE-NO-DECISION')
if (documentType !== 'vvt' && documentType !== 'tom' && documentType !== 'dsi') {
violations.push(
'Scope-Evaluierung fehlt. Bitte zuerst das Compliance-Profiling durchfuehren.'
)
} else {
adjustments.push(
'Ohne Scope-Evaluierung wird Level L1 (Basis) angenommen.'
)
}
return {
allowed: violations.length === 0,
violations,
adjustments,
checkedRules,
}
}
const level = decision.determinedLevel
const levelNumeric = getDepthLevelNumeric(level)
// -----------------------------------------------------------------------
// Rule 1: Dokumenttyp in requiredDocuments?
// -----------------------------------------------------------------------
checkedRules.push('RULE-DOC-REQUIRED')
const isRequired = decision.requiredDocuments.some(
d => d.documentType === documentType && d.required
)
const scopeReq = DOCUMENT_SCOPE_MATRIX[documentType]?.[level]
if (!isRequired && scopeReq && !scopeReq.required) {
// Nicht blockieren, aber warnen
adjustments.push(
`Dokument "${documentType}" ist auf Level ${level} nicht als Pflicht eingestuft. ` +
`Entwurf ist moeglich, aber optional.`
)
}
// -----------------------------------------------------------------------
// Rule 2: Draft-Tiefe passt zum Level?
// -----------------------------------------------------------------------
checkedRules.push('RULE-DEPTH-MATCH')
if (requestedDepthLevel) {
const requestedNumeric = getDepthLevelNumeric(requestedDepthLevel)
if (requestedNumeric > levelNumeric) {
violations.push(
`Angefragte Tiefe ${requestedDepthLevel} ueberschreitet das bestimmte Level ${level}. ` +
`Die Scope-Engine hat Level ${level} festgelegt. ` +
`Ein Draft mit Tiefe ${requestedDepthLevel} ist nicht erlaubt.`
)
} else if (requestedNumeric < levelNumeric) {
adjustments.push(
`Angefragte Tiefe ${requestedDepthLevel} liegt unter dem bestimmten Level ${level}. ` +
`Draft wird auf Level ${level} angehoben.`
)
}
}
// -----------------------------------------------------------------------
// Rule 3: DSFA-Enforcement
// -----------------------------------------------------------------------
checkedRules.push('RULE-DSFA-ENFORCEMENT')
if (documentType === 'dsfa') {
const dsfaRequired = decision.triggeredHardTriggers.some(
t => t.rule.dsfaRequired
)
if (!dsfaRequired && level !== 'L4') {
adjustments.push(
'DSFA ist laut Scope-Engine nicht verpflichtend. ' +
'Entwurf wird als freiwillige Massnahme gekennzeichnet.'
)
}
}
// Umgekehrt: Wenn DSFA verpflichtend und Typ != dsfa, ggf. hinweisen
if (documentType !== 'dsfa') {
const dsfaRequired = decision.triggeredHardTriggers.some(
t => t.rule.dsfaRequired
)
const dsfaInRequired = decision.requiredDocuments.some(
d => d.documentType === 'dsfa' && d.required
)
if (dsfaRequired && dsfaInRequired) {
// Nur ein Hinweis, kein Block
adjustments.push(
'Hinweis: Eine DSFA ist laut Scope-Engine verpflichtend. ' +
'Bitte sicherstellen, dass auch eine DSFA erstellt wird.'
)
}
}
// -----------------------------------------------------------------------
// Rule 4: Risiko-Flags beruecksichtigt?
// -----------------------------------------------------------------------
checkedRules.push('RULE-RISK-FLAGS')
const criticalRisks = decision.riskFlags.filter(
f => f.severity === 'CRITICAL' || f.severity === 'HIGH'
)
if (criticalRisks.length > 0) {
adjustments.push(
`${criticalRisks.length} kritische/hohe Risiko-Flags erkannt. ` +
`Draft muss diese adressieren: ${criticalRisks.map(r => r.title).join(', ')}`
)
}
// -----------------------------------------------------------------------
// Rule 5: Hard-Trigger Consistency
// -----------------------------------------------------------------------
checkedRules.push('RULE-HARD-TRIGGER-CONSISTENCY')
for (const trigger of decision.triggeredHardTriggers) {
const mandatoryDocs = trigger.rule.mandatoryDocuments
if (mandatoryDocs.includes(documentType)) {
// Gut - wir erstellen ein mandatory document
} else {
// Pruefen ob die mandatory documents des Triggers vorhanden sind
// (nur Hinweis, kein Block)
}
}
return {
allowed: violations.length === 0,
violations,
adjustments,
checkedRules,
}
}
/**
* Convenience: Prueft aus einem DraftContext heraus.
*/
checkFromContext(
documentType: ScopeDocumentType,
context: DraftContext
): ConstraintCheckResult {
// Reconstruct a minimal ScopeDecision from context
const pseudoDecision: ScopeDecision = {
id: 'projected',
determinedLevel: context.decisions.level,
scores: context.decisions.scores,
triggeredHardTriggers: context.decisions.hardTriggers.map(t => ({
rule: {
id: t.id,
label: t.label,
description: '',
conditionField: '',
conditionOperator: 'EQUALS' as const,
conditionValue: null,
minimumLevel: context.decisions.level,
mandatoryDocuments: [],
dsfaRequired: false,
legalReference: t.legalReference,
},
matchedValue: null,
explanation: '',
})),
requiredDocuments: context.decisions.requiredDocuments.map(d => ({
documentType: d.documentType,
label: d.documentType,
required: true,
depth: d.depth,
detailItems: d.detailItems,
estimatedEffort: '',
triggeredBy: [],
})),
riskFlags: context.constraints.riskFlags.map(f => ({
id: `rf-${f.title}`,
severity: f.severity as 'LOW' | 'MEDIUM' | 'HIGH' | 'CRITICAL',
title: f.title,
description: '',
recommendation: f.recommendation,
})),
gaps: [],
nextActions: [],
reasoning: [],
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}
return this.check(documentType, pseudoDecision)
}
}
/** Singleton-Instanz */
export const constraintEnforcer = new ConstraintEnforcer()

View File

@@ -1,241 +0,0 @@
/**
* Intent Classifier - Leichtgewichtiger Pattern-Matcher
*
* Erkennt den Agent-Modus anhand des Nutzer-Inputs ohne LLM-Call.
* Deutsche und englische Muster werden unterstuetzt.
*
* Confidence-Schwellen:
* - >0.8: Hohe Sicherheit, automatisch anwenden
* - 0.6-0.8: Mittel, Nutzer kann bestaetigen
* - <0.6: Fallback zu 'explain'
*/
import type { AgentMode, IntentClassification } from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
// ============================================================================
// Pattern Definitions
// ============================================================================
interface ModePattern {
mode: AgentMode
patterns: RegExp[]
/** Base-Confidence wenn ein Pattern matched */
baseConfidence: number
}
const MODE_PATTERNS: ModePattern[] = [
{
mode: 'draft',
baseConfidence: 0.85,
patterns: [
/\b(erstell|generier|entw[iu]rf|entwer[ft]|schreib|verfass|formulier|anlege)/i,
/\b(draft|create|generate|write|compose)\b/i,
/\b(neues?\s+(?:vvt|tom|dsfa|dokument|loeschkonzept|datenschutzerklaerung))\b/i,
/\b(vorlage|template)\s+(erstell|generier)/i,
/\bfuer\s+(?:uns|mich|unser)\b.*\b(erstell|schreib)/i,
],
},
{
mode: 'validate',
baseConfidence: 0.80,
patterns: [
/\b(pruef|validier|check|kontrollier|ueberpruef)\b/i,
/\b(korrekt|richtig|vollstaendig|konsistent|komplett)\b.*\?/i,
/\b(stimmt|passt)\b.*\b(das|mein|unser)\b/i,
/\b(validate|verify|check|review)\b/i,
/\b(fehler|luecken?|maengel)\b.*\b(find|such|zeig)\b/i,
/\bcross[\s-]?check\b/i,
/\b(vvt|tom|dsfa)\b.*\b(konsisten[tz]|widerspruch|uebereinstimm)/i,
],
},
{
mode: 'ask',
baseConfidence: 0.75,
patterns: [
/\bwas\s+fehlt\b/i,
/\b(luecken?|gaps?)\b.*\b(zeig|find|identifizier|analysier)/i,
/\b(unvollstaendig|unfertig|offen)\b/i,
/\bwelche\s+(dokumente?|informationen?|daten)\b.*\b(fehlen?|brauch|benoetig)/i,
/\b(naechste[rn]?\s+schritt|next\s+step|todo)\b/i,
/\bworan\s+(muss|soll)\b/i,
],
},
]
/** Dokumenttyp-Erkennung */
const DOCUMENT_TYPE_PATTERNS: Array<{
type: ScopeDocumentType
patterns: RegExp[]
}> = [
{
type: 'vvt',
patterns: [
/\bv{1,2}t\b/i,
/\bverarbeitungsverzeichnis\b/i,
/\bverarbeitungstaetigkeit/i,
/\bprocessing\s+activit/i,
/\bart\.?\s*30\b/i,
],
},
{
type: 'tom',
patterns: [
/\btom\b/i,
/\btechnisch.*organisatorisch.*massnahm/i,
/\bart\.?\s*32\b/i,
/\bsicherheitsmassnahm/i,
],
},
{
type: 'dsfa',
patterns: [
/\bdsfa\b/i,
/\bdatenschutz[\s-]?folgenabschaetzung\b/i,
/\bdpia\b/i,
/\bart\.?\s*35\b/i,
/\bimpact\s+assessment\b/i,
],
},
{
type: 'dsi',
patterns: [
/\bdatenschutzerklaerung\b/i,
/\bprivacy\s+policy\b/i,
/\bdsi\b/i,
/\bart\.?\s*13\b/i,
/\bart\.?\s*14\b/i,
],
},
{
type: 'lf',
patterns: [
/\bloeschfrist/i,
/\bloeschkonzept/i,
/\bretention/i,
/\baufbewahr/i,
],
},
{
type: 'av_vertrag',
patterns: [
/\bavv?\b/i,
/\bauftragsverarbeit/i,
/\bdata\s+processing\s+agreement/i,
/\bart\.?\s*28\b/i,
],
},
{
type: 'betroffenenrechte',
patterns: [
/\bbetroffenenrecht/i,
/\bdata\s+subject\s+right/i,
/\bart\.?\s*15\b/i,
/\bauskunft/i,
],
},
{
type: 'einwilligung',
patterns: [
/\beinwillig/i,
/\bconsent/i,
/\bcookie/i,
],
},
]
// ============================================================================
// Classifier
// ============================================================================
export class IntentClassifier {
/**
* Klassifiziert die Nutzerabsicht anhand des Inputs.
*
* @param input - Die Nutzer-Nachricht
* @returns IntentClassification mit Mode, Confidence, Patterns
*/
classify(input: string): IntentClassification {
const normalized = this.normalize(input)
let bestMatch: IntentClassification = {
mode: 'explain',
confidence: 0.3,
matchedPatterns: [],
}
for (const modePattern of MODE_PATTERNS) {
const matched: string[] = []
for (const pattern of modePattern.patterns) {
if (pattern.test(normalized)) {
matched.push(pattern.source)
}
}
if (matched.length > 0) {
// Mehr Matches = hoehere Confidence (bis zum Maximum)
const matchBonus = Math.min(matched.length - 1, 2) * 0.05
const confidence = Math.min(modePattern.baseConfidence + matchBonus, 0.99)
if (confidence > bestMatch.confidence) {
bestMatch = {
mode: modePattern.mode,
confidence,
matchedPatterns: matched,
}
}
}
}
// Dokumenttyp erkennen
const detectedDocType = this.detectDocumentType(normalized)
if (detectedDocType) {
bestMatch.detectedDocumentType = detectedDocType
// Dokumenttyp-Erkennung erhoeht Confidence leicht
bestMatch.confidence = Math.min(bestMatch.confidence + 0.05, 0.99)
}
// Fallback: Bei Confidence <0.6 immer 'explain'
if (bestMatch.confidence < 0.6) {
bestMatch.mode = 'explain'
}
return bestMatch
}
/**
* Erkennt den Dokumenttyp aus dem Input.
*/
detectDocumentType(input: string): ScopeDocumentType | undefined {
const normalized = this.normalize(input)
for (const docPattern of DOCUMENT_TYPE_PATTERNS) {
for (const pattern of docPattern.patterns) {
if (pattern.test(normalized)) {
return docPattern.type
}
}
}
return undefined
}
/**
* Normalisiert den Input fuer Pattern-Matching.
* Ersetzt Umlaute, entfernt Sonderzeichen.
*/
private normalize(input: string): string {
return input
.replace(/ä/g, 'ae')
.replace(/ö/g, 'oe')
.replace(/ü/g, 'ue')
.replace(/ß/g, 'ss')
.replace(/Ä/g, 'Ae')
.replace(/Ö/g, 'Oe')
.replace(/Ü/g, 'Ue')
}
}
/** Singleton-Instanz */
export const intentClassifier = new IntentClassifier()

View File

@@ -1,49 +0,0 @@
/**
* Gap Analysis Prompt - Lueckenanalyse und gezielte Fragen
*/
import type { GapContext } from '../types'
export interface GapAnalysisInput {
context: GapContext
instructions?: string
}
export function buildGapAnalysisPrompt(input: GapAnalysisInput): string {
const { context, instructions } = input
return `## Aufgabe: Compliance-Lueckenanalyse
### Identifizierte Luecken:
${context.gaps.length > 0
? context.gaps.map(g => `- [${g.severity}] ${g.title}: ${g.description}`).join('\n')
: '- Keine Luecken identifiziert'}
### Fehlende Pflichtdokumente:
${context.missingDocuments.length > 0
? context.missingDocuments.map(d => `- ${d.label} (Tiefe: ${d.depth}, Aufwand: ${d.estimatedEffort})`).join('\n')
: '- Alle Pflichtdokumente vorhanden'}
### Unbeantwortete Fragen:
${context.unansweredQuestions.length > 0
? context.unansweredQuestions.map(q => `- [${q.blockId}] ${q.question}`).join('\n')
: '- Alle Fragen beantwortet'}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Aufgabe:
Analysiere den Stand und stelle EINE gezielte Frage, die die wichtigste Luecke adressiert.
Priorisiere nach:
1. Fehlende Pflichtdokumente
2. Kritische Luecken (HIGH/CRITICAL severity)
3. Unbeantwortete Pflichtfragen
4. Mittlere Luecken
### Antwort-Format:
Antworte in dieser Struktur:
1. **Statusuebersicht**: Kurze Zusammenfassung des Compliance-Stands (2-3 Saetze)
2. **Wichtigste Luecke**: Was fehlt am dringendsten?
3. **Gezielte Frage**: Eine konkrete Frage an den Nutzer
4. **Warum wichtig**: Warum muss diese Luecke geschlossen werden?
5. **Empfohlener naechster Schritt**: Link/Verweis zum SDK-Modul`
}

View File

@@ -1,91 +0,0 @@
/**
* DSFA Draft Prompt - Datenschutz-Folgenabschaetzung (Art. 35 DSGVO)
*/
import type { DraftContext } from '../types'
export interface DSFADraftInput {
context: DraftContext
processingDescription?: string
instructions?: string
}
export function buildDSFADraftPrompt(input: DSFADraftInput): string {
const { context, processingDescription, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
const hardTriggers = context.decisions.hardTriggers
return `## Aufgabe: DSFA entwerfen (Art. 35 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Hard Triggers (Gruende fuer DSFA-Pflicht):
${hardTriggers.length > 0
? hardTriggers.map(t => `- ${t.id}: ${t.label} (${t.legalReference})`).join('\n')
: '- Keine Hard Triggers (DSFA auf Wunsch)'}
### Erforderliche Inhalte:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${processingDescription ? `### Beschreibung der Verarbeitung: ${processingDescription}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "beschreibung",
"title": "Systematische Beschreibung der Verarbeitung",
"content": "...",
"schemaField": "processingDescription"
},
{
"id": "notwendigkeit",
"title": "Notwendigkeit und Verhaeltnismaessigkeit",
"content": "...",
"schemaField": "necessityAssessment"
},
{
"id": "risikobewertung",
"title": "Bewertung der Risiken fuer die Rechte und Freiheiten",
"content": "...",
"schemaField": "riskAssessment"
},
{
"id": "massnahmen",
"title": "Massnahmen zur Eindaemmung der Risiken",
"content": "...",
"schemaField": "mitigationMeasures"
},
{
"id": "stellungnahme_dsb",
"title": "Stellungnahme des Datenschutzbeauftragten",
"content": "...",
"schemaField": "dpoOpinion"
},
{
"id": "standpunkt_betroffene",
"title": "Standpunkt der betroffenen Personen",
"content": "...",
"schemaField": "dataSubjectView"
},
{
"id": "ergebnis",
"title": "Ergebnis und Empfehlung",
"content": "...",
"schemaField": "conclusion"
}
]
}
Halte die Tiefe exakt auf Level ${level}.
Nutze WP248-Kriterien als Leitfaden fuer die Risikobewertung.`
}

View File

@@ -1,78 +0,0 @@
/**
* Loeschfristen Draft Prompt - Loeschkonzept
*/
import type { DraftContext } from '../types'
export interface LoeschfristenDraftInput {
context: DraftContext
instructions?: string
}
export function buildLoeschfristenDraftPrompt(input: LoeschfristenDraftInput): string {
const { context, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: Loeschkonzept / Loeschfristen entwerfen
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${context.existingDocumentData ? `### Bestehende Loeschfristen: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "grundsaetze",
"title": "Grundsaetze der Datenlöschung",
"content": "...",
"schemaField": "principles"
},
{
"id": "kategorien",
"title": "Datenkategorien und Loeschfristen",
"content": "Tabellarische Uebersicht...",
"schemaField": "retentionSchedule"
},
{
"id": "gesetzliche_fristen",
"title": "Gesetzliche Aufbewahrungsfristen",
"content": "HGB, AO, weitere...",
"schemaField": "legalRetention"
},
{
"id": "loeschprozess",
"title": "Technischer Loeschprozess",
"content": "...",
"schemaField": "deletionProcess"
},
{
"id": "verantwortlichkeiten",
"title": "Verantwortlichkeiten",
"content": "...",
"schemaField": "responsibilities"
},
{
"id": "ausnahmen",
"title": "Ausnahmen und Sonderfaelle",
"content": "...",
"schemaField": "exceptions"
}
]
}
Halte die Tiefe exakt auf Level ${level}.
Beruecksichtige branchenspezifische Aufbewahrungsfristen fuer ${context.companyProfile.industry}.`
}

View File

@@ -1,102 +0,0 @@
/**
* Privacy Policy Draft Prompt - Datenschutzerklaerung (Art. 13/14 DSGVO)
*/
import type { DraftContext } from '../types'
export interface PrivacyPolicyDraftInput {
context: DraftContext
websiteUrl?: string
instructions?: string
}
export function buildPrivacyPolicyDraftPrompt(input: PrivacyPolicyDraftInput): string {
const { context, websiteUrl, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: Datenschutzerklaerung entwerfen (Art. 13/14 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : ''}
${websiteUrl ? `- Website: ${websiteUrl}` : ''}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "verantwortlicher",
"title": "Verantwortlicher",
"content": "...",
"schemaField": "controller"
},
{
"id": "dsb",
"title": "Datenschutzbeauftragter",
"content": "...",
"schemaField": "dpo"
},
{
"id": "verarbeitungen",
"title": "Verarbeitungstaetigkeiten und Zwecke",
"content": "...",
"schemaField": "processingPurposes"
},
{
"id": "rechtsgrundlagen",
"title": "Rechtsgrundlagen der Verarbeitung",
"content": "...",
"schemaField": "legalBases"
},
{
"id": "empfaenger",
"title": "Empfaenger und Datenweitergabe",
"content": "...",
"schemaField": "recipients"
},
{
"id": "drittland",
"title": "Uebermittlung in Drittlaender",
"content": "...",
"schemaField": "thirdCountryTransfers"
},
{
"id": "speicherdauer",
"title": "Speicherdauer",
"content": "...",
"schemaField": "retentionPeriods"
},
{
"id": "betroffenenrechte",
"title": "Ihre Rechte als betroffene Person",
"content": "...",
"schemaField": "dataSubjectRights"
},
{
"id": "cookies",
"title": "Cookies und Tracking",
"content": "...",
"schemaField": "cookies"
},
{
"id": "aenderungen",
"title": "Aenderungen dieser Datenschutzerklaerung",
"content": "...",
"schemaField": "changes"
}
]
}
Halte die Tiefe exakt auf Level ${level}.`
}

View File

@@ -1,99 +0,0 @@
/**
* TOM Draft Prompt - Technische und Organisatorische Massnahmen (Art. 32 DSGVO)
*/
import type { DraftContext } from '../types'
export interface TOMDraftInput {
context: DraftContext
focusArea?: string
instructions?: string
}
export function buildTOMDraftPrompt(input: TOMDraftInput): string {
const { context, focusArea, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: TOM-Dokument entwerfen (Art. 32 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte fuer Level ${level}:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
### Constraints
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}`).join('\n')}` : ''}
${focusArea ? `### Fokusbereich: ${focusArea}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
${context.existingDocumentData ? `### Bestehende TOM: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "zutrittskontrolle",
"title": "Zutrittskontrolle",
"content": "Massnahmen die unbefugten Zutritt zu Datenverarbeitungsanlagen verhindern...",
"schemaField": "accessControl"
},
{
"id": "zugangskontrolle",
"title": "Zugangskontrolle",
"content": "Massnahmen gegen unbefugte Systemnutzung...",
"schemaField": "systemAccessControl"
},
{
"id": "zugriffskontrolle",
"title": "Zugriffskontrolle",
"content": "Massnahmen zur Sicherstellung berechtigter Datenzugriffe...",
"schemaField": "dataAccessControl"
},
{
"id": "weitergabekontrolle",
"title": "Weitergabekontrolle / Uebertragungssicherheit",
"content": "Massnahmen bei Datenuebertragung und -transport...",
"schemaField": "transferControl"
},
{
"id": "eingabekontrolle",
"title": "Eingabekontrolle",
"content": "Nachvollziehbarkeit von Dateneingaben...",
"schemaField": "inputControl"
},
{
"id": "auftragskontrolle",
"title": "Auftragskontrolle",
"content": "Massnahmen zur weisungsgemaessen Auftragsverarbeitung...",
"schemaField": "orderControl"
},
{
"id": "verfuegbarkeitskontrolle",
"title": "Verfuegbarkeitskontrolle",
"content": "Schutz gegen Datenverlust...",
"schemaField": "availabilityControl"
},
{
"id": "trennungsgebot",
"title": "Trennungsgebot",
"content": "Getrennte Verarbeitung fuer verschiedene Zwecke...",
"schemaField": "separationControl"
}
]
}
Fuelle fehlende Informationen mit [PLATZHALTER: ...].
Halte die Tiefe exakt auf Level ${level}.`
}

View File

@@ -1,109 +0,0 @@
/**
* VVT Draft Prompt - Verarbeitungsverzeichnis (Art. 30 DSGVO)
*/
import type { DraftContext } from '../types'
export interface VVTDraftInput {
context: DraftContext
activityName?: string
activityPurpose?: string
instructions?: string
}
export function buildVVTDraftPrompt(input: VVTDraftInput): string {
const { context, activityName, activityPurpose, instructions } = input
const level = context.decisions.level
const depthItems = context.constraints.depthRequirements.detailItems
return `## Aufgabe: VVT-Eintrag entwerfen (Art. 30 DSGVO)
### Unternehmensprofil
- Name: ${context.companyProfile.name}
- Branche: ${context.companyProfile.industry}
- Mitarbeiter: ${context.companyProfile.employeeCount}
- Geschaeftsmodell: ${context.companyProfile.businessModel}
${context.companyProfile.dataProtectionOfficer ? `- DSB: ${context.companyProfile.dataProtectionOfficer.name} (${context.companyProfile.dataProtectionOfficer.email})` : '- DSB: Nicht benannt'}
### Compliance-Level: ${level}
Tiefe: ${context.constraints.depthRequirements.depth}
### Erforderliche Inhalte fuer Level ${level}:
${depthItems.map((item, i) => `${i + 1}. ${item}`).join('\n')}
### Constraints
${context.constraints.boundaries.map(b => `- ${b}`).join('\n')}
${context.constraints.riskFlags.length > 0 ? `### Risiko-Flags
${context.constraints.riskFlags.map(f => `- [${f.severity}] ${f.title}: ${f.recommendation}`).join('\n')}` : ''}
${activityName ? `### Gewuenschte Verarbeitungstaetigkeit: ${activityName}` : ''}
${activityPurpose ? `### Zweck: ${activityPurpose}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
${context.existingDocumentData ? `### Bestehende VVT-Eintraege: ${JSON.stringify(context.existingDocumentData).slice(0, 500)}` : ''}
### Antwort-Format
Antworte als JSON:
{
"sections": [
{
"id": "bezeichnung",
"title": "Bezeichnung der Verarbeitungstaetigkeit",
"content": "...",
"schemaField": "name"
},
{
"id": "verantwortlicher",
"title": "Verantwortlicher",
"content": "...",
"schemaField": "controller"
},
{
"id": "zweck",
"title": "Zweck der Verarbeitung",
"content": "...",
"schemaField": "purpose"
},
{
"id": "rechtsgrundlage",
"title": "Rechtsgrundlage",
"content": "...",
"schemaField": "legalBasis"
},
{
"id": "betroffene",
"title": "Kategorien betroffener Personen",
"content": "...",
"schemaField": "dataSubjects"
},
{
"id": "datenkategorien",
"title": "Kategorien personenbezogener Daten",
"content": "...",
"schemaField": "dataCategories"
},
{
"id": "empfaenger",
"title": "Empfaenger",
"content": "...",
"schemaField": "recipients"
},
{
"id": "speicherdauer",
"title": "Speicherdauer / Loeschfristen",
"content": "...",
"schemaField": "retentionPeriod"
},
{
"id": "tom_referenz",
"title": "TOM-Referenz",
"content": "...",
"schemaField": "tomReference"
}
]
}
Fuelle fehlende Informationen mit [PLATZHALTER: Beschreibung was hier eingetragen werden muss].
Halte die Tiefe exakt auf Level ${level} (${context.constraints.depthRequirements.depth}).`
}

View File

@@ -1,11 +0,0 @@
/**
* Drafting Engine Prompts - Re-Exports
*/
export { buildVVTDraftPrompt, type VVTDraftInput } from './draft-vvt'
export { buildTOMDraftPrompt, type TOMDraftInput } from './draft-tom'
export { buildDSFADraftPrompt, type DSFADraftInput } from './draft-dsfa'
export { buildPrivacyPolicyDraftPrompt, type PrivacyPolicyDraftInput } from './draft-privacy-policy'
export { buildLoeschfristenDraftPrompt, type LoeschfristenDraftInput } from './draft-loeschfristen'
export { buildCrossCheckPrompt, type CrossCheckInput } from './validate-cross-check'
export { buildGapAnalysisPrompt, type GapAnalysisInput } from './ask-gap-analysis'

View File

@@ -1,66 +0,0 @@
/**
* Cross-Document Validation Prompt
*/
import type { ValidationContext } from '../types'
export interface CrossCheckInput {
context: ValidationContext
focusDocuments?: string[]
instructions?: string
}
export function buildCrossCheckPrompt(input: CrossCheckInput): string {
const { context, focusDocuments, instructions } = input
return `## Aufgabe: Cross-Dokument-Konsistenzpruefung
### Scope-Level: ${context.scopeLevel}
### Vorhandene Dokumente:
${context.documents.map(d => `- ${d.type}: ${d.contentSummary}`).join('\n')}
### Cross-Referenzen:
- VVT-Kategorien: ${context.crossReferences.vvtCategories.join(', ') || 'Keine'}
- DSFA-Risiken: ${context.crossReferences.dsfaRisks.join(', ') || 'Keine'}
- TOM-Controls: ${context.crossReferences.tomControls.join(', ') || 'Keine'}
- Loeschfristen-Kategorien: ${context.crossReferences.retentionCategories.join(', ') || 'Keine'}
### Tiefenpruefung pro Dokument:
${context.documents.map(d => {
const req = context.depthRequirements[d.type]
return req ? `- ${d.type}: Erforderlich=${req.required}, Tiefe=${req.depth}` : `- ${d.type}: Keine Requirements`
}).join('\n')}
${focusDocuments ? `### Fokus auf: ${focusDocuments.join(', ')}` : ''}
${instructions ? `### Zusaetzliche Anweisungen: ${instructions}` : ''}
### Pruefkriterien:
1. Jede VVT-Taetigkeit muss einen TOM-Verweis haben
2. Jede VVT-Kategorie muss eine Loeschfrist haben
3. Bei DSFA-pflichtigen Verarbeitungen muss eine DSFA existieren
4. TOM-Massnahmen muessen zum Risikoprofil passen
5. Loeschfristen duerfen gesetzliche Minima nicht unterschreiten
6. Dokument-Tiefe muss Level ${context.scopeLevel} entsprechen
### Antwort-Format
Antworte als JSON:
{
"passed": true/false,
"errors": [
{
"id": "ERR-001",
"severity": "error",
"category": "scope_violation|inconsistency|missing_content|depth_mismatch|cross_reference",
"title": "...",
"description": "...",
"documentType": "vvt|tom|dsfa|...",
"crossReferenceType": "...",
"legalReference": "Art. ... DSGVO",
"suggestion": "..."
}
],
"warnings": [...],
"suggestions": [...]
}`
}

View File

@@ -1,337 +0,0 @@
/**
* State Projector - Token-budgetierte Projektion des SDK-State
*
* Extrahiert aus dem vollen SDKState (der ~50k Tokens betragen kann) nur die
* relevanten Slices fuer den jeweiligen Agent-Modus.
*
* Token-Budgets:
* - Draft: ~1500 Tokens
* - Ask: ~600 Tokens
* - Validate: ~2000 Tokens
*/
import type { SDKState, CompanyProfile } from '../types'
import type {
ComplianceScopeState,
ScopeDecision,
ScopeDocumentType,
ScopeGap,
RequiredDocument,
RiskFlag,
DOCUMENT_SCOPE_MATRIX,
DocumentDepthRequirement,
} from '../compliance-scope-types'
import { DOCUMENT_SCOPE_MATRIX as DOC_MATRIX, DOCUMENT_TYPE_LABELS } from '../compliance-scope-types'
import type {
DraftContext,
GapContext,
ValidationContext,
} from './types'
// ============================================================================
// State Projector
// ============================================================================
export class StateProjector {
/**
* Projiziert den SDKState fuer Draft-Operationen.
* Fokus: Scope-Decision, Company-Profile, Dokument-spezifische Constraints.
*
* ~1500 Tokens
*/
projectForDraft(
state: SDKState,
documentType: ScopeDocumentType
): DraftContext {
const decision = state.complianceScope?.decision ?? null
const level = decision?.determinedLevel ?? 'L1'
const depthReq = DOC_MATRIX[documentType]?.[level] ?? {
required: false,
depth: 'Basis',
detailItems: [],
estimatedEffort: 'N/A',
}
return {
decisions: {
level,
scores: decision?.scores ?? {
risk_score: 0,
complexity_score: 0,
assurance_need: 0,
composite_score: 0,
},
hardTriggers: (decision?.triggeredHardTriggers ?? []).map(t => ({
id: t.rule.id,
label: t.rule.label,
legalReference: t.rule.legalReference,
})),
requiredDocuments: (decision?.requiredDocuments ?? [])
.filter(d => d.required)
.map(d => ({
documentType: d.documentType,
depth: d.depth,
detailItems: d.detailItems,
})),
},
companyProfile: this.projectCompanyProfile(state.companyProfile),
constraints: {
depthRequirements: depthReq,
riskFlags: (decision?.riskFlags ?? []).map(f => ({
severity: f.severity,
title: f.title,
recommendation: f.recommendation,
})),
boundaries: this.deriveBoundaries(decision, documentType),
},
existingDocumentData: this.extractExistingDocumentData(state, documentType),
}
}
/**
* Projiziert den SDKState fuer Ask-Operationen.
* Fokus: Luecken, unbeantwortete Fragen, fehlende Dokumente.
*
* ~600 Tokens
*/
projectForAsk(state: SDKState): GapContext {
const decision = state.complianceScope?.decision ?? null
// Fehlende Pflichtdokumente ermitteln
const requiredDocs = (decision?.requiredDocuments ?? []).filter(d => d.required)
const existingDocTypes = this.getExistingDocumentTypes(state)
const missingDocuments = requiredDocs
.filter(d => !existingDocTypes.includes(d.documentType))
.map(d => ({
documentType: d.documentType,
label: DOCUMENT_TYPE_LABELS[d.documentType] ?? d.documentType,
depth: d.depth,
estimatedEffort: d.estimatedEffort,
}))
// Gaps aus der Scope-Decision
const gaps = (decision?.gaps ?? []).map(g => ({
id: g.id,
severity: g.severity,
title: g.title,
description: g.description,
relatedDocuments: g.relatedDocuments,
}))
// Unbeantwortete Fragen (aus dem Scope-Profiling)
const answers = state.complianceScope?.answers ?? []
const answeredIds = new Set(answers.map(a => a.questionId))
return {
unansweredQuestions: [], // Populated dynamically from question catalog
gaps,
missingDocuments,
}
}
/**
* Projiziert den SDKState fuer Validate-Operationen.
* Fokus: Cross-Dokument-Konsistenz, Scope-Compliance.
*
* ~2000 Tokens
*/
projectForValidate(
state: SDKState,
documentTypes: ScopeDocumentType[]
): ValidationContext {
const decision = state.complianceScope?.decision ?? null
const level = decision?.determinedLevel ?? 'L1'
// Dokument-Zusammenfassungen sammeln
const documents = documentTypes.map(type => ({
type,
contentSummary: this.summarizeDocument(state, type),
structuredData: this.extractExistingDocumentData(state, type),
}))
// Cross-Referenzen extrahieren
const crossReferences = {
vvtCategories: (state.vvt ?? []).map(v =>
typeof v === 'object' && v !== null && 'name' in v ? String((v as Record<string, unknown>).name) : ''
).filter(Boolean),
dsfaRisks: state.dsfa
? ['DSFA vorhanden']
: [],
tomControls: (state.toms ?? []).map(t =>
typeof t === 'object' && t !== null && 'name' in t ? String((t as Record<string, unknown>).name) : ''
).filter(Boolean),
retentionCategories: (state.retentionPolicies ?? []).map(p =>
typeof p === 'object' && p !== null && 'name' in p ? String((p as Record<string, unknown>).name) : ''
).filter(Boolean),
}
// Depth-Requirements fuer alle angefragten Typen
const depthRequirements: Record<string, DocumentDepthRequirement> = {}
for (const type of documentTypes) {
depthRequirements[type] = DOC_MATRIX[type]?.[level] ?? {
required: false,
depth: 'Basis',
detailItems: [],
estimatedEffort: 'N/A',
}
}
return {
documents,
crossReferences,
scopeLevel: level,
depthRequirements: depthRequirements as Record<ScopeDocumentType, DocumentDepthRequirement>,
}
}
// ==========================================================================
// Private Helpers
// ==========================================================================
private projectCompanyProfile(
profile: CompanyProfile | null
): DraftContext['companyProfile'] {
if (!profile) {
return {
name: 'Unbekannt',
industry: 'Unbekannt',
employeeCount: 0,
businessModel: 'Unbekannt',
isPublicSector: false,
}
}
return {
name: profile.companyName ?? profile.name ?? 'Unbekannt',
industry: profile.industry ?? 'Unbekannt',
employeeCount: typeof profile.employeeCount === 'number'
? profile.employeeCount
: parseInt(String(profile.employeeCount ?? '0'), 10) || 0,
businessModel: profile.businessModel ?? 'Unbekannt',
isPublicSector: profile.isPublicSector ?? false,
...(profile.dataProtectionOfficer ? {
dataProtectionOfficer: {
name: profile.dataProtectionOfficer.name ?? '',
email: profile.dataProtectionOfficer.email ?? '',
},
} : {}),
}
}
/**
* Leitet Grenzen (Boundaries) ab, die der Agent nicht ueberschreiten darf.
*/
private deriveBoundaries(
decision: ScopeDecision | null,
documentType: ScopeDocumentType
): string[] {
const boundaries: string[] = []
const level = decision?.determinedLevel ?? 'L1'
// Grundregel: Scope-Engine ist autoritativ
boundaries.push(
`Maximale Dokumenttiefe: ${level} (${DOC_MATRIX[documentType]?.[level]?.depth ?? 'Basis'})`
)
// DSFA-Boundary
if (documentType === 'dsfa') {
const dsfaRequired = decision?.triggeredHardTriggers?.some(
t => t.rule.dsfaRequired
) ?? false
if (!dsfaRequired && level !== 'L4') {
boundaries.push('DSFA ist laut Scope-Engine NICHT erforderlich. Nur auf expliziten Wunsch erstellen.')
}
}
// Dokument nicht in requiredDocuments?
const isRequired = decision?.requiredDocuments?.some(
d => d.documentType === documentType && d.required
) ?? false
if (!isRequired) {
boundaries.push(
`Dokument "${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}" ist auf Level ${level} nicht als Pflicht eingestuft.`
)
}
return boundaries
}
/**
* Extrahiert bereits vorhandene Dokumentdaten aus dem SDK-State.
*/
private extractExistingDocumentData(
state: SDKState,
documentType: ScopeDocumentType
): Record<string, unknown> | undefined {
switch (documentType) {
case 'vvt':
return state.vvt?.length ? { entries: state.vvt.slice(0, 5), totalCount: state.vvt.length } : undefined
case 'tom':
return state.toms?.length ? { entries: state.toms.slice(0, 5), totalCount: state.toms.length } : undefined
case 'lf':
return state.retentionPolicies?.length
? { entries: state.retentionPolicies.slice(0, 5), totalCount: state.retentionPolicies.length }
: undefined
case 'dsfa':
return state.dsfa ? { assessment: state.dsfa } : undefined
case 'dsi':
return state.documents?.length
? { entries: state.documents.slice(0, 3), totalCount: state.documents.length }
: undefined
case 'einwilligung':
return state.consents?.length
? { entries: state.consents.slice(0, 5), totalCount: state.consents.length }
: undefined
default:
return undefined
}
}
/**
* Ermittelt welche Dokumenttypen bereits im State vorhanden sind.
*/
private getExistingDocumentTypes(state: SDKState): ScopeDocumentType[] {
const types: ScopeDocumentType[] = []
if (state.vvt?.length) types.push('vvt')
if (state.toms?.length) types.push('tom')
if (state.retentionPolicies?.length) types.push('lf')
if (state.dsfa) types.push('dsfa')
if (state.documents?.length) types.push('dsi')
if (state.consents?.length) types.push('einwilligung')
if (state.cookieBanner) types.push('einwilligung')
return types
}
/**
* Erstellt eine kurze Zusammenfassung eines Dokuments fuer Validierung.
*/
private summarizeDocument(
state: SDKState,
documentType: ScopeDocumentType
): string {
switch (documentType) {
case 'vvt':
return state.vvt?.length
? `${state.vvt.length} Verarbeitungstaetigkeiten erfasst`
: 'Keine VVT-Eintraege vorhanden'
case 'tom':
return state.toms?.length
? `${state.toms.length} TOM-Massnahmen definiert`
: 'Keine TOM-Massnahmen vorhanden'
case 'lf':
return state.retentionPolicies?.length
? `${state.retentionPolicies.length} Loeschfristen definiert`
: 'Keine Loeschfristen vorhanden'
case 'dsfa':
return state.dsfa
? 'DSFA vorhanden'
: 'Keine DSFA vorhanden'
default:
return `Dokument ${DOCUMENT_TYPE_LABELS[documentType] ?? documentType}`
}
}
}
/** Singleton-Instanz */
export const stateProjector = new StateProjector()

View File

@@ -1,279 +0,0 @@
/**
* Drafting Engine - Type Definitions
*
* Typen fuer die 4 Agent-Rollen: Explain, Ask, Draft, Validate
* Die Drafting Engine erweitert den Compliance Advisor um aktive Dokumententwurfs-
* und Validierungsfaehigkeiten, stets unter Beachtung der deterministischen Scope-Engine.
*/
import type {
ComplianceDepthLevel,
ComplianceScores,
ScopeDecision,
ScopeDocumentType,
ScopeGap,
RequiredDocument,
RiskFlag,
DocumentDepthRequirement,
ScopeProfilingQuestion,
} from '../compliance-scope-types'
import type { CompanyProfile } from '../types'
// ============================================================================
// Agent Mode
// ============================================================================
/** Die 4 Agent-Rollen */
export type AgentMode = 'explain' | 'ask' | 'draft' | 'validate'
/** Confidence-Score fuer Intent-Erkennung */
export interface IntentClassification {
mode: AgentMode
confidence: number
matchedPatterns: string[]
/** Falls Draft oder Validate: erkannter Dokumenttyp */
detectedDocumentType?: ScopeDocumentType
}
// ============================================================================
// Draft Context (fuer Draft-Mode)
// ============================================================================
/** Projizierter State fuer Draft-Operationen (~1500 Tokens) */
export interface DraftContext {
/** Scope-Entscheidung (Level, Scores, Hard Triggers) */
decisions: {
level: ComplianceDepthLevel
scores: ComplianceScores
hardTriggers: Array<{ id: string; label: string; legalReference: string }>
requiredDocuments: Array<{
documentType: ScopeDocumentType
depth: string
detailItems: string[]
}>
}
/** Firmenprofil-Auszug */
companyProfile: {
name: string
industry: string
employeeCount: number
businessModel: string
isPublicSector: boolean
dataProtectionOfficer?: { name: string; email: string }
}
/** Constraints aus der Scope-Engine */
constraints: {
depthRequirements: DocumentDepthRequirement
riskFlags: Array<{ severity: string; title: string; recommendation: string }>
boundaries: string[]
}
/** Optional: bestehende Dokumentdaten aus dem SDK-State */
existingDocumentData?: Record<string, unknown>
}
// ============================================================================
// Gap Context (fuer Ask-Mode)
// ============================================================================
/** Projizierter State fuer Ask-Operationen (~600 Tokens) */
export interface GapContext {
/** Noch unbeantwortete Fragen aus dem Scope-Profiling */
unansweredQuestions: Array<{
id: string
question: string
type: string
blockId: string
}>
/** Identifizierte Luecken */
gaps: Array<{
id: string
severity: string
title: string
description: string
relatedDocuments: ScopeDocumentType[]
}>
/** Fehlende Pflichtdokumente */
missingDocuments: Array<{
documentType: ScopeDocumentType
label: string
depth: string
estimatedEffort: string
}>
}
// ============================================================================
// Validation Context (fuer Validate-Mode)
// ============================================================================
/** Projizierter State fuer Validate-Operationen (~2000 Tokens) */
export interface ValidationContext {
/** Zu validierende Dokumente */
documents: Array<{
type: ScopeDocumentType
/** Zusammenfassung/Auszug des Inhalts */
contentSummary: string
/** Strukturierte Daten falls vorhanden */
structuredData?: Record<string, unknown>
}>
/** Cross-Referenzen zwischen Dokumenten */
crossReferences: {
/** VVT Kategorien (Verarbeitungstaetigkeiten) */
vvtCategories: string[]
/** DSFA Risiken */
dsfaRisks: string[]
/** TOM Controls */
tomControls: string[]
/** Loeschfristen-Kategorien */
retentionCategories: string[]
}
/** Scope-Level fuer Tiefenpruefung */
scopeLevel: ComplianceDepthLevel
/** Relevante Depth-Requirements */
depthRequirements: Record<ScopeDocumentType, DocumentDepthRequirement>
}
// ============================================================================
// Validation Result
// ============================================================================
export type ValidationSeverity = 'error' | 'warning' | 'suggestion'
export interface ValidationFinding {
id: string
severity: ValidationSeverity
category: 'scope_violation' | 'inconsistency' | 'missing_content' | 'depth_mismatch' | 'cross_reference'
title: string
description: string
/** Betroffenes Dokument */
documentType: ScopeDocumentType
/** Optional: Referenz zu anderem Dokument */
crossReferenceType?: ScopeDocumentType
/** Rechtsgrundlage falls relevant */
legalReference?: string
/** Vorschlag zur Behebung */
suggestion?: string
/** Kann automatisch uebernommen werden */
autoFixable?: boolean
}
export interface ValidationResult {
passed: boolean
timestamp: string
scopeLevel: ComplianceDepthLevel
errors: ValidationFinding[]
warnings: ValidationFinding[]
suggestions: ValidationFinding[]
}
// ============================================================================
// Draft Session
// ============================================================================
export interface DraftRevision {
id: string
content: string
sections: DraftSection[]
createdAt: string
instruction?: string
}
export interface DraftSection {
id: string
title: string
content: string
/** Mapping zum Dokumentschema (z.B. VVT-Feld) */
schemaField?: string
}
export interface DraftSession {
id: string
mode: AgentMode
documentType: ScopeDocumentType
/** Aktueller Draft-Inhalt */
currentDraft: DraftRevision | null
/** Alle bisherigen Revisionen */
revisions: DraftRevision[]
/** Validierungszustand */
validationState: ValidationResult | null
/** Constraint-Check Ergebnis */
constraintCheck: ConstraintCheckResult | null
createdAt: string
updatedAt: string
}
// ============================================================================
// Constraint Check (Hard Gate)
// ============================================================================
export interface ConstraintCheckResult {
/** Darf der Draft erstellt werden? */
allowed: boolean
/** Verletzungen die den Draft blockieren */
violations: string[]
/** Anpassungen die vorgenommen werden sollten */
adjustments: string[]
/** Gepruefte Regeln */
checkedRules: string[]
}
// ============================================================================
// Chat / API Types
// ============================================================================
export interface DraftingChatMessage {
role: 'user' | 'assistant' | 'system'
content: string
/** Metadata fuer Agent-Nachrichten */
metadata?: {
mode: AgentMode
documentType?: ScopeDocumentType
hasDraft?: boolean
hasValidation?: boolean
}
}
export interface DraftingChatRequest {
message: string
history: DraftingChatMessage[]
sdkStateProjection: DraftContext | GapContext | ValidationContext
mode?: AgentMode
documentType?: ScopeDocumentType
}
export interface DraftRequest {
documentType: ScopeDocumentType
draftContext: DraftContext
instructions?: string
existingDraft?: DraftRevision
}
export interface DraftResponse {
draft: DraftRevision
constraintCheck: ConstraintCheckResult
tokensUsed: number
}
export interface ValidateRequest {
documentType: ScopeDocumentType
draftContent: string
validationContext: ValidationContext
}
// ============================================================================
// Feature Flag
// ============================================================================
export interface DraftingEngineConfig {
/** Feature-Flag: Drafting Engine aktiviert */
enableDraftingEngine: boolean
/** Verfuegbare Modi (fuer schrittweises Rollout) */
enabledModes: AgentMode[]
/** Max Token-Budget fuer State-Projection */
maxProjectionTokens: number
}
export const DEFAULT_DRAFTING_ENGINE_CONFIG: DraftingEngineConfig = {
enableDraftingEngine: false,
enabledModes: ['explain'],
maxProjectionTokens: 4096,
}

View File

@@ -1,343 +0,0 @@
'use client'
/**
* useDraftingEngine - React Hook fuer die Drafting Engine
*
* Managed: currentMode, activeDocumentType, draftSessions, validationState
* Handled: State-Projection, API-Calls, Streaming
* Provides: sendMessage(), requestDraft(), validateDraft(), acceptDraft()
*/
import { useState, useCallback, useRef } from 'react'
import { useSDK } from '../context'
import { stateProjector } from './state-projector'
import { intentClassifier } from './intent-classifier'
import { constraintEnforcer } from './constraint-enforcer'
import type {
AgentMode,
DraftSession,
DraftRevision,
DraftingChatMessage,
ValidationResult,
ConstraintCheckResult,
DraftContext,
GapContext,
ValidationContext,
} from './types'
import type { ScopeDocumentType } from '../compliance-scope-types'
export interface DraftingEngineState {
currentMode: AgentMode
activeDocumentType: ScopeDocumentType | null
messages: DraftingChatMessage[]
isTyping: boolean
currentDraft: DraftRevision | null
validationResult: ValidationResult | null
constraintCheck: ConstraintCheckResult | null
error: string | null
}
export interface DraftingEngineActions {
setMode: (mode: AgentMode) => void
setDocumentType: (type: ScopeDocumentType) => void
sendMessage: (content: string) => Promise<void>
requestDraft: (instructions?: string) => Promise<void>
validateDraft: () => Promise<void>
acceptDraft: () => void
stopGeneration: () => void
clearMessages: () => void
}
export function useDraftingEngine(): DraftingEngineState & DraftingEngineActions {
const { state, dispatch } = useSDK()
const abortControllerRef = useRef<AbortController | null>(null)
const [currentMode, setCurrentMode] = useState<AgentMode>('explain')
const [activeDocumentType, setActiveDocumentType] = useState<ScopeDocumentType | null>(null)
const [messages, setMessages] = useState<DraftingChatMessage[]>([])
const [isTyping, setIsTyping] = useState(false)
const [currentDraft, setCurrentDraft] = useState<DraftRevision | null>(null)
const [validationResult, setValidationResult] = useState<ValidationResult | null>(null)
const [constraintCheck, setConstraintCheck] = useState<ConstraintCheckResult | null>(null)
const [error, setError] = useState<string | null>(null)
// Get state projection based on mode
const getProjection = useCallback(() => {
switch (currentMode) {
case 'draft':
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
case 'ask':
return stateProjector.projectForAsk(state)
case 'validate':
return activeDocumentType
? stateProjector.projectForValidate(state, [activeDocumentType])
: stateProjector.projectForValidate(state, ['vvt', 'tom', 'lf'])
default:
return activeDocumentType
? stateProjector.projectForDraft(state, activeDocumentType)
: null
}
}, [state, currentMode, activeDocumentType])
const setMode = useCallback((mode: AgentMode) => {
setCurrentMode(mode)
}, [])
const setDocumentType = useCallback((type: ScopeDocumentType) => {
setActiveDocumentType(type)
}, [])
const sendMessage = useCallback(async (content: string) => {
if (!content.trim() || isTyping) return
setError(null)
// Auto-detect mode if needed
const classification = intentClassifier.classify(content)
if (classification.confidence > 0.7 && classification.mode !== currentMode) {
setCurrentMode(classification.mode)
}
if (classification.detectedDocumentType && !activeDocumentType) {
setActiveDocumentType(classification.detectedDocumentType)
}
const userMessage: DraftingChatMessage = {
role: 'user',
content: content.trim(),
}
setMessages(prev => [...prev, userMessage])
setIsTyping(true)
abortControllerRef.current = new AbortController()
try {
const projection = getProjection()
const response = await fetch('/api/sdk/drafting-engine/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
message: content.trim(),
history: messages.map(m => ({ role: m.role, content: m.content })),
sdkStateProjection: projection,
mode: currentMode,
documentType: activeDocumentType,
}),
signal: abortControllerRef.current.signal,
})
if (!response.ok) {
const errorData = await response.json().catch(() => ({ error: 'Unbekannter Fehler' }))
throw new Error(errorData.error || `Server-Fehler (${response.status})`)
}
const agentMessageId = `msg-${Date.now()}-agent`
setMessages(prev => [...prev, {
role: 'assistant',
content: '',
metadata: { mode: currentMode, documentType: activeDocumentType ?? undefined },
}])
// Stream response
const reader = response.body!.getReader()
const decoder = new TextDecoder()
let accumulated = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
accumulated += decoder.decode(value, { stream: true })
const text = accumulated
setMessages(prev =>
prev.map((m, i) => i === prev.length - 1 ? { ...m, content: text } : m)
)
}
setIsTyping(false)
} catch (err) {
if ((err as Error).name === 'AbortError') {
setIsTyping(false)
return
}
setError((err as Error).message)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Fehler: ${(err as Error).message}`,
}])
setIsTyping(false)
}
}, [isTyping, messages, currentMode, activeDocumentType, getProjection])
const requestDraft = useCallback(async (instructions?: string) => {
if (!activeDocumentType) {
setError('Bitte waehlen Sie zuerst einen Dokumenttyp.')
return
}
setError(null)
setIsTyping(true)
try {
const draftContext = stateProjector.projectForDraft(state, activeDocumentType)
const response = await fetch('/api/sdk/drafting-engine/draft', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType,
draftContext,
instructions,
existingDraft: currentDraft,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Draft-Generierung fehlgeschlagen')
}
setCurrentDraft(result.draft)
setConstraintCheck(result.constraintCheck)
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft fuer ${activeDocumentType} erstellt (${result.draft.sections.length} Sections). Oeffnen Sie den Editor zur Bearbeitung.`,
metadata: { mode: 'draft', documentType: activeDocumentType, hasDraft: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const validateDraft = useCallback(async () => {
setError(null)
setIsTyping(true)
try {
const docTypes: ScopeDocumentType[] = activeDocumentType
? [activeDocumentType]
: ['vvt', 'tom', 'lf']
const validationContext = stateProjector.projectForValidate(state, docTypes)
const response = await fetch('/api/sdk/drafting-engine/validate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
documentType: activeDocumentType || 'vvt',
draftContent: currentDraft?.content || '',
validationContext,
}),
})
const result = await response.json()
if (!response.ok) {
throw new Error(result.error || 'Validierung fehlgeschlagen')
}
setValidationResult(result)
const summary = result.passed
? `Validierung bestanden. ${result.warnings.length} Warnungen, ${result.suggestions.length} Vorschlaege.`
: `Validierung fehlgeschlagen. ${result.errors.length} Fehler, ${result.warnings.length} Warnungen.`
setMessages(prev => [...prev, {
role: 'assistant',
content: summary,
metadata: { mode: 'validate', hasValidation: true },
}])
setIsTyping(false)
} catch (err) {
setError((err as Error).message)
setIsTyping(false)
}
}, [activeDocumentType, state, currentDraft])
const acceptDraft = useCallback(() => {
if (!currentDraft || !activeDocumentType) return
// Dispatch the draft data into SDK state
switch (activeDocumentType) {
case 'vvt':
dispatch({
type: 'ADD_PROCESSING_ACTIVITY',
payload: {
id: `draft-vvt-${Date.now()}`,
name: currentDraft.sections.find(s => s.schemaField === 'name')?.content || 'Neuer VVT-Eintrag',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
case 'tom':
dispatch({
type: 'ADD_TOM',
payload: {
id: `draft-tom-${Date.now()}`,
name: 'TOM-Entwurf',
...Object.fromEntries(
currentDraft.sections
.filter(s => s.schemaField)
.map(s => [s.schemaField!, s.content])
),
},
})
break
default:
dispatch({
type: 'ADD_DOCUMENT',
payload: {
id: `draft-${activeDocumentType}-${Date.now()}`,
type: activeDocumentType,
content: currentDraft.content,
sections: currentDraft.sections,
},
})
}
setMessages(prev => [...prev, {
role: 'assistant',
content: `Draft wurde in den SDK-State uebernommen.`,
}])
setCurrentDraft(null)
}, [currentDraft, activeDocumentType, dispatch])
const stopGeneration = useCallback(() => {
abortControllerRef.current?.abort()
setIsTyping(false)
}, [])
const clearMessages = useCallback(() => {
setMessages([])
setCurrentDraft(null)
setValidationResult(null)
setConstraintCheck(null)
setError(null)
}, [])
return {
currentMode,
activeDocumentType,
messages,
isTyping,
currentDraft,
validationResult,
constraintCheck,
error,
setMode,
setDocumentType,
sendMessage,
requestDraft,
validateDraft,
acceptDraft,
stopGeneration,
clearMessages,
}
}

View File

@@ -1,881 +0,0 @@
/**
* DSR API Client
*
* API client for Data Subject Request management
* Connects to the Go Consent Service backend
*/
import {
DSRRequest,
DSRListResponse,
DSRFilters,
DSRCreateRequest,
DSRUpdateRequest,
DSRVerifyIdentityRequest,
DSRCompleteRequest,
DSRRejectRequest,
DSRExtendDeadlineRequest,
DSRSendCommunicationRequest,
DSRCommunication,
DSRAuditEntry,
DSRStatistics,
DSRDataExport,
DSRErasureChecklist
} from './types'
// =============================================================================
// CONFIGURATION
// =============================================================================
const DSR_API_BASE = process.env.NEXT_PUBLIC_CONSENT_SERVICE_URL || 'http://localhost:8081'
const API_TIMEOUT = 30000 // 30 seconds
// =============================================================================
// HELPER FUNCTIONS
// =============================================================================
function getTenantId(): string {
// In a real app, this would come from auth context or localStorage
if (typeof window !== 'undefined') {
return localStorage.getItem('tenantId') || 'default-tenant'
}
return 'default-tenant'
}
function getAuthHeaders(): HeadersInit {
const headers: HeadersInit = {
'Content-Type': 'application/json',
'X-Tenant-ID': getTenantId()
}
// Add auth token if available
if (typeof window !== 'undefined') {
const token = localStorage.getItem('authToken')
if (token) {
headers['Authorization'] = `Bearer ${token}`
}
}
return headers
}
async function fetchWithTimeout<T>(
url: string,
options: RequestInit = {},
timeout: number = API_TIMEOUT
): Promise<T> {
const controller = new AbortController()
const timeoutId = setTimeout(() => controller.abort(), timeout)
try {
const response = await fetch(url, {
...options,
signal: controller.signal,
headers: {
...getAuthHeaders(),
...options.headers
}
})
if (!response.ok) {
const errorBody = await response.text()
let errorMessage = `HTTP ${response.status}: ${response.statusText}`
try {
const errorJson = JSON.parse(errorBody)
errorMessage = errorJson.error || errorJson.message || errorMessage
} catch {
// Keep the HTTP status message
}
throw new Error(errorMessage)
}
// Handle empty responses
const contentType = response.headers.get('content-type')
if (contentType && contentType.includes('application/json')) {
return response.json()
}
return {} as T
} finally {
clearTimeout(timeoutId)
}
}
// =============================================================================
// DSR LIST & CRUD
// =============================================================================
/**
* Fetch all DSR requests with optional filters
*/
export async function fetchDSRList(filters?: DSRFilters): Promise<DSRListResponse> {
const params = new URLSearchParams()
if (filters) {
if (filters.status) {
const statuses = Array.isArray(filters.status) ? filters.status : [filters.status]
statuses.forEach(s => params.append('status', s))
}
if (filters.type) {
const types = Array.isArray(filters.type) ? filters.type : [filters.type]
types.forEach(t => params.append('type', t))
}
if (filters.priority) params.set('priority', filters.priority)
if (filters.assignedTo) params.set('assignedTo', filters.assignedTo)
if (filters.overdue !== undefined) params.set('overdue', String(filters.overdue))
if (filters.search) params.set('search', filters.search)
if (filters.dateFrom) params.set('dateFrom', filters.dateFrom)
if (filters.dateTo) params.set('dateTo', filters.dateTo)
}
const queryString = params.toString()
const url = `${DSR_API_BASE}/api/v1/admin/dsr${queryString ? `?${queryString}` : ''}`
return fetchWithTimeout<DSRListResponse>(url)
}
/**
* Fetch a single DSR request by ID
*/
export async function fetchDSR(id: string): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`)
}
/**
* Create a new DSR request
*/
export async function createDSR(request: DSRCreateRequest): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr`, {
method: 'POST',
body: JSON.stringify(request)
})
}
/**
* Update a DSR request
*/
export async function updateDSR(id: string, update: DSRUpdateRequest): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
method: 'PUT',
body: JSON.stringify(update)
})
}
/**
* Delete a DSR request (soft delete - marks as cancelled)
*/
export async function deleteDSR(id: string): Promise<void> {
await fetchWithTimeout<void>(`${DSR_API_BASE}/api/v1/admin/dsr/${id}`, {
method: 'DELETE'
})
}
// =============================================================================
// DSR WORKFLOW ACTIONS
// =============================================================================
/**
* Verify the identity of the requester
*/
export async function verifyIdentity(
dsrId: string,
verification: DSRVerifyIdentityRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/verify-identity`,
{
method: 'POST',
body: JSON.stringify(verification)
}
)
}
/**
* Complete a DSR request
*/
export async function completeDSR(
dsrId: string,
completion?: DSRCompleteRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/complete`,
{
method: 'POST',
body: JSON.stringify(completion || {})
}
)
}
/**
* Reject a DSR request
*/
export async function rejectDSR(
dsrId: string,
rejection: DSRRejectRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/reject`,
{
method: 'POST',
body: JSON.stringify(rejection)
}
)
}
/**
* Extend the deadline for a DSR request
*/
export async function extendDeadline(
dsrId: string,
extension: DSRExtendDeadlineRequest
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/extend`,
{
method: 'POST',
body: JSON.stringify(extension)
}
)
}
/**
* Assign a DSR request to a user
*/
export async function assignDSR(
dsrId: string,
assignedTo: string
): Promise<DSRRequest> {
return fetchWithTimeout<DSRRequest>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/assign`,
{
method: 'POST',
body: JSON.stringify({ assignedTo })
}
)
}
// =============================================================================
// COMMUNICATION
// =============================================================================
/**
* Get all communications for a DSR request
*/
export async function getCommunications(dsrId: string): Promise<DSRCommunication[]> {
return fetchWithTimeout<DSRCommunication[]>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/communications`
)
}
/**
* Send a communication (email, letter, internal note)
*/
export async function sendCommunication(
dsrId: string,
communication: DSRSendCommunicationRequest
): Promise<DSRCommunication> {
return fetchWithTimeout<DSRCommunication>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/send-communication`,
{
method: 'POST',
body: JSON.stringify(communication)
}
)
}
// =============================================================================
// AUDIT LOG
// =============================================================================
/**
* Get audit log entries for a DSR request
*/
export async function getAuditLog(dsrId: string): Promise<DSRAuditEntry[]> {
return fetchWithTimeout<DSRAuditEntry[]>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/audit`
)
}
// =============================================================================
// STATISTICS
// =============================================================================
/**
* Get DSR statistics
*/
export async function getDSRStatistics(): Promise<DSRStatistics> {
return fetchWithTimeout<DSRStatistics>(
`${DSR_API_BASE}/api/v1/admin/dsr/statistics`
)
}
// =============================================================================
// DATA EXPORT (Art. 15, 20)
// =============================================================================
/**
* Generate data export for Art. 15 (access) or Art. 20 (portability)
*/
export async function generateDataExport(
dsrId: string,
format: 'json' | 'csv' | 'xml' | 'pdf' = 'json'
): Promise<DSRDataExport> {
return fetchWithTimeout<DSRDataExport>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export`,
{
method: 'POST',
body: JSON.stringify({ format })
}
)
}
/**
* Download generated data export
*/
export async function downloadDataExport(dsrId: string): Promise<Blob> {
const response = await fetch(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/export/download`,
{
headers: getAuthHeaders()
}
)
if (!response.ok) {
throw new Error(`Download failed: ${response.statusText}`)
}
return response.blob()
}
// =============================================================================
// ERASURE CHECKLIST (Art. 17)
// =============================================================================
/**
* Get the erasure checklist for an Art. 17 request
*/
export async function getErasureChecklist(dsrId: string): Promise<DSRErasureChecklist> {
return fetchWithTimeout<DSRErasureChecklist>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`
)
}
/**
* Update the erasure checklist
*/
export async function updateErasureChecklist(
dsrId: string,
checklist: DSRErasureChecklist
): Promise<DSRErasureChecklist> {
return fetchWithTimeout<DSRErasureChecklist>(
`${DSR_API_BASE}/api/v1/admin/dsr/${dsrId}/erasure-checklist`,
{
method: 'PUT',
body: JSON.stringify(checklist)
}
)
}
// =============================================================================
// EMAIL TEMPLATES
// =============================================================================
/**
* Get available email templates
*/
export async function getEmailTemplates(): Promise<{ id: string; name: string; stage: string }[]> {
return fetchWithTimeout<{ id: string; name: string; stage: string }[]>(
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates`
)
}
/**
* Preview an email with variables filled in
*/
export async function previewEmail(
templateId: string,
dsrId: string
): Promise<{ subject: string; body: string }> {
return fetchWithTimeout<{ subject: string; body: string }>(
`${DSR_API_BASE}/api/v1/admin/dsr/email-templates/${templateId}/preview`,
{
method: 'POST',
body: JSON.stringify({ dsrId })
}
)
}
// =============================================================================
// SDK API FUNCTIONS (via Next.js proxy to ai-compliance-sdk)
// =============================================================================
interface BackendDSR {
id: string
tenant_id: string
namespace_id?: string
request_type: string
status: string
subject_name: string
subject_email: string
subject_identifier?: string
request_description: string
request_channel: string
received_at: string
verified_at?: string
verification_method?: string
deadline_at: string
extended_deadline_at?: string
extension_reason?: string
completed_at?: string
response_sent: boolean
response_sent_at?: string
response_method?: string
rejection_reason?: string
notes?: string
affected_systems?: string[]
assigned_to?: string
created_at: string
updated_at: string
}
function mapBackendStatus(status: string): import('./types').DSRStatus {
const mapping: Record<string, import('./types').DSRStatus> = {
'received': 'intake',
'verified': 'identity_verification',
'in_progress': 'processing',
'completed': 'completed',
'rejected': 'rejected',
'extended': 'processing',
}
return mapping[status] || 'intake'
}
function mapBackendChannel(channel: string): import('./types').DSRSource {
const mapping: Record<string, import('./types').DSRSource> = {
'email': 'email',
'form': 'web_form',
'phone': 'phone',
'letter': 'letter',
}
return mapping[channel] || 'other'
}
/**
* Transform flat backend DSR to nested SDK DSRRequest format
*/
export function transformBackendDSR(b: BackendDSR): DSRRequest {
const deadlineAt = b.extended_deadline_at || b.deadline_at
const receivedDate = new Date(b.received_at)
const defaultDeadlineDays = 30
const originalDeadline = b.deadline_at || new Date(receivedDate.getTime() + defaultDeadlineDays * 24 * 60 * 60 * 1000).toISOString()
return {
id: b.id,
referenceNumber: `DSR-${new Date(b.created_at).getFullYear()}-${b.id.slice(0, 6).toUpperCase()}`,
type: b.request_type as DSRRequest['type'],
status: mapBackendStatus(b.status),
priority: 'normal',
requester: {
name: b.subject_name,
email: b.subject_email,
customerId: b.subject_identifier,
},
source: mapBackendChannel(b.request_channel),
requestText: b.request_description,
receivedAt: b.received_at,
deadline: {
originalDeadline,
currentDeadline: deadlineAt,
extended: !!b.extended_deadline_at,
extensionReason: b.extension_reason,
},
completedAt: b.completed_at,
identityVerification: {
verified: !!b.verified_at,
verifiedAt: b.verified_at,
method: b.verification_method as any,
},
assignment: {
assignedTo: b.assigned_to || null,
},
notes: b.notes,
createdAt: b.created_at,
createdBy: 'system',
updatedAt: b.updated_at,
tenantId: b.tenant_id,
}
}
function getSdkHeaders(): HeadersInit {
if (typeof window === 'undefined') return {}
return {
'Content-Type': 'application/json',
'X-Tenant-ID': localStorage.getItem('bp_tenant_id') || '',
'X-User-ID': localStorage.getItem('bp_user_id') || '',
}
}
/**
* Fetch DSR list from SDK backend via proxy
*/
export async function fetchSDKDSRList(): Promise<{ requests: DSRRequest[]; statistics: DSRStatistics }> {
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
headers: getSdkHeaders(),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
const data = await res.json()
const backendDSRs: BackendDSR[] = data.dsrs || []
const requests = backendDSRs.map(transformBackendDSR)
// Calculate statistics locally
const now = new Date()
const statistics: DSRStatistics = {
total: requests.length,
byStatus: {
intake: requests.filter(r => r.status === 'intake').length,
identity_verification: requests.filter(r => r.status === 'identity_verification').length,
processing: requests.filter(r => r.status === 'processing').length,
completed: requests.filter(r => r.status === 'completed').length,
rejected: requests.filter(r => r.status === 'rejected').length,
cancelled: requests.filter(r => r.status === 'cancelled').length,
},
byType: {
access: requests.filter(r => r.type === 'access').length,
rectification: requests.filter(r => r.type === 'rectification').length,
erasure: requests.filter(r => r.type === 'erasure').length,
restriction: requests.filter(r => r.type === 'restriction').length,
portability: requests.filter(r => r.type === 'portability').length,
objection: requests.filter(r => r.type === 'objection').length,
},
overdue: requests.filter(r => {
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
return new Date(r.deadline.currentDeadline) < now
}).length,
dueThisWeek: requests.filter(r => {
if (r.status === 'completed' || r.status === 'rejected' || r.status === 'cancelled') return false
const deadline = new Date(r.deadline.currentDeadline)
const weekFromNow = new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000)
return deadline >= now && deadline <= weekFromNow
}).length,
averageProcessingDays: 0,
completedThisMonth: requests.filter(r => {
if (r.status !== 'completed' || !r.completedAt) return false
const completed = new Date(r.completedAt)
return completed.getMonth() === now.getMonth() && completed.getFullYear() === now.getFullYear()
}).length,
}
return { requests, statistics }
}
/**
* Create a new DSR via SDK backend
*/
export async function createSDKDSR(request: DSRCreateRequest): Promise<void> {
const body = {
request_type: request.type,
subject_name: request.requester.name,
subject_email: request.requester.email,
subject_identifier: request.requester.customerId || '',
request_description: request.requestText || '',
request_channel: request.source === 'web_form' ? 'form' : request.source,
notes: '',
}
const res = await fetch('/api/sdk/v1/dsgvo/dsr', {
method: 'POST',
headers: getSdkHeaders(),
body: JSON.stringify(body),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
}
/**
* Fetch a single DSR by ID from SDK backend
*/
export async function fetchSDKDSR(id: string): Promise<DSRRequest | null> {
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
headers: getSdkHeaders(),
})
if (!res.ok) {
return null
}
const data = await res.json()
if (!data || !data.id) return null
return transformBackendDSR(data)
}
/**
* Update DSR status via SDK backend
*/
export async function updateSDKDSRStatus(id: string, status: string): Promise<void> {
const res = await fetch(`/api/sdk/v1/dsgvo/dsr/${id}`, {
method: 'PUT',
headers: getSdkHeaders(),
body: JSON.stringify({ status }),
})
if (!res.ok) {
throw new Error(`HTTP ${res.status}`)
}
}
// =============================================================================
// MOCK DATA FUNCTIONS (kept as fallback)
// =============================================================================
export function createMockDSRList(): DSRRequest[] {
const now = new Date()
return [
{
id: 'dsr-001',
referenceNumber: 'DSR-2025-000001',
type: 'access',
status: 'intake',
priority: 'high',
requester: {
name: 'Max Mustermann',
email: 'max.mustermann@example.de'
},
source: 'web_form',
sourceDetails: 'Kontaktformular auf breakpilot.de',
receivedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 28 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: false
},
assignment: {
assignedTo: null
},
createdAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-002',
referenceNumber: 'DSR-2025-000002',
type: 'erasure',
status: 'identity_verification',
priority: 'high',
requester: {
name: 'Anna Schmidt',
email: 'anna.schmidt@example.de',
phone: '+49 170 1234567'
},
source: 'email',
requestText: 'Ich moechte, dass alle meine Daten geloescht werden.',
receivedAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 9 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: false
},
assignment: {
assignedTo: 'DSB Mueller',
assignedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString()
},
createdAt: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 4 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-003',
referenceNumber: 'DSR-2025-000003',
type: 'rectification',
status: 'processing',
priority: 'normal',
requester: {
name: 'Peter Meier',
email: 'peter.meier@example.de'
},
source: 'email',
requestText: 'Meine Adresse ist falsch gespeichert.',
receivedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 7 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: true,
method: 'existing_account',
verifiedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'DSB Mueller',
assignedAt: new Date(now.getTime() - 6 * 24 * 60 * 60 * 1000).toISOString()
},
rectificationDetails: {
fieldsToCorrect: [
{
field: 'Adresse',
currentValue: 'Musterstr. 1, 12345 Berlin',
requestedValue: 'Musterstr. 10, 12345 Berlin',
corrected: false
}
]
},
createdAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-004',
referenceNumber: 'DSR-2025-000004',
type: 'portability',
status: 'processing',
priority: 'normal',
requester: {
name: 'Lisa Weber',
email: 'lisa.weber@example.de'
},
source: 'web_form',
receivedAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() + 20 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
identityVerification: {
verified: true,
method: 'id_document',
verifiedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'IT Team',
assignedAt: new Date(now.getTime() - 8 * 24 * 60 * 60 * 1000).toISOString()
},
notes: 'JSON-Export wird vorbereitet',
createdAt: new Date(now.getTime() - 10 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-005',
referenceNumber: 'DSR-2025-000005',
type: 'objection',
status: 'rejected',
priority: 'low',
requester: {
name: 'Thomas Klein',
email: 'thomas.klein@example.de'
},
source: 'letter',
requestText: 'Ich widerspreche der Verarbeitung meiner Daten fuer Marketingzwecke.',
receivedAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() - 5 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
completedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
identityVerification: {
verified: true,
method: 'postal',
verifiedAt: new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'Rechtsabteilung',
assignedAt: new Date(now.getTime() - 28 * 24 * 60 * 60 * 1000).toISOString()
},
objectionDetails: {
processingPurpose: 'Marketing',
legalBasis: 'Berechtigtes Interesse (Art. 6(1)(f))',
objectionGrounds: 'Keine konkreten Gruende genannt',
decision: 'rejected',
decisionReason: 'Zwingende schutzwuerdige Gruende fuer die Verarbeitung ueberwiegen',
decisionBy: 'Rechtsabteilung',
decisionAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString()
},
notes: 'Widerspruch unberechtigt - zwingende schutzwuerdige Gruende',
createdAt: new Date(now.getTime() - 35 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
},
{
id: 'dsr-006',
referenceNumber: 'DSR-2025-000006',
type: 'access',
status: 'completed',
priority: 'normal',
requester: {
name: 'Sarah Braun',
email: 'sarah.braun@example.de'
},
source: 'email',
receivedAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
deadline: {
originalDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
currentDeadline: new Date(now.getTime() - 15 * 24 * 60 * 60 * 1000).toISOString(),
extended: false
},
completedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
identityVerification: {
verified: true,
method: 'id_document',
verifiedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString(),
verifiedBy: 'DSB Mueller'
},
assignment: {
assignedTo: 'DSB Mueller',
assignedAt: new Date(now.getTime() - 42 * 24 * 60 * 60 * 1000).toISOString()
},
dataExport: {
format: 'pdf',
generatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
generatedBy: 'DSB Mueller',
fileName: 'datenauskunft_sarah_braun.pdf',
fileSize: 245000,
includesThirdPartyData: false
},
createdAt: new Date(now.getTime() - 45 * 24 * 60 * 60 * 1000).toISOString(),
createdBy: 'system',
updatedAt: new Date(now.getTime() - 20 * 24 * 60 * 60 * 1000).toISOString(),
tenantId: 'default-tenant'
}
]
}
export function createMockStatistics(): DSRStatistics {
return {
total: 6,
byStatus: {
intake: 1,
identity_verification: 1,
processing: 2,
completed: 1,
rejected: 1,
cancelled: 0
},
byType: {
access: 2,
rectification: 1,
erasure: 1,
restriction: 0,
portability: 1,
objection: 1
},
overdue: 0,
dueThisWeek: 2,
averageProcessingDays: 18,
completedThisMonth: 1
}
}

Some files were not shown because too many files have changed in this diff Show More