'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 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 }: { v: SnapshotVendor; lib?: LibCategories }) { const [open, setOpen] = useState(false) const cookies = 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 Rolle Zweck Laufzeit
{c.name} {mm && ( → sollte: {CANON_LABEL[mm.actual]} )} {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 }: { snapshot: Snapshot; cookieCategories?: LibCategories }, ) { const vendors = snapshot.cmp_vendors || [] const [viewMode, setViewMode] = useState<'role' | 'category'>('role') 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).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) .sort(sortByScore), })) .filter(g => g.vendors.length > 0) }, [vendors, viewMode]) 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'} />
{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) => )}
))}
) }