Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/CookieLibraryPanel.tsx
T
Benjamin Admin b0115cb10b feat(cookie): 2. Sicht Banner-Kategorie + Fehl-Einsortierung
CookieResultView bekommt einen Umschalter [Rechtliche Rolle] ↔
[Banner-Kategorie] (Notwendig/Funktional/Statistik/Marketing). In beiden
Sichten zeigt jede Cookie-Zeile '→ sollte: Marketing', wenn die tatsächliche
Kategorie laut Library von der deklarierten abweicht (rot bei Tracker als
notwendig, § 25 TDDDG). Neue KPI 'Falsch einsortiert'. Backend liefert dazu
cookie_categories (name→actual_category) aus big_lib im cookie-check-Output;
Seite lädt cookie-check einmal und reicht es an beide Komponenten.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-06-11 10:33:33 +02:00

162 lines
6.2 KiB
TypeScript

'use client'
/**
* CookieLibraryPanel — Pro-Cookie-Abgleich gegen die Knowledge-Library:
* findet als „notwendig" deklarierte Tracker + fehlende Zwecke und zeigt je
* Befund die Abstellmaßnahme. Lädt aus dem Snapshot (kein Re-Crawl).
*/
import React, { useEffect, useState } from 'react'
export interface CookieFinding {
vendor: string
cookie: string
type: string
severity: string
declared: string
library_purpose: string
remediation: string
control?: { control_id?: string | null; regulation?: string; article?: string }
}
interface CheckData {
summary?: { checked?: number; in_library?: number; findings?: number }
findings?: CookieFinding[]
storage_inventory?: {
total?: number
by_type?: Record<string, number>
real_cookies?: number
other_storage?: number
}
drift?: {
declared_count?: number
browser_count?: number
high_findings?: number
low_findings?: number
}
}
const SEV_COLOR: Record<string, string> = {
HIGH: 'bg-red-100 text-red-700',
MEDIUM: 'bg-amber-100 text-amber-700',
LOW: 'bg-blue-100 text-blue-700',
}
const TYPE_LABEL: Record<string, string> = {
tracker_as_necessary: 'Tracker als „notwendig" deklariert',
missing_purpose: 'Zweck fehlt',
excessive_lifetime: 'Speicherdauer zu lang',
vague_duration: 'Speicherdauer nicht konkret',
missing_retention: 'Keine Speicherdauer/Löschfrist',
third_country: 'Drittland-Transfer',
eu_alternative: 'EU-Alternative verfügbar',
storage_transparency: 'Speichertyp nicht transparent',
}
const STORAGE_LABEL: Record<string, string> = {
cookie: 'Cookies', local_storage: 'Local Storage',
session_storage: 'Session Storage', indexeddb: 'IndexedDB',
framework_storage: 'Framework-Storage',
}
// Pure, testbar.
export function CookieFindingList({ data }: { data: CheckData }) {
const findings = data.findings || []
const s = data.summary || {}
const inv = data.storage_inventory
const drift = data.drift
const driftShown =
!!drift && ((drift.declared_count ?? 0) + (drift.browser_count ?? 0)) > 0
return (
<div className="border rounded-lg overflow-hidden">
{driftShown && (
<div className="px-4 py-2.5 bg-amber-50 border-b text-xs text-amber-900">
<span className="font-semibold">Richtlinie Realität:</span>{' '}
<strong>{drift!.declared_count ?? 0}</strong> in der Cookie-Richtlinie
dokumentiert · <strong>{drift!.browser_count ?? 0}</strong> im Browser geladen
{(drift!.high_findings ?? 0) > 0 && (
<> · <strong className="text-red-700">{drift!.high_findings} undokumentiert geladen</strong></>
)}
{(drift!.low_findings ?? 0) > 0 && (
<> · {drift!.low_findings} dokumentiert, aber nicht geladen</>
)}
</div>
)}
{inv && (inv.total ?? 0) > 0 && (
<div className="px-4 py-2.5 bg-blue-50 border-b text-xs text-blue-900">
<span className="font-semibold">Storage-Inventar:</span>{' '}
{inv.total} als Cookies" gelistet →{' '}
<strong>{inv.real_cookies} echte Cookies</strong>
{(inv.other_storage ?? 0) > 0 && (
<> + <strong className="text-amber-700">{inv.other_storage} andere Endgeräte-Speicher</strong></>
)}
{inv.by_type && (
<span className="text-blue-700 ml-1">
({Object.entries(inv.by_type)
.map(([k, n]) => `${n} ${STORAGE_LABEL[k] || k}`)
.join(' · ')})
</span>
)}
</div>
)}
<div className="px-4 py-2.5 bg-slate-50 border-b text-sm font-semibold text-gray-800">
Library-Abgleich — {findings.length} Befund{findings.length !== 1 ? 'e' : ''}
<span className="ml-2 text-xs font-normal text-gray-400">
{s.in_library ?? 0}/{s.checked ?? 0} Cookies in der Library erkannt
</span>
</div>
{findings.length === 0 ? (
<div className="px-4 py-3 text-sm text-green-700">
Keine Abweichungen gegen die Library.
</div>
) : (
<div className="divide-y divide-gray-100 max-h-96 overflow-auto">
{findings.map((f, i) => (
<div key={i} className="px-4 py-2.5 space-y-1">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-[10px] font-semibold px-1.5 py-0.5 rounded ${SEV_COLOR[f.severity] || 'bg-gray-100 text-gray-600'}`}>
{f.severity}
</span>
<code className="text-xs text-gray-700">{f.cookie}</code>
<span className="text-xs text-gray-400">· {f.vendor}</span>
<span className="text-[10px] text-gray-500 ml-auto">
{TYPE_LABEL[f.type] || f.type}
</span>
</div>
{f.library_purpose && (
<div className="text-xs text-gray-500">Library-Zweck: {f.library_purpose}</div>
)}
<div className="text-xs text-gray-700">{f.remediation}</div>
{f.control?.regulation && f.control.regulation !== '—' && (
<div className="text-[10px] text-gray-400">
Rechtsgrundlage: {f.control.regulation} {f.control.article}
{f.control.control_id && ` · Control ${f.control.control_id}`}
</div>
)}
</div>
))}
</div>
)}
</div>
)
}
export function CookieLibraryPanel(
{ snapshotId, data: provided }: { snapshotId: string; data?: CheckData },
) {
const [data, setData] = useState<CheckData | null>(provided ?? null)
const [loading, setLoading] = useState(!provided)
useEffect(() => {
if (provided) { setData(provided); setLoading(false); return }
let cancelled = false
fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/cookie-check`)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d) })
.catch(() => { if (!cancelled) setData({ findings: [] }) })
.finally(() => { if (!cancelled) setLoading(false) })
return () => { cancelled = true }
}, [snapshotId, provided])
if (loading) return <div className="text-xs text-gray-400">Library-Abgleich läuft</div>
return <CookieFindingList data={data || {}} />
}