Merge feat/zeroclaw-compliance-agent into main

Brings all compliance doc-check features:
- 162 regex checks + 1874 Master Controls
- LLM-agnostic agent with tool calling
- Banner check (46 checks, 30 CMPs, stealth, Shadow DOM)
- Impressum check (24 checks)
- Deep consent verification (DataLayer, GCM, TCF)
- CMP E2E tests (39 tests)
- HTML email reports, FAQ, persistent history

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-05-11 11:44:20 +02:00
175 changed files with 20063 additions and 1283 deletions
@@ -141,6 +141,54 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
setIsTyping(false)
}, [])
const [emailSending, setEmailSending] = useState(false)
const [emailSent, setEmailSent] = useState(false)
const handleSendAsEmail = useCallback(async () => {
if (messages.length === 0 || emailSending) return
setEmailSending(true)
try {
// Build HTML from chat messages
const qaPairs = messages.reduce<{ q: string; a: string }[]>((acc, m, i) => {
if (m.role === 'user') {
const next = messages[i + 1]
acc.push({ q: m.content, a: next?.role === 'agent' ? next.content : '(keine Antwort)' })
}
return acc
}, [])
const qaHtml = qaPairs.map(({ q, a }) =>
`<div style="margin-bottom:16px;"><p style="font-weight:600;color:#1e293b;">Frage: ${q}</p><p style="color:#475569;white-space:pre-wrap;">${a}</p></div>`
).join('')
const bodyHtml = `
<h2 style="color:#1e293b;">Compliance Advisor — Beratungsprotokoll</h2>
<p style="color:#64748b;font-size:13px;">Datum: ${new Date().toLocaleString('de-DE')} | Land: ${selectedCountry} | Kontext: ${currentStep}</p>
<hr style="border-color:#e2e8f0;margin:16px 0;">
${qaHtml}
<hr style="border-color:#e2e8f0;margin:16px 0;">
<p style="color:#94a3b8;font-size:11px;">Automatisch erstellt vom BreakPilot Compliance Advisor (Qwen)</p>
`
await fetch('/api/sdk/v1/agent/notify', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
recipient: 'dsb@breakpilot.local',
subject: `Compliance Advisor — ${qaPairs.length} Fragen (${currentStep})`,
body_html: bodyHtml,
role: 'Datenschutzbeauftragter',
}),
})
setEmailSent(true)
setTimeout(() => setEmailSent(false), 3000)
} catch (e) {
console.error('Email send failed:', e)
} finally {
setEmailSending(false)
}
}, [messages, emailSending, selectedCountry, currentStep])
const handleKeyDown = (e: React.KeyboardEvent) => {
if (e.key === 'Enter' && !e.shiftKey) {
e.preventDefault()
@@ -188,6 +236,31 @@ export function ComplianceAdvisorWidget({ currentStep = 'default' }: ComplianceA
</div>
</div>
<div className="flex items-center gap-1">
{/* Send as Email */}
{messages.length > 0 && (
<button
onClick={handleSendAsEmail}
disabled={emailSending}
className={`text-white/80 hover:text-white transition-colors ${emailSent ? 'text-green-300' : ''}`}
aria-label="Als Email an DSB senden"
title={emailSent ? 'Email gesendet!' : 'Beratungsprotokoll als Email senden'}
>
{emailSent ? (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M5 13l4 4L19 7" />
</svg>
) : emailSending ? (
<svg className="w-5 h-5 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 12h4z" />
</svg>
) : (
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" />
</svg>
)}
</button>
)}
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-white/80 hover:text-white transition-colors"
@@ -93,9 +93,17 @@ export function CookieBannerOverlay() {
return (
<>
<<<<<<< HEAD
{/* Non-blocking banner — no overlay, no pointer-events blocking */}
<div className="fixed bottom-0 left-16 xl:left-64 right-0 z-50 pointer-events-none">
<div className="max-w-3xl mx-auto m-4 bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden pointer-events-auto">
=======
{/* Overlay — leaves sidebar (left 64px/16px) accessible */}
<div className="fixed inset-0 ml-16 xl:ml-64 bg-black/30 z-[9998]" onClick={() => setIsOpen(false)} />
<div className="fixed bottom-0 left-0 right-0 z-[9999]">
<div className="max-w-3xl mx-auto m-4 bg-white rounded-2xl shadow-2xl border border-gray-200 overflow-hidden">
>>>>>>> feat/zeroclaw-compliance-agent
{/* Header with EWR toggle + close button */}
<div className="px-6 pt-5 pb-3">
@@ -7,6 +7,7 @@ import type { ProjectInfo } from '@/lib/sdk/types'
import { CreateProjectDialog, normalizeProject } from './CreateProjectDialog'
import { ProjectActionDialog } from './ProjectActionDialog'
import { ProjectCard } from './ProjectCard'
import { PresetSection } from '@/app/sdk/_components/PresetSection'
export function ProjectSelector() {
const router = useRouter()
@@ -152,6 +153,9 @@ export function ProjectSelector() {
</div>
)}
{/* Industry Presets — quick-start for new projects */}
{!loading && <PresetSection />}
{/* Archived Projects Section */}
{!loading && archivedProjects.length > 0 && (
<div className="mt-8">
@@ -38,6 +38,7 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
<AdditionalModuleItem href="/sdk/email-templates" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z" /></svg>} label="E-Mail-Templates" isActive={pathname === '/sdk/email-templates'} collapsed={collapsed} projectId={projectId} />
</div>
<<<<<<< HEAD
{/* Master Controls Browser */}
<AdditionalModuleItem
href="/sdk/master-controls"
@@ -53,6 +54,8 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
projectId={projectId}
/>
=======
>>>>>>> feat/zeroclaw-compliance-agent
{/* Maschinenrecht / CE */}
<div className="border-t border-gray-100 py-2">
{!collapsed && (
@@ -129,6 +132,7 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
Zusatzmodule
</div>
)}
<AdditionalModuleItem href="/sdk/rollenkonzept" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0zm6 3a2 2 0 11-4 0 2 2 0 014 0zM7 10a2 2 0 11-4 0 2 2 0 014 0z" /></svg>} label="Rollenkonzept" isActive={pathname?.startsWith('/sdk/rollenkonzept') ?? false} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/training" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 6.253v13m0-13C10.832 5.477 9.246 5 7.5 5S4.168 5.477 3 6.253v13C4.168 18.477 5.754 18 7.5 18s3.332.477 4.5 1.253m0-13C13.168 5.477 14.754 5 16.5 5c1.747 0 3.332.477 4.5 1.253v13C19.832 18.477 18.247 18 16.5 18c-1.746 0-3.332.477-4.5 1.253" /></svg>} label="Schulung (Admin)" isActive={pathname === '/sdk/training'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/training/learner" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M16 7a4 4 0 11-8 0 4 4 0 018 0zM12 14a7 7 0 00-7 7h14a7 7 0 00-7-7z" /></svg>} label="Schulung (Learner)" isActive={pathname === '/sdk/training/learner'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/rag" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" /></svg>} label="Legal RAG" isActive={pathname === '/sdk/rag'} collapsed={collapsed} projectId={projectId} />
@@ -143,7 +147,8 @@ export function SidebarModuleList({ collapsed, projectId, pendingCRCount }: Side
<AdditionalModuleItem href="/sdk/workshop" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M17 20h5v-2a3 3 0 00-5.356-1.857M17 20H7m10 0v-2c0-.656-.126-1.283-.356-1.857M7 20H2v-2a3 3 0 015.356-1.857M7 20v-2c0-.656.126-1.283.356-1.857m0 0a5.002 5.002 0 019.288 0M15 7a3 3 0 11-6 0 3 3 0 016 0z" /></svg>} label="Workshop" isActive={pathname === '/sdk/workshop'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/portfolio" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M19 11H5m14 0a2 2 0 012 2v6a2 2 0 01-2 2H5a2 2 0 01-2-2v-6a2 2 0 012-2m14 0V9a2 2 0 00-2-2M5 11V9a2 2 0 012-2m0 0V5a2 2 0 012-2h6a2 2 0 012 2v2M7 7h10" /></svg>} label="Portfolio" isActive={pathname === '/sdk/portfolio'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/roadmap" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 17V7m0 10a2 2 0 01-2 2H5a2 2 0 01-2-2V7a2 2 0 012-2h2a2 2 0 012 2m0 10a2 2 0 002 2h2a2 2 0 002-2M9 7a2 2 0 012-2h2a2 2 0 012 2m0 10V7m0 10a2 2 0 002 2h2a2 2 0 002-2V7a2 2 0 00-2-2h-2a2 2 0 00-2 2" /></svg>} label="Roadmap" isActive={pathname === '/sdk/roadmap'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/isms" icon={<svg className="w-5 h-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>} label="ISMS (ISO 27001)" isActive={pathname === '/sdk/isms'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/source-policy" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M21 12a9 9 0 01-9 9m9-9a9 9 0 00-9-9m9 9H3m9 9a9 9 0 01-9-9m9 9c1.657 0 3-4.03 3-9s-1.343-9-3-9m0 18c-1.657 0-3-4.03-3-9s1.343-9 3-9m-9 9a9 9 0 019-9" /></svg>} label="Quellen-Verwaltung" isActive={pathname?.startsWith('/sdk/source-policy') ?? false} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/isms" icon={<svg className="w-5 h-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>} label="ISMS Readiness" isActive={pathname === '/sdk/isms'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/audit-llm" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M9 19v-6a2 2 0 00-2-2H5a2 2 0 00-2 2v6a2 2 0 002 2h2a2 2 0 002-2zm0 0V9a2 2 0 012-2h2a2 2 0 012 2v10m-6 0a2 2 0 002 2h2a2 2 0 002-2m0 0V5a2 2 0 012-2h2a2 2 0 012 2v14a2 2 0 01-2 2h-2a2 2 0 01-2-2z" /></svg>} label="LLM Audit" isActive={pathname === '/sdk/audit-llm'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/rbac" icon={<svg className="w-5 h-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>} label="RBAC Admin" isActive={pathname === '/sdk/rbac'} collapsed={collapsed} projectId={projectId} />
<AdditionalModuleItem href="/sdk/catalog-manager" icon={<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M4 7v10c0 2.21 3.582 4 8 4s8-1.79 8-4V7M4 7c0 2.21 3.582 4 8 4s8-1.79 8-4M4 7c0-2.21 3.582-4 8-4s8 1.79 8 4m0 5c0 2.21-3.582 4-8 4s-8-1.79-8-4" /></svg>} label="Kataloge" isActive={pathname === '/sdk/catalog-manager'} collapsed={collapsed} projectId={projectId} />
@@ -5,6 +5,7 @@ import Link from 'next/link'
import { useRouter } from 'next/navigation'
import { useSDK, getStepById, getNextStep, getPreviousStep, SDK_STEPS } from '@/lib/sdk'
import { STEP_EXPLANATIONS } from './StepExplanations'
export { STEP_EXPLANATIONS }
// =============================================================================
// TYPES
@@ -110,6 +110,8 @@ interface RegulationsPanelProps {
supervisoryAuthorities?: SupervisoryAuthorityInfo[]
regulationAssessmentLoading?: boolean
onGoToObligations?: () => void
enabledModules?: string[]
onToggleModule?: (moduleId: string, enabled: boolean) => void
}
export function RegulationsPanel({
@@ -117,6 +119,8 @@ export function RegulationsPanel({
supervisoryAuthorities,
regulationAssessmentLoading,
onGoToObligations,
enabledModules,
onToggleModule,
}: RegulationsPanelProps) {
if (!applicableRegulations && !regulationAssessmentLoading) return null
return (
@@ -149,9 +153,22 @@ export function RegulationsPanel({
)}
</div>
</div>
<div className="text-right text-sm text-gray-600">
<span>{reg.obligation_count} Pflichten</span>
{reg.control_count > 0 && <span className="ml-2">{reg.control_count} Controls</span>}
<div className="flex items-center gap-3">
<div className="text-right text-sm text-gray-600">
<span>{reg.obligation_count} Pflichten</span>
{reg.control_count > 0 && <span className="ml-2">{reg.control_count} Controls</span>}
</div>
{onToggleModule && (
<label className="flex items-center gap-1.5 cursor-pointer">
<input
type="checkbox"
checked={enabledModules?.includes(reg.id) ?? true}
onChange={e => onToggleModule(reg.id, e.target.checked)}
className="w-4 h-4 text-purple-600 rounded"
/>
<span className="text-xs text-gray-500">Aktiv</span>
</label>
)}
</div>
</div>
))}
@@ -25,6 +25,8 @@ interface ScopeDecisionTabProps {
supervisoryAuthorities?: SupervisoryAuthorityInfo[]
regulationAssessmentLoading?: boolean
onGoToObligations?: () => void
enabledModules?: string[]
onToggleModule?: (moduleId: string, enabled: boolean) => void
}
export function ScopeDecisionTab({
@@ -38,6 +40,8 @@ export function ScopeDecisionTab({
supervisoryAuthorities,
regulationAssessmentLoading,
onGoToObligations,
enabledModules,
onToggleModule,
}: ScopeDecisionTabProps) {
const [expandedTrigger, setExpandedTrigger] = useState<number | null>(null)
const [showAuditTrail, setShowAuditTrail] = useState(false)
@@ -71,6 +75,8 @@ export function ScopeDecisionTab({
applicableRegulations={applicableRegulations}
supervisoryAuthorities={supervisoryAuthorities}
regulationAssessmentLoading={regulationAssessmentLoading}
enabledModules={enabledModules}
onToggleModule={onToggleModule}
onGoToObligations={onGoToObligations}
/>