'use client' /** * BrowserBehaviorView — On-demand-Browser-Verhaltens-Matrix für einen Snapshot. * Lädt das gespeicherte Ergebnis (GET, kein Re-Crawl); ohne Ergebnis ein * „Browser-Test starten"-Button (POST run → Live-Lauf je Engine). Zeigt je * Browser: Cookies vor Consent / nach Ablehnen / Ablehnen respektiert + Score, * darunter Engine-Detail mit Banner-Screenshot + Oberflächen-Befunden. * Aggregierte Maßnahmen + Cross-Finding folgen separat (Phase 4). */ import React, { useEffect, useState } from 'react' type Finding = { text: string; severity: string; legal_ref?: string; service?: string } type Surface = { has_impressum_link?: boolean; has_dse_link?: boolean; banner_text_issues?: number } type Violations = { before_consent?: number; after_reject?: number; banner_text?: number } type Summary = { cookies_before_consent?: number; cookies_after_reject?: number reject_respected?: boolean; banner_detected?: boolean; banner_provider?: string banner_screenshot_b64?: string; surface?: Surface; banner_findings?: Finding[] violations?: Violations } type Row = { profile_id: string; label: string; engine?: string; is_mobile?: boolean score?: number; verbal?: string; summary?: Summary | null; error?: string } type CrossFinding = { title: string; detail?: string; severity: string; affected?: string[]; measure?: string } type Matrix = { browser_matrix?: Row[]; aggregate?: Record url?: string; scanned_at?: string; cross_findings?: CrossFinding[] } const sevCls = (s: string) => { const u = (s || '').toUpperCase() if (u === 'CRITICAL' || u === 'HIGH') return 'bg-red-100 text-red-700' if (u === 'MEDIUM') return 'bg-amber-100 text-amber-700' return 'bg-gray-100 text-gray-600' } const scoreCls = (n?: number) => n == null ? 'text-gray-400' : n >= 80 ? 'text-green-700' : n >= 60 ? 'text-amber-700' : 'text-red-700' export function BrowserBehaviorView({ snapshotId }: { snapshotId: string }) { const [matrix, setMatrix] = useState(null) const [loading, setLoading] = useState(true) const [running, setRunning] = useState(false) const [error, setError] = useState(null) const [sel, setSel] = useState('') useEffect(() => { let cancelled = false fetch(`/api/sdk/v1/agent/snapshots/${snapshotId}/browser-behavior`) .then(r => r.json()) .then(d => { if (!cancelled) setMatrix(d?.browser_matrix || null) }) .catch(() => { if (!cancelled) setMatrix(null) }) .finally(() => { if (!cancelled) setLoading(false) }) return () => { cancelled = true } }, [snapshotId]) const rows = matrix?.browser_matrix || [] useEffect(() => { if (!sel && rows.length) { const withData = rows.filter(r => r.summary) const worst = [...(withData.length ? withData : rows)] .sort((a, b) => (a.score ?? 999) - (b.score ?? 999))[0] if (worst) setSel(worst.profile_id) } }, [rows, sel]) const run = async () => { setRunning(true); setError(null) try { const r = await fetch( `/api/sdk/v1/agent/snapshots/${snapshotId}/browser-behavior/run`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: '{}' }) const d = await r.json() if (!r.ok || d?.error) setError(d?.error || `Fehler ${r.status}`) else { setMatrix(d); setSel('') } } catch (e) { setError(String(e)) } finally { setRunning(false) } } if (loading) return
Lade Browser-Verhalten…
if (!matrix || !rows.length) { return (

Browser-Verhalten testen

Prüft das Cookie-Banner live in mehreren Browser-Engines (Chromium, Firefox/Gecko, Safari/WebKit) sowie – sofern verfügbar – in echtem Chrome, Edge, Brave und mobil. Gemessen wird je Browser: werden Cookies vor der Einwilligung gesetzt, und werden sie nach „Ablehnen" wirklich entfernt? Dazu eine Oberflächenanalyse (Impressum-/DSE-Links, Banner-Auffälligkeiten) mit Screenshot je Engine.

Der Test crawlt die Seite live und dauert je nach Browser-Anzahl einige Minuten.

{error &&
{error}
}
) } const selRow = rows.find(r => r.profile_id === sel) || rows[0] const agg: Record = matrix.aggregate || {} return (
{matrix.scanned_at ? `Test vom ${String(matrix.scanned_at).slice(0, 16).replace('T', ' ')}` : ''} {agg.profiles_run ? ` · ${String(agg.profiles_run)} Browser` : ''} {' · '}Live-Messung, kann von der Snapshot-Zeit abweichen
{error &&
{error}
} {/* Cross-Browser-Befunde — der Mehrwert ggü. Einzel-Browser-Scan */} {(matrix.cross_findings?.length ?? 0) > 0 && (

Cross-Browser-Befunde

{matrix.cross_findings!.map((f, i) => (
{f.severity} {f.title}
{f.detail &&

{f.detail}

} {(f.affected?.length ?? 0) > 0 && (
{f.affected!.map((a, j) => ( {a} ))}
)} {f.measure &&

Maßnahme: {f.measure}

}
))}
)}
{rows.map(r => { const s = r.summary const before = s?.cookies_before_consent ?? null const after = s?.cookies_after_reject ?? null const trackBefore = s?.violations?.before_consent ?? 0 const sld = r.profile_id === sel return ( setSel(r.profile_id)} className={`border-t border-gray-100 cursor-pointer ${sld ? 'bg-blue-50' : 'hover:bg-gray-50'}`}> {r.error || !s ? ( ) : ( <> )} ) })}
Browser Cookies vor Consent Cookies nach Ablehnen Ablehnen respektiert Oberfläche Score
{r.label} {r.is_mobile && Mobil} nicht verfügbar{r.error ? ` (${r.error.slice(0, 40)})` : ''} 0 ? 'text-red-700 font-semibold' : 'text-gray-500'}`} title={trackBefore > 0 ? `${trackBefore} davon Tracking (Verstoß)` : 'kein Tracking vor Consent'}> {before}{trackBefore > 0 ? ` · ${trackBefore}⚠` : ''} {after} {s.reject_respected ? : } {!s.surface?.has_impressum_link && Impressum fehlt } {!s.surface?.has_dse_link && DSE fehlt } {(s.surface?.banner_text_issues ?? 0) > 0 ? {s.surface?.banner_text_issues} Hinweis(e) : (s.surface?.has_impressum_link && s.surface?.has_dse_link ? ok : null)} {r.score ?? '–'}

„Cookies vor Consent" ist die Rohzahl — technisch notwendige Cookies (inkl. des Consent-Cookies, das die Ablehnung speichert) sind nach § 25 Abs. 2 TDDDG erlaubt. Rot/⚠ markiert nur den einwilligungs­pflichtigen Tracking-Anteil. Das Verdikt zu „Ablehnen" trägt die Spalte rechts.

{selRow && (

{selRow.label}

{selRow.verbal && · {selRow.verbal}}
{selRow.summary?.banner_screenshot_b64 ? ( {`Banner ) : (
Kein Banner-Screenshot erfasst.
)} {(selRow.summary?.banner_findings?.length ?? 0) > 0 ? (
    {selRow.summary!.banner_findings!.map((f, i) => (
  • {f.severity || 'INFO'} {f.text}{f.legal_ref && · {f.legal_ref}}
  • ))}
) : selRow.summary ? (
Keine Oberflächen-Auffälligkeiten in dieser Engine.
) : null}
)}
) }