df7d83134b
After a compliance-check run finishes, the user can now apply the
extracted vendor inventory directly to their own:
- CookieBanner config (admin /sdk/einwilligungen)
- Cookie-Policy / VVT-Register / Privacy-Policy templates
(admin /sdk/document-generator)
Backend:
- migration_to_banner.py: vendor list -> CookieBannerConfig with
ESSENTIAL/PERFORMANCE/PERSONALIZATION/EXTERNAL_MEDIA buckets +
review flags (broken opt-out URLs, missing expiry, no cookies listed)
- migration_to_document.py: vendor list -> pre-fills for 3 doc
templates, recipient-type aware (INTERNAL/GROUP/PROCESSOR/CONTROLLER)
- agent_migration_routes.py: GET /banner-preview, /document-preview,
/summary keyed on check_id
- compliance_audit_log: new check_payloads table persists cmp_vendors +
extracted_profile so the preview survives an app restart
- tests: 9 mapper units + 4 endpoint integration tests
Frontend:
- MigrationPanel.tsx: modal showing banner-config diff + document
pre-fills, plus links into the existing editors
- ComplianceCheckTab.tsx: replaces standalone audit link with the
panel; net -3 lines, stays at the 500-cap
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
195 lines
7.1 KiB
TypeScript
195 lines
7.1 KiB
TypeScript
'use client'
|
|
|
|
import { useState } from 'react'
|
|
|
|
interface BannerFlag {
|
|
level: 'ERROR' | 'WARNING' | 'INFO'
|
|
vendor: string
|
|
issue: string
|
|
message: string
|
|
}
|
|
|
|
interface BannerPreview {
|
|
config: { categories: { id: string; cookies: { name: string }[] }[] }
|
|
flags: BannerFlag[]
|
|
summary: {
|
|
vendors_total: number
|
|
vendors_with_no_cookies: number
|
|
cookies_total: number
|
|
categories: Record<string, number>
|
|
flags_error: number
|
|
flags_warning: number
|
|
flags_info: number
|
|
}
|
|
}
|
|
|
|
interface DocumentPreview {
|
|
check_id: string
|
|
vendor_count: number
|
|
templates: Record<string, {
|
|
templateType: string
|
|
initialContent: string
|
|
suggested_template_search?: string
|
|
}>
|
|
}
|
|
|
|
type Mode = 'banner' | 'documents'
|
|
|
|
export function MigrationPanel({ checkId }: { checkId: string }) {
|
|
const [open, setOpen] = useState(false)
|
|
const [mode, setMode] = useState<Mode>('banner')
|
|
const [loading, setLoading] = useState(false)
|
|
const [error, setError] = useState<string | null>(null)
|
|
const [banner, setBanner] = useState<BannerPreview | null>(null)
|
|
const [docs, setDocs] = useState<DocumentPreview | null>(null)
|
|
|
|
async function loadPreview(next: Mode) {
|
|
setMode(next)
|
|
setOpen(true)
|
|
setError(null)
|
|
setLoading(true)
|
|
try {
|
|
const path = next === 'banner'
|
|
? `/api/sdk/v1/agent/migration/${checkId}/banner-preview`
|
|
: `/api/sdk/v1/agent/migration/${checkId}/document-preview`
|
|
const r = await fetch(path)
|
|
if (!r.ok) throw new Error(`HTTP ${r.status}`)
|
|
const data = await r.json()
|
|
if (next === 'banner') setBanner(data)
|
|
else setDocs(data)
|
|
} catch (e) {
|
|
setError(e instanceof Error ? e.message : 'Preview-Ladefehler')
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
|
|
return (
|
|
<>
|
|
<div className="mt-3 flex items-center justify-between gap-3 flex-wrap">
|
|
<div className="flex items-center gap-2">
|
|
<button onClick={() => loadPreview('banner')}
|
|
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-purple-50 text-purple-700 border border-purple-200 hover:bg-purple-100">
|
|
Cookie-Banner uebernehmen
|
|
</button>
|
|
<button onClick={() => loadPreview('documents')}
|
|
className="px-3 py-1.5 text-xs font-medium rounded-lg bg-amber-50 text-amber-700 border border-amber-200 hover:bg-amber-100">
|
|
Dokumente vorbefuellen
|
|
</button>
|
|
</div>
|
|
<a href={`/sdk/agent/audit/${checkId}`} target="_blank" rel="noopener"
|
|
className="text-xs text-blue-700 hover:text-blue-900 underline">
|
|
Voll-Audit oeffnen (alle MCs) →
|
|
</a>
|
|
</div>
|
|
|
|
{open && (
|
|
<div className="fixed inset-0 z-50 bg-black/40 flex items-start justify-center p-6 overflow-y-auto">
|
|
<div className="bg-white rounded-2xl shadow-2xl w-full max-w-3xl p-6 mt-12">
|
|
<div className="flex items-center justify-between mb-4">
|
|
<h3 className="text-lg font-semibold text-gray-900">
|
|
{mode === 'banner' ? 'Cookie-Banner Migration' : 'Dokument-Vorbefuellung'}
|
|
</h3>
|
|
<button onClick={() => setOpen(false)}
|
|
className="text-gray-400 hover:text-gray-600 text-xl leading-none">×</button>
|
|
</div>
|
|
|
|
{loading && <div className="text-sm text-gray-500">Lade Preview ...</div>}
|
|
{error && <div className="text-sm text-red-600">Fehler: {error}</div>}
|
|
|
|
{!loading && !error && mode === 'banner' && banner && (
|
|
<BannerPreviewBody data={banner} />
|
|
)}
|
|
|
|
{!loading && !error && mode === 'documents' && docs && (
|
|
<DocumentPreviewBody data={docs} />
|
|
)}
|
|
|
|
<div className="mt-5 flex justify-end gap-2">
|
|
<button onClick={() => setOpen(false)}
|
|
className="px-3 py-1.5 text-sm rounded-lg border border-gray-200 hover:bg-gray-50">
|
|
Schliessen
|
|
</button>
|
|
<a href={mode === 'banner' ? '/sdk/einwilligungen' : '/sdk/document-generator'}
|
|
className="px-3 py-1.5 text-sm rounded-lg bg-purple-600 text-white hover:bg-purple-700">
|
|
Im Editor oeffnen
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</>
|
|
)
|
|
}
|
|
|
|
function BannerPreviewBody({ data }: { data: BannerPreview }) {
|
|
const { summary, flags, config } = data
|
|
return (
|
|
<div className="space-y-4 text-sm">
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<Stat label="Anbieter" value={summary.vendors_total} />
|
|
<Stat label="Cookies" value={summary.cookies_total} />
|
|
<Stat label="Kategorien" value={Object.values(summary.categories).filter(n => n > 0).length} />
|
|
</div>
|
|
<div className="grid grid-cols-3 gap-3">
|
|
<Stat label="Fehler" value={summary.flags_error} tone="red" />
|
|
<Stat label="Warnungen" value={summary.flags_warning} tone="amber" />
|
|
<Stat label="Hinweise" value={summary.flags_info} tone="gray" />
|
|
</div>
|
|
<div>
|
|
<h4 className="font-medium text-gray-700 mb-1">Kategorien</h4>
|
|
<ul className="text-xs text-gray-600 space-y-0.5">
|
|
{config.categories.map(c => (
|
|
<li key={c.id}>{c.id}: {c.cookies.length} Cookie(s)</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
{flags.length > 0 && (
|
|
<div>
|
|
<h4 className="font-medium text-gray-700 mb-1">Pruefpunkte</h4>
|
|
<ul className="text-xs space-y-0.5 max-h-48 overflow-y-auto">
|
|
{flags.map((f, i) => (
|
|
<li key={i} className={f.level === 'ERROR' ? 'text-red-700' : f.level === 'WARNING' ? 'text-amber-700' : 'text-gray-600'}>
|
|
[{f.level}] {f.vendor}: {f.message}
|
|
</li>
|
|
))}
|
|
</ul>
|
|
</div>
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function DocumentPreviewBody({ data }: { data: DocumentPreview }) {
|
|
return (
|
|
<div className="space-y-4 text-sm">
|
|
<div className="text-xs text-gray-600">
|
|
{data.vendor_count} Anbieter werden in {Object.keys(data.templates).length} Vorlagen eingespielt.
|
|
</div>
|
|
{Object.entries(data.templates).map(([key, tpl]) => (
|
|
<div key={key} className="border border-gray-200 rounded-lg p-3">
|
|
<div className="flex items-center justify-between mb-2">
|
|
<h4 className="font-medium text-gray-800">{tpl.templateType}</h4>
|
|
{tpl.suggested_template_search && (
|
|
<span className="text-xs text-gray-500">Vorschlag: {tpl.suggested_template_search}</span>
|
|
)}
|
|
</div>
|
|
<pre className="text-xs bg-gray-50 rounded p-2 max-h-48 overflow-auto whitespace-pre-wrap">
|
|
{tpl.initialContent.slice(0, 1200)}{tpl.initialContent.length > 1200 ? '\n…' : ''}
|
|
</pre>
|
|
</div>
|
|
))}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
function Stat({ label, value, tone = 'gray' }: { label: string; value: number; tone?: 'red' | 'amber' | 'gray' }) {
|
|
const color = tone === 'red' ? 'text-red-700' : tone === 'amber' ? 'text-amber-700' : 'text-gray-800'
|
|
return (
|
|
<div className="border border-gray-200 rounded-lg p-2 text-center">
|
|
<div className={`text-lg font-semibold ${color}`}>{value}</div>
|
|
<div className="text-xs text-gray-500">{label}</div>
|
|
</div>
|
|
)
|
|
}
|