From b7f9099ad9b8a1d87a10320842b62a171aaaf896 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 29 Apr 2026 12:38:15 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20Cookie-Test=20tab=20=E2=80=94=203-phase?= =?UTF-8?q?=20consent=20test=20UI=20+=20API=20proxy?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Third tab "Cookie-Test" in Compliance Agent: - Phase A: Before consent (tracking without permission) - Phase B: After rejection (CRITICAL if tracking persists) - Phase C: After acceptance (undocumented services) - CMP badge (Didomi, OneTrust, etc.) - Violation cards with severity badges and legal references Co-Authored-By: Claude Opus 4.6 (1M context) --- .../api/sdk/v1/agent/consent-test/route.ts | 37 ++++ .../agent/_components/ConsentTestResult.tsx | 166 ++++++++++++++++++ admin-compliance/app/sdk/agent/page.tsx | 47 ++++- 3 files changed, 241 insertions(+), 9 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/agent/consent-test/route.ts create mode 100644 admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx diff --git a/admin-compliance/app/api/sdk/v1/agent/consent-test/route.ts b/admin-compliance/app/api/sdk/v1/agent/consent-test/route.ts new file mode 100644 index 0000000..4440458 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/consent-test/route.ts @@ -0,0 +1,37 @@ +/** + * Consent Test API Proxy + * POST /api/sdk/v1/agent/consent-test → consent-tester:8094/scan + */ + +import { NextRequest, NextResponse } from 'next/server' + +const CONSENT_TESTER_URL = process.env.CONSENT_TESTER_URL || 'http://bp-compliance-consent-tester:8094' + +export async function POST(request: NextRequest) { + try { + const body = await request.text() + + const response = await fetch(`${CONSENT_TESTER_URL}/scan`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(180000), // 3 min — 3 browser phases + }) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json( + { error: `Consent-Tester: ${response.status}`, detail: errorText }, + { status: response.status } + ) + } + + return NextResponse.json(await response.json()) + } catch (error) { + console.error('Consent test proxy error:', error) + return NextResponse.json( + { error: 'Cookie-Test fehlgeschlagen oder Timeout' }, + { status: 503 } + ) + } +} diff --git a/admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx b/admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx new file mode 100644 index 0000000..57385ae --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/ConsentTestResult.tsx @@ -0,0 +1,166 @@ +'use client' + +import React from 'react' + +interface Violation { + service: string + severity: string + text: string + legal_ref: string +} + +interface PhaseData { + scripts: string[] + cookies: string[] + tracking_services?: string[] + new_tracking?: string[] + violations?: Violation[] + undocumented?: string[] +} + +interface ConsentData { + banner_detected: boolean + banner_provider: string + phases: { + before_consent: PhaseData + after_reject: PhaseData + after_accept: PhaseData + } + summary: { + critical: number + high: number + undocumented: number + total_violations: number + } +} + +const SEV = { + CRITICAL: { bg: 'bg-red-100 border-red-300', text: 'text-red-800', badge: 'bg-red-600' }, + HIGH: { bg: 'bg-orange-100 border-orange-300', text: 'text-orange-800', badge: 'bg-orange-500' }, +} + +function PhaseCard({ title, icon, data, type }: { + title: string; icon: string; data: PhaseData; type: 'before' | 'reject' | 'accept' +}) { + const violations = data.violations || [] + const tracking = data.tracking_services || data.new_tracking || [] + const undocumented = data.undocumented || [] + const hasProblem = violations.length > 0 || undocumented.length > 0 + + return ( +
+

+ {icon} {title} +

+ + {/* Violations */} + {violations.map((v, i) => ( +
+
+ + {v.severity} + + + {v.service} + +
+

{v.text}

+

{v.legal_ref}

+
+ ))} + + {/* Undocumented (Phase C only) */} + {undocumented.map((s, i) => ( +
+ ✗ {s} — nicht in Cookie-Policy dokumentiert +
+ ))} + + {/* Tracking services (no violations) */} + {violations.length === 0 && undocumented.length === 0 && tracking.length > 0 && ( +
+ {tracking.map((t, i) =>
✓ {t} — {type === 'accept' ? 'mit Consent OK' : 'erkannt'}
)} +
+ )} + + {violations.length === 0 && undocumented.length === 0 && tracking.length === 0 && ( +

✓ Keine Tracking-Dienste erkannt

+ )} + + {/* Cookie/Script count */} +
+ {data.scripts?.length || 0} Scripts + {data.cookies?.length || 0} Cookies +
+
+ ) +} + +export function ConsentTestResult({ data }: { data: ConsentData }) { + const s = data.summary + + return ( +
+ {/* Header */} +
+
+ + + Cookie-Banner: {data.banner_detected ? data.banner_provider : 'Nicht erkannt'} + +
+
+ {s.critical > 0 && ( + + {s.critical} Kritisch + + )} + {s.high > 0 && ( + + {s.high} Hoch + + )} + {s.total_violations === 0 && ( + + Keine Verstoesse + + )} +
+
+ + {/* Three Phases */} +
+ + {data.banner_detected && ( + <> + + + + )} +
+ + {/* No banner warning */} + {!data.banner_detected && ( +
+ Kein Cookie-Banner erkannt. Alle erkannten Tracking-Dienste laden ohne + Einwilligung — dies ist ein Verstoss gegen §25 TDDDG. +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/page.tsx b/admin-compliance/app/sdk/agent/page.tsx index 6c5391a..404fee3 100644 --- a/admin-compliance/app/sdk/agent/page.tsx +++ b/admin-compliance/app/sdk/agent/page.tsx @@ -6,9 +6,10 @@ import { AnalysisResult } from './_components/AnalysisResult' import { AnalysisHistory } from './_components/AnalysisHistory' import { FollowUpQuestions } from './_components/FollowUpQuestions' import { ScanResult } from './_components/ScanResult' +import { ConsentTestResult } from './_components/ConsentTestResult' type AnalysisMode = 'pre_launch' | 'post_launch' -type AnalysisTab = 'quick' | 'scan' +type AnalysisTab = 'quick' | 'scan' | 'consent' const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [ { id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '📋' }, @@ -17,7 +18,8 @@ const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = const TABS: { id: AnalysisTab; label: string; info: string }[] = [ { id: 'quick', label: 'Schnellanalyse', info: 'Analysiert nur die eingegebene URL. Fuer einen umfassenden Check nutzen Sie den Website-Scan.' }, - { id: 'scan', label: 'Website-Scan', info: 'Scannt automatisch 5-10 Unterseiten (Startseite, Datenschutz, Impressum, AGB, Cookies) und gleicht erkannte Dienste mit der Datenschutzerklaerung ab.' }, + { id: 'scan', label: 'Website-Scan', info: 'Scannt automatisch 5-10 Unterseiten und gleicht erkannte Dienste mit der Datenschutzerklaerung ab.' }, + { id: 'consent', label: 'Cookie-Test', info: 'Testet mit echtem Browser was VOR und NACH Cookie-Einwilligung geladen wird. Erkennt Verstoesse gegen §25 TDDDG.' }, ] export default function AgentPage() { @@ -28,6 +30,9 @@ export default function AgentPage() { const [scanError, setScanError] = useState(null) const [scanData, setScanData] = useState(null) const [scanHistory, setScanHistory] = useState([]) + const [consentLoading, setConsentLoading] = useState(false) + const [consentError, setConsentError] = useState(null) + const [consentData, setConsentData] = useState(null) const { analyze, answerFollowUp, loading, error, result, history } = useAgentAnalysis() const handleSubmit = async (e: React.FormEvent) => { @@ -36,7 +41,7 @@ export default function AgentPage() { if (tab === 'quick') { analyze(url.trim(), mode) - } else { + } else if (tab === 'scan') { setScanLoading(true) setScanError(null) setScanData(null) @@ -55,11 +60,28 @@ export default function AgentPage() { } finally { setScanLoading(false) } + } else { + setConsentLoading(true) + setConsentError(null) + setConsentData(null) + try { + const res = await fetch('/api/sdk/v1/agent/consent-test', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: url.trim() }), + }) + if (!res.ok) throw new Error(`Cookie-Test fehlgeschlagen: ${res.status}`) + setConsentData(await res.json()) + } catch (e) { + setConsentError(e instanceof Error ? e.message : 'Unbekannter Fehler') + } finally { + setConsentLoading(false) + } } } - const isLoading = tab === 'quick' ? loading : scanLoading - const currentError = tab === 'quick' ? error : scanError + const isLoading = tab === 'quick' ? loading : tab === 'scan' ? scanLoading : consentLoading + const currentError = tab === 'quick' ? error : tab === 'scan' ? scanError : consentError const currentTab = TABS.find(t => t.id === tab)! return ( @@ -105,7 +127,7 @@ export default function AgentPage() { {/* URL Input */}
setUrl(e.target.value)} - placeholder={tab === 'scan' ? 'https://www.example.com/' : 'https://example.com/datenschutz'} + placeholder={tab === 'consent' ? 'https://www.example.com/' : tab === 'scan' ? 'https://www.example.com/' : 'https://example.com/datenschutz'} className="flex-1 px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 focus:border-transparent text-sm" disabled={isLoading} required />
@@ -143,6 +165,13 @@ export default function AgentPage() { )} + {/* Consent Test Result */} + {tab === 'consent' && consentData && ( +
+ +
+ )} + {/* History */} {tab === 'quick' && ( { setUrl(r.url); analyze(r.url, mode) }} /> @@ -152,7 +181,7 @@ export default function AgentPage() {

Letzte Scans

{scanHistory.map((item, i) => ( -