'use client' /** * CookieResultView — strukturierte Cookie-/Vendor-Auswertung aus einem * gespeicherten Snapshot (cmp_vendors), OHNE Re-Crawl. * * Zwei Sichten (Umschalter): * - Rechtliche Rolle: Eigene / Auftragsverarbeiter / Joint Controller (VVT) * - Banner-Kategorie: Notwendig / Funktional / Statistik / Marketing — die im * Consent-Banner implementierte Einteilung. Pro Cookie wird die tatsächliche * Kategorie laut Library gegengeprüft → '→ sollte: Marketing' bei * Fehl-Einsortierung (Tracker als notwendig = § 25 TDDDG-relevant). */ import React, { useMemo, useState } from 'react' export interface SnapshotCookie { name: string expiry?: string purpose?: string is_third_party?: boolean functional_role?: string } export interface SnapshotVendor { name: string cookies?: SnapshotCookie[] category?: string country?: string recipient_type?: string compliance_score?: number compliance_flags?: string[] opt_out_ok?: boolean } interface Snapshot { id: string site_domain?: string created_at?: string cmp_vendors?: SnapshotVendor[] } // name_lower → tatsächliche Kategorie laut Library (aus /cookie-check). export type LibCategories = Record // name_lower → Speichertyp (cookie | local_storage | framework_storage | …). export type StorageTypes = Record const STORAGE_LABEL: Record = { cookie: 'Cookie', local_storage: 'Local Storage', session_storage: 'Session Storage', indexeddb: 'IndexedDB', framework_storage: 'Framework', } const STORAGE_COLOR: Record = { cookie: 'bg-gray-100 text-gray-500', local_storage: 'bg-purple-100 text-purple-700', session_storage: 'bg-indigo-100 text-indigo-700', indexeddb: 'bg-cyan-100 text-cyan-700', framework_storage: 'bg-orange-100 text-orange-700', } const STORAGE_ORDER = ['cookie', 'local_storage', 'session_storage', 'indexeddb', 'framework_storage'] function storageOf(name: string, st?: StorageTypes): string { return st?.[(name || '').toLowerCase()] || 'cookie' } const ROLE_LABEL: Record = { unknown: 'Unbekannt', ad_pixel: 'Werbe-Pixel', auth_token: 'Auth-Token', preference: 'Präferenz', visitor_id: 'Besucher-ID', consent_state: 'Consent', tracking: 'Tracking', } const CAT_COLOR: Record = { necessary: 'bg-green-100 text-green-700', functional: 'bg-blue-100 text-blue-700', statistics: 'bg-amber-100 text-amber-700', marketing: 'bg-red-100 text-red-700', } const EEA = new Set([ 'DE','FR','IE','NL','AT','BE','BG','HR','CY','CZ','DK','EE','FI','GR','HU', 'IT','LV','LT','LU','MT','PL','PT','RO','SK','SI','ES','SE','IS','LI','NO', ]) const GROUPS = [ { key: 'own', label: 'Eigene Verarbeitungen (VVT, Art. 30)', test: (r: string) => !r || r === 'INTERNAL' || r === 'GROUP' }, { key: 'proc', label: 'Auftragsverarbeiter (AVV, Art. 28)', test: (r: string) => r === 'PROCESSOR' }, { key: 'joint', label: 'Eigenverantwortliche Dritte / Joint Controller (Art. 26)', test: (r: string) => r === 'JOINT_CONTROLLER' || r === 'CONTROLLER' }, { key: 'other', label: 'Sonstige Empfänger', test: () => true }, ] // Banner-Kategorie-Sicht: kanonische Buckets + Labels. const CAT_CANON: Record = { necessary: 'necessary', essential: 'necessary', notwendig: 'necessary', essenziell: 'necessary', security: 'necessary', 'strictly necessary': 'necessary', functional: 'functional', funktional: 'functional', preferences: 'functional', preference: 'functional', präferenzen: 'functional', statistics: 'statistics', statistik: 'statistics', analytics: 'statistics', performance: 'statistics', marketing: 'marketing', targeting: 'marketing', advertising: 'marketing', werbung: 'marketing', social_media: 'marketing', social: 'marketing', ad: 'marketing', } const CANON_LABEL: Record = { necessary: 'Notwendig', functional: 'Funktional', statistics: 'Statistik', marketing: 'Marketing', unknown: '—', } const CATEGORY_GROUPS = [ { key: 'necessary', label: 'Notwendig (essenziell)' }, { key: 'functional', label: 'Funktional' }, { key: 'statistics', label: 'Statistik' }, { key: 'marketing', label: 'Marketing' }, { key: 'unknown', label: 'Ohne Kategorie' }, ] function canonCat(c?: string): string { return CAT_CANON[(c || '').toLowerCase().trim()] || 'unknown' } // Tatsächliche Kategorie laut Library vs. deklarierte Banner-Kategorie. function mismatch(name: string, declaredCanon: string, lib?: LibCategories) { const raw = lib?.[name.toLowerCase()] if (!raw) return null const actual = canonCat(raw) if (actual === 'unknown' || actual === declaredCanon) return null // severe: als notwendig deklariert, laut Library einwilligungspflichtig. const severe = declaredCanon === 'necessary' && (actual === 'marketing' || actual === 'statistics') return { actual, severe } } function scoreColor(s?: number): string { if (s == null) return 'text-gray-400' return s >= 80 ? 'text-green-700' : s >= 50 ? 'text-amber-700' : 'text-red-700' } function Tile({ label, value, tone }: { label: string; value: React.ReactNode; tone: string }) { return (
{value}
{label}
) } function VendorRow( { v, lib, st, sf }: { v: SnapshotVendor; lib?: LibCategories; st?: StorageTypes; sf: string }, ) { const [open, setOpen] = useState(false) const cookies = sf ? (v.cookies || []).filter(c => storageOf(c.name, st) === sf) : (v.cookies || []) const cat = (v.category || '').toLowerCase() const declaredCanon = canonCat(v.category) const drittland = !!v.country && !EEA.has((v.country || '').toUpperCase()) return (
{open && cookies.length > 0 && (
{cookies.map((c, i) => { const mm = mismatch(c.name, declaredCanon, lib) return ( ) })}
Cookie Speicher Rolle Zweck Laufzeit
{c.name} {mm && ( → sollte: {CANON_LABEL[mm.actual]} )} {(() => { const t = storageOf(c.name, st) return t !== 'cookie' ? ( {STORAGE_LABEL[t] || t} ) : Cookie })()} {c.functional_role && c.functional_role !== 'unknown' ? (ROLE_LABEL[c.functional_role] || c.functional_role) : } {c.purpose ? c.purpose : kein Zweck} {c.expiry || '—'}
)}
) } export function CookieResultView( { snapshot, cookieCategories, storageTypes }: { snapshot: Snapshot; cookieCategories?: LibCategories; storageTypes?: StorageTypes }, ) { const vendors = snapshot.cmp_vendors || [] const [viewMode, setViewMode] = useState<'role' | 'category'>('role') const [storageFilter, setStorageFilter] = useState('') // Speichertyp-Verteilung über alle Cookies (für die Filter-Chips + Zähler). const storagePresent = useMemo(() => { const counts: Record = {} for (const v of vendors) for (const c of v.cookies || []) { const t = storageOf(c.name, storageTypes) counts[t] = (counts[t] || 0) + 1 } return counts }, [vendors, storageTypes]) const matchesSF = (v: SnapshotVendor) => !storageFilter || (v.cookies || []).some(c => storageOf(c.name, storageTypes) === storageFilter) const stats = useMemo(() => { const cookies = vendors.reduce((n, v) => n + (v.cookies?.length || 0), 0) const marketing = vendors.filter(v => (v.category || '').toLowerCase() === 'marketing').length const drittland = vendors.filter(v => v.country && !EEA.has(v.country.toUpperCase())).length let misplaced = 0 for (const v of vendors) { const dc = canonCat(v.category) for (const c of v.cookies || []) { if (mismatch(c.name, dc, cookieCategories)?.severe) misplaced++ } } return { cookies, marketing, drittland, misplaced } }, [vendors, cookieCategories]) const grouped = useMemo(() => { const sortByScore = (a: SnapshotVendor, b: SnapshotVendor) => (a.compliance_score ?? 100) - (b.compliance_score ?? 100) if (viewMode === 'category') { return CATEGORY_GROUPS .map(g => ({ ...g, vendors: vendors.filter(v => canonCat(v.category) === g.key).filter(matchesSF).sort(sortByScore) })) .filter(g => g.vendors.length > 0) } return GROUPS .map(g => ({ ...g, vendors: vendors .filter(v => GROUPS.find(gg => gg.test((v.recipient_type || '').toUpperCase()))?.key === g.key) .filter(matchesSF) .sort(sortByScore), })) .filter(g => g.vendors.length > 0) }, [vendors, viewMode, storageFilter, storageTypes]) const toggleBtn = (mode: 'role' | 'category', label: string) => ( ) return (

Cookie-Auswertung — {snapshot.site_domain || 'Snapshot'}

aus gespeichertem Snapshot (kein Re-Crawl) ·{' '} {snapshot.created_at ? snapshot.created_at.slice(0, 19).replace('T', ' ') : ''}

Gruppierung: {toggleBtn('role', 'Rechtliche Rolle')} {toggleBtn('category', 'Banner-Kategorie')}
0 ? 'text-red-700' : 'text-gray-800'} /> 0 ? 'text-amber-700' : 'text-gray-800'} /> 0 ? 'text-red-700' : 'text-gray-800'} />
{Object.keys(storagePresent).filter(t => t !== 'cookie').length > 0 && (
Speichertyp: {STORAGE_ORDER.filter(t => storagePresent[t]).map(t => ( ))}
)} {viewMode === 'category' && (

Banner-Kategorie wie im Consent-Tool deklariert. Badge{' '} → sollte: …{' '} zeigt die tatsächliche Kategorie laut Library (Fehl-Einsortierung).

)} {grouped.map(g => (
{g.label} ({g.vendors.length})
{g.vendors.map((v, i) => )}
))}
) }