Initial commit: breakpilot-compliance - Compliance SDK Platform

Services: Admin-Compliance, Backend-Compliance,
AI-Compliance-SDK, Consent-SDK, Developer-Portal,
PCA-Platform, DSMS

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Boenisch
2026-02-11 23:47:28 +01:00
commit 4435e7ea0a
734 changed files with 251369 additions and 0 deletions

View File

@@ -0,0 +1,240 @@
'use client';
import { useControls } from '@breakpilot/compliance-sdk-react';
import Link from 'next/link';
import {
ArrowLeft,
CheckCircle,
Shield,
FileCheck,
AlertTriangle,
Target,
BarChart3,
FileText,
Download,
} from 'lucide-react';
export default function CompliancePage() {
const { controls, evidence, risks, isLoading } = useControls();
const domains = [
{ id: 'access', name: 'Access Control', count: 5 },
{ id: 'data', name: 'Data Protection', count: 6 },
{ id: 'network', name: 'Network Security', count: 4 },
{ id: 'incident', name: 'Incident Response', count: 5 },
{ id: 'business', name: 'Business Continuity', count: 4 },
{ id: 'vendor', name: 'Vendor Management', count: 4 },
{ id: 'training', name: 'Security Training', count: 4 },
{ id: 'physical', name: 'Physical Security', count: 6 },
{ id: 'governance', name: 'Governance', count: 6 },
];
const controlStats = {
total: controls?.length ?? 44,
implemented:
controls?.filter((c) => c.implementationStatus === 'IMPLEMENTED').length ??
32,
inProgress:
controls?.filter((c) => c.implementationStatus === 'IN_PROGRESS').length ??
8,
notImplemented:
controls?.filter((c) => c.implementationStatus === 'NOT_IMPLEMENTED')
.length ?? 4,
};
const implementationRate = Math.round(
(controlStats.implemented / controlStats.total) * 100
);
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link
href="/"
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</Link>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-green-500/10">
<CheckCircle className="h-6 w-6 text-green-500" />
</div>
<div>
<h1 className="text-xl font-semibold">Compliance Hub</h1>
<p className="text-sm text-muted-foreground">
Controls, Evidence & Obligations
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Link
href="/compliance/export"
className="inline-flex items-center gap-2 px-4 py-2 border rounded-lg hover:bg-muted transition-colors"
>
<Download className="h-4 w-4" />
Export Report
</Link>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-6 py-8">
{/* Stats Overview */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-6 mb-8">
<div className="bg-card border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<Shield className="h-8 w-8 text-green-500" />
<span className="text-3xl font-bold">{implementationRate}%</span>
</div>
<div className="text-sm font-medium">Implementation Rate</div>
<div className="w-full bg-muted rounded-full h-2 mt-2">
<div
className="bg-green-500 h-2 rounded-full transition-all"
style={{ width: `${implementationRate}%` }}
/>
</div>
</div>
<div className="bg-card border rounded-xl p-6">
<div className="text-3xl font-bold text-green-500">
{controlStats.implemented}
</div>
<div className="text-sm text-muted-foreground mt-1">
Controls Implemented
</div>
<div className="text-xs text-muted-foreground mt-2">
of {controlStats.total} total
</div>
</div>
<div className="bg-card border rounded-xl p-6">
<div className="text-3xl font-bold text-blue-500">
{evidence?.length ?? 0}
</div>
<div className="text-sm text-muted-foreground mt-1">
Evidence Items
</div>
<div className="text-xs text-muted-foreground mt-2">
Uploaded documents
</div>
</div>
<div className="bg-card border rounded-xl p-6">
<div className="text-3xl font-bold text-yellow-500">
{risks?.filter((r) => r.status !== 'MITIGATED').length ?? 0}
</div>
<div className="text-sm text-muted-foreground mt-1">Open Risks</div>
<div className="text-xs text-muted-foreground mt-2">
Require attention
</div>
</div>
</div>
{/* Quick Links */}
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<Link
href="/compliance/controls"
className="bg-card border rounded-xl p-6 hover:border-primary/50 transition-colors group"
>
<div className="flex items-center gap-4">
<div className="p-3 rounded-lg bg-green-500/10">
<Shield className="h-6 w-6 text-green-500" />
</div>
<div>
<h3 className="font-semibold">Controls</h3>
<p className="text-sm text-muted-foreground">
44+ controls in 9 domains
</p>
</div>
</div>
</Link>
<Link
href="/compliance/evidence"
className="bg-card border rounded-xl p-6 hover:border-primary/50 transition-colors group"
>
<div className="flex items-center gap-4">
<div className="p-3 rounded-lg bg-blue-500/10">
<FileCheck className="h-6 w-6 text-blue-500" />
</div>
<div>
<h3 className="font-semibold">Evidence</h3>
<p className="text-sm text-muted-foreground">
{evidence?.length ?? 0} documents uploaded
</p>
</div>
</div>
</Link>
<Link
href="/compliance/risks"
className="bg-card border rounded-xl p-6 hover:border-primary/50 transition-colors group"
>
<div className="flex items-center gap-4">
<div className="p-3 rounded-lg bg-yellow-500/10">
<AlertTriangle className="h-6 w-6 text-yellow-500" />
</div>
<div>
<h3 className="font-semibold">Risk Register</h3>
<p className="text-sm text-muted-foreground">
{risks?.length ?? 0} risks tracked
</p>
</div>
</div>
</Link>
</div>
{/* Control Domains */}
<div className="bg-card border rounded-xl p-6">
<h2 className="text-lg font-semibold mb-6">Control Domains</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
{domains.map((domain) => {
const domainControls = controls?.filter(
(c) => c.domain === domain.id
);
const implemented =
domainControls?.filter(
(c) => c.implementationStatus === 'IMPLEMENTED'
).length ?? 0;
const total = domain.count;
const percent = Math.round((implemented / total) * 100);
return (
<Link
key={domain.id}
href={`/compliance/controls?domain=${domain.id}`}
className="p-4 border rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex items-center justify-between mb-2">
<span className="font-medium">{domain.name}</span>
<span className="text-sm text-muted-foreground">
{implemented}/{total}
</span>
</div>
<div className="w-full bg-muted rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
percent === 100
? 'bg-green-500'
: percent >= 50
? 'bg-blue-500'
: 'bg-yellow-500'
}`}
style={{ width: `${percent}%` }}
/>
</div>
</Link>
);
})}
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,209 @@
'use client';
import { useState } from 'react';
import { useDSGVO } from '@breakpilot/compliance-sdk-react';
import Link from 'next/link';
import {
ArrowLeft,
ClipboardCheck,
Plus,
Search,
Filter,
MoreVertical,
CheckCircle,
XCircle,
Clock,
} from 'lucide-react';
export default function ConsentManagementPage() {
const { consents, grantConsent, revokeConsent, isLoading } = useDSGVO();
const [searchQuery, setSearchQuery] = useState('');
const [filterPurpose, setFilterPurpose] = useState<string>('all');
const purposes = [
{ id: 'analytics', name: 'Analytics', description: 'Website usage tracking' },
{ id: 'marketing', name: 'Marketing', description: 'Marketing communications' },
{ id: 'functional', name: 'Functional', description: 'Enhanced features' },
{ id: 'necessary', name: 'Necessary', description: 'Essential cookies' },
];
const filteredConsents = consents?.filter((consent) => {
const matchesSearch =
consent.userId.toLowerCase().includes(searchQuery.toLowerCase()) ||
consent.purpose.toLowerCase().includes(searchQuery.toLowerCase());
const matchesPurpose =
filterPurpose === 'all' || consent.purpose === filterPurpose;
return matchesSearch && matchesPurpose;
});
const stats = {
total: consents?.length ?? 0,
granted: consents?.filter((c) => c.granted).length ?? 0,
revoked: consents?.filter((c) => !c.granted).length ?? 0,
pending: consents?.filter((c) => c.granted === null).length ?? 0,
};
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center gap-4">
<Link
href="/dsgvo"
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</Link>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/10">
<ClipboardCheck className="h-6 w-6 text-blue-500" />
</div>
<div>
<h1 className="text-xl font-semibold">Einwilligungen</h1>
<p className="text-sm text-muted-foreground">
Art. 6, 7 DSGVO - Consent Management
</p>
</div>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-6 py-8">
{/* Stats */}
<div className="grid grid-cols-4 gap-4 mb-8">
<div className="bg-card border rounded-lg p-4">
<div className="text-2xl font-bold">{stats.total}</div>
<div className="text-sm text-muted-foreground">Total</div>
</div>
<div className="bg-card border rounded-lg p-4">
<div className="text-2xl font-bold text-green-500">
{stats.granted}
</div>
<div className="text-sm text-muted-foreground">Granted</div>
</div>
<div className="bg-card border rounded-lg p-4">
<div className="text-2xl font-bold text-red-500">
{stats.revoked}
</div>
<div className="text-sm text-muted-foreground">Revoked</div>
</div>
<div className="bg-card border rounded-lg p-4">
<div className="text-2xl font-bold text-yellow-500">
{stats.pending}
</div>
<div className="text-sm text-muted-foreground">Pending</div>
</div>
</div>
{/* Filters */}
<div className="flex items-center gap-4 mb-6">
<div className="relative flex-1">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" />
<input
type="text"
placeholder="Search by user or purpose..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
className="w-full pl-10 pr-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
/>
</div>
<select
value={filterPurpose}
onChange={(e) => setFilterPurpose(e.target.value)}
className="px-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
>
<option value="all">All Purposes</option>
{purposes.map((p) => (
<option key={p.id} value={p.id}>
{p.name}
</option>
))}
</select>
</div>
{/* Consent List */}
<div className="bg-card border rounded-xl overflow-hidden">
<table className="w-full">
<thead className="bg-muted/50 border-b">
<tr>
<th className="text-left px-6 py-3 text-sm font-medium">
User ID
</th>
<th className="text-left px-6 py-3 text-sm font-medium">
Purpose
</th>
<th className="text-left px-6 py-3 text-sm font-medium">
Status
</th>
<th className="text-left px-6 py-3 text-sm font-medium">
Source
</th>
<th className="text-left px-6 py-3 text-sm font-medium">
Created
</th>
<th className="text-right px-6 py-3 text-sm font-medium">
Actions
</th>
</tr>
</thead>
<tbody className="divide-y">
{filteredConsents?.map((consent) => (
<tr key={consent.id} className="hover:bg-muted/30">
<td className="px-6 py-4">
<span className="font-mono text-sm">{consent.userId}</span>
</td>
<td className="px-6 py-4">
<span className="capitalize">{consent.purpose}</span>
</td>
<td className="px-6 py-4">
{consent.granted ? (
<span className="inline-flex items-center gap-1 text-green-500">
<CheckCircle className="h-4 w-4" />
Granted
</span>
) : consent.granted === false ? (
<span className="inline-flex items-center gap-1 text-red-500">
<XCircle className="h-4 w-4" />
Revoked
</span>
) : (
<span className="inline-flex items-center gap-1 text-yellow-500">
<Clock className="h-4 w-4" />
Pending
</span>
)}
</td>
<td className="px-6 py-4 text-sm text-muted-foreground">
{consent.source || 'Website'}
</td>
<td className="px-6 py-4 text-sm text-muted-foreground">
{consent.createdAt
? new Date(consent.createdAt).toLocaleDateString('de-DE')
: '-'}
</td>
<td className="px-6 py-4 text-right">
<button className="p-2 hover:bg-muted rounded-lg">
<MoreVertical className="h-4 w-4" />
</button>
</td>
</tr>
))}
{(!filteredConsents || filteredConsents.length === 0) && (
<tr>
<td
colSpan={6}
className="px-6 py-12 text-center text-muted-foreground"
>
No consents found
</td>
</tr>
)}
</tbody>
</table>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,188 @@
'use client';
import { useDSGVO } from '@breakpilot/compliance-sdk-react';
import Link from 'next/link';
import {
Shield,
Users,
FileCheck,
FileText,
Lock,
Trash2,
ClipboardCheck,
AlertCircle,
ArrowRight,
ArrowLeft,
} from 'lucide-react';
export default function DSGVOPage() {
const { consents, dsrRequests, vvtActivities, toms, isLoading } = useDSGVO();
const modules = [
{
id: 'consent',
title: 'Einwilligungen',
description: 'Consent-Tracking Multi-Channel',
icon: ClipboardCheck,
href: '/dsgvo/consent',
articles: 'Art. 6, 7',
count: consents?.length ?? 0,
color: 'bg-blue-500',
},
{
id: 'dsr',
title: 'Betroffenenrechte (DSR)',
description: 'Auskunft, Loeschung, Berichtigung',
icon: Users,
href: '/dsgvo/dsr',
articles: 'Art. 15-21',
count: dsrRequests?.length ?? 0,
color: 'bg-green-500',
},
{
id: 'vvt',
title: 'Verarbeitungsverzeichnis',
description: 'Dokumentation aller Verarbeitungstaetigkeiten',
icon: FileText,
href: '/dsgvo/vvt',
articles: 'Art. 30',
count: vvtActivities?.length ?? 0,
color: 'bg-purple-500',
},
{
id: 'dsfa',
title: 'Datenschutz-Folgenabschaetzung',
description: 'Risk Assessment fuer Verarbeitungen',
icon: AlertCircle,
href: '/dsgvo/dsfa',
articles: 'Art. 35, 36',
count: 0,
color: 'bg-yellow-500',
},
{
id: 'tom',
title: 'TOM',
description: 'Technische & Organisatorische Massnahmen',
icon: Lock,
href: '/dsgvo/tom',
articles: 'Art. 32',
count: toms?.length ?? 0,
color: 'bg-red-500',
},
{
id: 'retention',
title: 'Loeschfristen',
description: 'Retention Policies & Automations',
icon: Trash2,
href: '/dsgvo/retention',
articles: 'Art. 5, 17',
count: 0,
color: 'bg-orange-500',
},
];
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center gap-4">
<Link
href="/"
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</Link>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-blue-500/10">
<Shield className="h-6 w-6 text-blue-500" />
</div>
<div>
<h1 className="text-xl font-semibold">DSGVO Modul</h1>
<p className="text-sm text-muted-foreground">
Datenschutz-Grundverordnung Compliance
</p>
</div>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-6 py-8">
{/* Progress Overview */}
<div className="bg-card border rounded-xl p-6 mb-8">
<h2 className="text-lg font-medium mb-4">DSGVO Compliance Status</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
<div className="text-center">
<div className="text-3xl font-bold text-blue-500">
{consents?.length ?? 0}
</div>
<div className="text-sm text-muted-foreground">
Active Consents
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-green-500">
{dsrRequests?.filter((r) => r.status === 'PENDING').length ?? 0}
</div>
<div className="text-sm text-muted-foreground">Pending DSRs</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-purple-500">
{vvtActivities?.length ?? 0}
</div>
<div className="text-sm text-muted-foreground">
Processing Activities
</div>
</div>
<div className="text-center">
<div className="text-3xl font-bold text-red-500">
{toms?.filter((t) => t.implementationStatus === 'IMPLEMENTED').length ??
0}
</div>
<div className="text-sm text-muted-foreground">
Implemented TOMs
</div>
</div>
</div>
</div>
{/* Module Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{modules.map((module) => {
const Icon = module.icon;
return (
<Link
key={module.id}
href={module.href}
className="group bg-card border rounded-xl p-6 hover:shadow-lg transition-all duration-200 hover:border-primary/50"
>
<div className="flex items-start justify-between mb-4">
<div className={`p-3 rounded-lg ${module.color}/10`}>
<Icon className={`h-6 w-6 ${module.color.replace('bg-', 'text-')}`} />
</div>
<span className="text-xs font-mono text-muted-foreground bg-muted px-2 py-1 rounded">
{module.articles}
</span>
</div>
<h3 className="text-lg font-semibold mb-1">{module.title}</h3>
<p className="text-sm text-muted-foreground mb-4">
{module.description}
</p>
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
<span className="font-medium text-foreground">
{module.count}
</span>{' '}
Eintraege
</span>
<ArrowRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
</div>
</Link>
);
})}
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,59 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
--radius: 0.5rem;
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}
@layer base {
* {
@apply border-border;
}
body {
@apply bg-background text-foreground;
}
}

View File

@@ -0,0 +1,25 @@
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import { Providers } from './providers';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: 'BreakPilot Compliance Admin',
description: 'Compliance Management Dashboard',
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="de" suppressHydrationWarning>
<body className={inter.className}>
<Providers>{children}</Providers>
</body>
</html>
);
}

View File

@@ -0,0 +1,265 @@
'use client';
import { useCompliance } from '@breakpilot/compliance-sdk-react';
import Link from 'next/link';
import {
Shield,
FileText,
Lock,
Search,
AlertTriangle,
CheckCircle,
ArrowRight,
BarChart3,
Settings,
} from 'lucide-react';
export default function DashboardPage() {
const { state, isLoading, error, progress } = useCompliance();
if (isLoading) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-primary mx-auto mb-4" />
<p className="text-muted-foreground">Loading compliance data...</p>
</div>
</div>
);
}
if (error) {
return (
<div className="min-h-screen flex items-center justify-center bg-background">
<div className="text-center text-destructive">
<AlertTriangle className="h-12 w-12 mx-auto mb-4" />
<p>Error loading compliance data</p>
<p className="text-sm text-muted-foreground mt-2">{error}</p>
</div>
</div>
);
}
const modules = [
{
id: 'dsgvo',
title: 'DSGVO',
description: 'Datenschutz-Grundverordnung Compliance',
icon: Shield,
href: '/dsgvo',
stats: { completed: 8, total: 12 },
color: 'text-blue-500',
},
{
id: 'compliance',
title: 'Compliance Hub',
description: 'Controls, Evidence & Obligations',
icon: CheckCircle,
href: '/compliance',
stats: { completed: 32, total: 44 },
color: 'text-green-500',
},
{
id: 'rag',
title: 'Legal Assistant',
description: 'AI-powered regulatory search',
icon: Search,
href: '/rag',
stats: { documents: 21 },
color: 'text-purple-500',
},
{
id: 'sbom',
title: 'SBOM',
description: 'Software Bill of Materials',
icon: FileText,
href: '/sbom',
stats: { components: 140 },
color: 'text-orange-500',
},
{
id: 'security',
title: 'Security',
description: 'Vulnerability scanning & findings',
icon: Lock,
href: '/security',
stats: { findings: 5, critical: 0 },
color: 'text-red-500',
},
];
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<Shield className="h-8 w-8 text-primary" />
<div>
<h1 className="text-xl font-semibold">BreakPilot Compliance</h1>
<p className="text-sm text-muted-foreground">Admin Dashboard</p>
</div>
</div>
<div className="flex items-center gap-4">
<Link
href="/settings"
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<Settings className="h-5 w-5" />
</Link>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-6 py-8">
{/* Overall Progress */}
<div className="mb-8">
<div className="bg-card border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<div>
<h2 className="text-lg font-medium">Overall Compliance Score</h2>
<p className="text-sm text-muted-foreground">
Based on all active modules
</p>
</div>
<div className="text-4xl font-bold text-primary">
{state?.complianceScore ?? progress?.overall ?? 0}%
</div>
</div>
<div className="w-full bg-muted rounded-full h-3">
<div
className="bg-primary h-3 rounded-full transition-all duration-500"
style={{
width: `${state?.complianceScore ?? progress?.overall ?? 0}%`,
}}
/>
</div>
<div className="mt-4 grid grid-cols-5 gap-4 text-center text-sm">
<div>
<div className="text-muted-foreground">DSGVO</div>
<div className="font-medium">{progress?.dsgvo ?? 0}%</div>
</div>
<div>
<div className="text-muted-foreground">Compliance</div>
<div className="font-medium">{progress?.compliance ?? 0}%</div>
</div>
<div>
<div className="text-muted-foreground">Security</div>
<div className="font-medium">{progress?.security ?? 0}%</div>
</div>
<div>
<div className="text-muted-foreground">SBOM</div>
<div className="font-medium">{progress?.sbom ?? 0}%</div>
</div>
<div>
<div className="text-muted-foreground">RAG</div>
<div className="font-medium">{progress?.rag ?? 0}%</div>
</div>
</div>
</div>
</div>
{/* Module Grid */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
{modules.map((module) => {
const Icon = module.icon;
return (
<Link
key={module.id}
href={module.href}
className="group bg-card border rounded-xl p-6 hover:shadow-lg transition-all duration-200 hover:border-primary/50"
>
<div className="flex items-start justify-between mb-4">
<div className={`p-3 rounded-lg bg-muted ${module.color}`}>
<Icon className="h-6 w-6" />
</div>
<ArrowRight className="h-5 w-5 text-muted-foreground group-hover:text-primary transition-colors" />
</div>
<h3 className="text-lg font-semibold mb-1">{module.title}</h3>
<p className="text-sm text-muted-foreground mb-4">
{module.description}
</p>
<div className="flex items-center gap-4 text-sm">
{'completed' in module.stats && (
<span className="text-muted-foreground">
<span className="font-medium text-foreground">
{module.stats.completed}
</span>
/{module.stats.total} complete
</span>
)}
{'documents' in module.stats && (
<span className="text-muted-foreground">
<span className="font-medium text-foreground">
{module.stats.documents}
</span>{' '}
documents
</span>
)}
{'components' in module.stats && (
<span className="text-muted-foreground">
<span className="font-medium text-foreground">
{module.stats.components}
</span>{' '}
components
</span>
)}
{'findings' in module.stats && (
<span className="text-muted-foreground">
<span className="font-medium text-foreground">
{module.stats.findings}
</span>{' '}
findings
{module.stats.critical > 0 && (
<span className="text-destructive ml-1">
({module.stats.critical} critical)
</span>
)}
</span>
)}
</div>
</Link>
);
})}
</div>
{/* Quick Actions */}
<div className="mt-8">
<h2 className="text-lg font-medium mb-4">Quick Actions</h2>
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<Link
href="/rag?action=ask"
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
>
<Search className="h-6 w-6 mx-auto mb-2 text-purple-500" />
<span className="text-sm font-medium">Ask Legal Question</span>
</Link>
<Link
href="/security/scan"
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
>
<Lock className="h-6 w-6 mx-auto mb-2 text-red-500" />
<span className="text-sm font-medium">Run Security Scan</span>
</Link>
<Link
href="/compliance/export"
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
>
<BarChart3 className="h-6 w-6 mx-auto mb-2 text-green-500" />
<span className="text-sm font-medium">Export Report</span>
</Link>
<Link
href="/dsgvo/consent"
className="bg-card border rounded-lg p-4 text-center hover:border-primary/50 transition-colors"
>
<Shield className="h-6 w-6 mx-auto mb-2 text-blue-500" />
<span className="text-sm font-medium">Manage Consents</span>
</Link>
</div>
</div>
</main>
</div>
);
}

View File

@@ -0,0 +1,23 @@
'use client';
import { ComplianceProvider } from '@breakpilot/compliance-sdk-react';
export function Providers({ children }: { children: React.ReactNode }) {
const apiEndpoint = process.env.NEXT_PUBLIC_API_ENDPOINT || 'http://localhost:8080/api/v1';
const apiKey = process.env.NEXT_PUBLIC_API_KEY || '';
const tenantId = process.env.NEXT_PUBLIC_TENANT_ID || 'default';
return (
<ComplianceProvider
apiEndpoint={apiEndpoint}
apiKey={apiKey}
tenantId={tenantId}
options={{
autoLoadState: true,
persistState: true,
}}
>
{children}
</ComplianceProvider>
);
}

View File

@@ -0,0 +1,245 @@
'use client';
import { useState } from 'react';
import { useRAG } from '@breakpilot/compliance-sdk-react';
import Link from 'next/link';
import {
ArrowLeft,
Search,
MessageSquare,
FileText,
Upload,
Send,
Loader2,
BookOpen,
Scale,
} from 'lucide-react';
export default function RAGPage() {
const { search, ask, isSearching, isAsking, searchResults, chatHistory } =
useRAG();
const [query, setQuery] = useState('');
const [mode, setMode] = useState<'search' | 'chat'>('chat');
const [results, setResults] = useState<any[]>([]);
const [answer, setAnswer] = useState<string>('');
const regulations = [
{ id: 'dsgvo', name: 'DSGVO', chunks: 99 },
{ id: 'ai-act', name: 'AI Act', chunks: 85 },
{ id: 'nis2', name: 'NIS2', chunks: 46 },
{ id: 'eprivacy', name: 'ePrivacy', chunks: 32 },
{ id: 'tdddg', name: 'TDDDG', chunks: 28 },
{ id: 'cra', name: 'CRA', chunks: 41 },
];
const handleSearch = async () => {
if (!query.trim()) return;
if (mode === 'search') {
const res = await search(query);
setResults(res || []);
} else {
const res = await ask(query);
setAnswer(res?.answer || '');
setResults(res?.sources || []);
}
};
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault();
handleSearch();
}
};
return (
<div className="min-h-screen bg-background flex flex-col">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center justify-between">
<div className="flex items-center gap-4">
<Link
href="/"
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</Link>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-purple-500/10">
<Scale className="h-6 w-6 text-purple-500" />
</div>
<div>
<h1 className="text-xl font-semibold">Legal Assistant</h1>
<p className="text-sm text-muted-foreground">
AI-powered regulatory knowledge base
</p>
</div>
</div>
</div>
<div className="flex items-center gap-2 bg-muted rounded-lg p-1">
<button
onClick={() => setMode('chat')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
mode === 'chat'
? 'bg-background shadow text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<MessageSquare className="h-4 w-4 inline mr-2" />
Chat
</button>
<button
onClick={() => setMode('search')}
className={`px-4 py-2 rounded-md text-sm font-medium transition-colors ${
mode === 'search'
? 'bg-background shadow text-foreground'
: 'text-muted-foreground hover:text-foreground'
}`}
>
<Search className="h-4 w-4 inline mr-2" />
Search
</button>
</div>
</div>
</div>
</header>
{/* Main Content */}
<div className="flex-1 flex">
{/* Sidebar - Regulations */}
<aside className="w-64 border-r bg-card p-4">
<h3 className="text-sm font-medium mb-4 flex items-center gap-2">
<BookOpen className="h-4 w-4" />
Indexed Regulations
</h3>
<div className="space-y-2">
{regulations.map((reg) => (
<div
key={reg.id}
className="flex items-center justify-between p-3 rounded-lg hover:bg-muted cursor-pointer"
>
<span className="text-sm font-medium">{reg.name}</span>
<span className="text-xs text-muted-foreground">
{reg.chunks} chunks
</span>
</div>
))}
</div>
<div className="mt-6 pt-6 border-t">
<button className="w-full flex items-center justify-center gap-2 px-4 py-2 border rounded-lg text-sm hover:bg-muted transition-colors">
<Upload className="h-4 w-4" />
Upload Document
</button>
</div>
</aside>
{/* Chat/Search Area */}
<main className="flex-1 flex flex-col">
{/* Results Area */}
<div className="flex-1 overflow-auto p-6">
{mode === 'chat' && answer && (
<div className="max-w-3xl mx-auto">
<div className="bg-card border rounded-xl p-6 mb-4">
<div className="prose prose-sm max-w-none dark:prose-invert">
{answer}
</div>
</div>
{results.length > 0 && (
<div>
<h4 className="text-sm font-medium mb-3">Sources</h4>
<div className="space-y-2">
{results.map((source, i) => (
<div
key={i}
className="bg-muted/50 rounded-lg p-3 text-sm"
>
<div className="font-medium">{source.regulation}</div>
<div className="text-muted-foreground text-xs mt-1">
{source.text?.substring(0, 200)}...
</div>
</div>
))}
</div>
</div>
)}
</div>
)}
{mode === 'search' && results.length > 0 && (
<div className="max-w-3xl mx-auto space-y-4">
{results.map((result, i) => (
<div
key={i}
className="bg-card border rounded-xl p-4 hover:border-primary/50 transition-colors"
>
<div className="flex items-center gap-2 mb-2">
<FileText className="h-4 w-4 text-muted-foreground" />
<span className="font-medium">{result.regulation}</span>
<span className="text-xs bg-muted px-2 py-0.5 rounded">
Score: {(result.score * 100).toFixed(0)}%
</span>
</div>
<p className="text-sm text-muted-foreground">
{result.text}
</p>
</div>
))}
</div>
)}
{!answer && results.length === 0 && (
<div className="h-full flex items-center justify-center text-center">
<div>
<Scale className="h-16 w-16 text-muted-foreground/30 mx-auto mb-4" />
<h3 className="text-lg font-medium mb-2">
Ask a Legal Question
</h3>
<p className="text-sm text-muted-foreground max-w-md">
Search through 21 indexed regulations including DSGVO, AI
Act, NIS2, and more. Get AI-powered answers with source
references.
</p>
</div>
</div>
)}
</div>
{/* Input Area */}
<div className="border-t bg-card p-4">
<div className="max-w-3xl mx-auto">
<div className="relative">
<textarea
value={query}
onChange={(e) => setQuery(e.target.value)}
onKeyDown={handleKeyDown}
placeholder={
mode === 'chat'
? 'Ask a question about compliance regulations...'
: 'Search for specific terms or articles...'
}
rows={2}
className="w-full px-4 py-3 pr-12 border rounded-xl bg-background resize-none focus:outline-none focus:ring-2 focus:ring-primary"
/>
<button
onClick={handleSearch}
disabled={isSearching || isAsking || !query.trim()}
className="absolute right-3 bottom-3 p-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
>
{isSearching || isAsking ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Send className="h-4 w-4" />
)}
</button>
</div>
<p className="text-xs text-muted-foreground mt-2 text-center">
Powered by local LLM (Qwen 2.5) with RAG over 21 legal documents
</p>
</div>
</div>
</main>
</div>
</div>
);
}

View File

@@ -0,0 +1,318 @@
'use client';
import { useState } from 'react';
import { useSecurity } from '@breakpilot/compliance-sdk-react';
import Link from 'next/link';
import {
ArrowLeft,
Lock,
AlertTriangle,
AlertCircle,
CheckCircle,
Play,
Loader2,
FileCode,
Package,
Database,
Shield,
} from 'lucide-react';
export default function SecurityPage() {
const { sbom, findings, scan, isScanning, generateSbom, isGeneratingSbom } =
useSecurity();
const [scanTarget, setScanTarget] = useState('');
const handleScan = async () => {
if (!scanTarget.trim()) return;
await scan(scanTarget);
setScanTarget('');
};
const tools = [
{
id: 'gitleaks',
name: 'Gitleaks',
description: 'Secret Detection',
icon: Lock,
},
{
id: 'semgrep',
name: 'Semgrep',
description: 'SAST Analysis',
icon: FileCode,
},
{
id: 'bandit',
name: 'Bandit',
description: 'Python Security',
icon: Shield,
},
{
id: 'trivy',
name: 'Trivy',
description: 'Container Scanning',
icon: Database,
},
{
id: 'grype',
name: 'Grype',
description: 'Dependency Vulnerabilities',
icon: Package,
},
{
id: 'syft',
name: 'Syft',
description: 'SBOM Generation',
icon: FileCode,
},
];
const findingsBySeverity = {
critical: findings?.filter((f) => f.severity === 'critical').length ?? 0,
high: findings?.filter((f) => f.severity === 'high').length ?? 0,
medium: findings?.filter((f) => f.severity === 'medium').length ?? 0,
low: findings?.filter((f) => f.severity === 'low').length ?? 0,
};
return (
<div className="min-h-screen bg-background">
{/* Header */}
<header className="border-b bg-card">
<div className="container mx-auto px-6 py-4">
<div className="flex items-center gap-4">
<Link
href="/"
className="p-2 hover:bg-muted rounded-lg transition-colors"
>
<ArrowLeft className="h-5 w-5" />
</Link>
<div className="flex items-center gap-3">
<div className="p-2 rounded-lg bg-red-500/10">
<Lock className="h-6 w-6 text-red-500" />
</div>
<div>
<h1 className="text-xl font-semibold">Security</h1>
<p className="text-sm text-muted-foreground">
Vulnerability Scanning & SBOM
</p>
</div>
</div>
</div>
</div>
</header>
<main className="container mx-auto px-6 py-8">
{/* Findings Overview */}
<div className="grid grid-cols-4 gap-4 mb-8">
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-red-500">
<div className="text-2xl font-bold text-red-500">
{findingsBySeverity.critical}
</div>
<div className="text-sm text-muted-foreground">Critical</div>
</div>
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-orange-500">
<div className="text-2xl font-bold text-orange-500">
{findingsBySeverity.high}
</div>
<div className="text-sm text-muted-foreground">High</div>
</div>
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-yellow-500">
<div className="text-2xl font-bold text-yellow-500">
{findingsBySeverity.medium}
</div>
<div className="text-sm text-muted-foreground">Medium</div>
</div>
<div className="bg-card border rounded-xl p-4 border-l-4 border-l-blue-500">
<div className="text-2xl font-bold text-blue-500">
{findingsBySeverity.low}
</div>
<div className="text-sm text-muted-foreground">Low</div>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-3 gap-8">
{/* Left Column - Scan & Tools */}
<div className="lg:col-span-2 space-y-6">
{/* Run Scan */}
<div className="bg-card border rounded-xl p-6">
<h2 className="text-lg font-semibold mb-4">Run Security Scan</h2>
<div className="flex gap-4">
<input
type="text"
value={scanTarget}
onChange={(e) => setScanTarget(e.target.value)}
placeholder="Enter repository path or URL..."
className="flex-1 px-4 py-2 border rounded-lg bg-background focus:outline-none focus:ring-2 focus:ring-primary"
/>
<button
onClick={handleScan}
disabled={isScanning || !scanTarget.trim()}
className="px-6 py-2 bg-primary text-primary-foreground rounded-lg hover:bg-primary/90 disabled:opacity-50 disabled:cursor-not-allowed inline-flex items-center gap-2"
>
{isScanning ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Play className="h-4 w-4" />
)}
Scan
</button>
</div>
</div>
{/* Tools Grid */}
<div className="bg-card border rounded-xl p-6">
<h2 className="text-lg font-semibold mb-4">Integrated Tools</h2>
<div className="grid grid-cols-2 md:grid-cols-3 gap-4">
{tools.map((tool) => {
const Icon = tool.icon;
return (
<div
key={tool.id}
className="p-4 border rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex items-center gap-3 mb-2">
<Icon className="h-5 w-5 text-muted-foreground" />
<span className="font-medium">{tool.name}</span>
</div>
<p className="text-sm text-muted-foreground">
{tool.description}
</p>
</div>
);
})}
</div>
</div>
{/* Recent Findings */}
<div className="bg-card border rounded-xl p-6">
<h2 className="text-lg font-semibold mb-4">Recent Findings</h2>
<div className="space-y-3">
{findings?.slice(0, 5).map((finding) => (
<div
key={finding.id}
className="p-4 border rounded-lg hover:bg-muted/50 transition-colors"
>
<div className="flex items-start gap-3">
{finding.severity === 'critical' && (
<AlertCircle className="h-5 w-5 text-red-500 shrink-0" />
)}
{finding.severity === 'high' && (
<AlertTriangle className="h-5 w-5 text-orange-500 shrink-0" />
)}
{finding.severity === 'medium' && (
<AlertTriangle className="h-5 w-5 text-yellow-500 shrink-0" />
)}
{finding.severity === 'low' && (
<CheckCircle className="h-5 w-5 text-blue-500 shrink-0" />
)}
<div className="flex-1">
<div className="flex items-center justify-between">
<span className="font-medium">{finding.title}</span>
<span className="text-xs font-mono text-muted-foreground">
{finding.tool}
</span>
</div>
<p className="text-sm text-muted-foreground mt-1">
{finding.description?.substring(0, 100)}...
</p>
{finding.filePath && (
<p className="text-xs font-mono text-muted-foreground mt-2">
{finding.filePath}
{finding.lineNumber && `:${finding.lineNumber}`}
</p>
)}
</div>
</div>
</div>
))}
{(!findings || findings.length === 0) && (
<div className="text-center py-8 text-muted-foreground">
No findings yet. Run a scan to get started.
</div>
)}
</div>
</div>
</div>
{/* Right Column - SBOM */}
<div className="space-y-6">
<div className="bg-card border rounded-xl p-6">
<div className="flex items-center justify-between mb-4">
<h2 className="text-lg font-semibold">SBOM</h2>
<button
onClick={() => generateSbom('.')}
disabled={isGeneratingSbom}
className="text-sm text-primary hover:underline inline-flex items-center gap-1"
>
{isGeneratingSbom ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<Package className="h-3 w-3" />
)}
Generate
</button>
</div>
{sbom && (
<div className="space-y-4">
<div className="text-center p-4 bg-muted rounded-lg">
<div className="text-3xl font-bold">
{sbom.components?.length ?? 0}
</div>
<div className="text-sm text-muted-foreground">
Components
</div>
</div>
<div className="space-y-2">
<h3 className="text-sm font-medium">By License</h3>
<div className="space-y-1">
{Object.entries(
sbom.components?.reduce(
(acc: Record<string, number>, c: any) => {
const license = c.license || 'Unknown';
acc[license] = (acc[license] || 0) + 1;
return acc;
},
{}
) ?? {}
)
.slice(0, 5)
.map(([license, count]) => (
<div
key={license}
className="flex items-center justify-between text-sm"
>
<span>{license}</span>
<span className="text-muted-foreground">
{count as number}
</span>
</div>
))}
</div>
</div>
<div className="pt-4 border-t space-y-2">
<button className="w-full px-4 py-2 border rounded-lg text-sm hover:bg-muted transition-colors">
Export CycloneDX
</button>
<button className="w-full px-4 py-2 border rounded-lg text-sm hover:bg-muted transition-colors">
Export SPDX
</button>
</div>
</div>
)}
{!sbom && (
<div className="text-center py-8 text-muted-foreground">
<Package className="h-12 w-12 mx-auto mb-3 opacity-30" />
<p className="text-sm">No SBOM generated yet</p>
</div>
)}
</div>
</div>
</div>
</main>
</div>
);
}