diff --git a/admin-compliance/app/api/sdk/v1/agent/banner-check/route.ts b/admin-compliance/app/api/sdk/v1/agent/banner-check/route.ts new file mode 100644 index 0000000..3923956 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/banner-check/route.ts @@ -0,0 +1,42 @@ +/** + * Banner Check API Proxy โ€” calls consent-tester /scan endpoint + * + * POST /api/sdk/v1/agent/banner-check โ†’ runs 3-phase cookie banner test + */ + +import { NextRequest, NextResponse } from 'next/server' + +const BACKEND_URL = process.env.BACKEND_API_URL || 'http://backend-compliance:8002' + +export async function POST(request: NextRequest) { + try { + const body = await request.json() + const { url } = body + + if (!url) { + return NextResponse.json({ error: 'URL erforderlich' }, { status: 400 }) + } + + // Call backend which proxies to consent-tester + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/banner-check`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url }), + signal: AbortSignal.timeout(120000), // 2 min for Playwright + }) + + if (!response.ok) { + const errorText = await response.text() + return NextResponse.json( + { error: `Backend: ${response.status}`, detail: errorText }, + { status: response.status }, + ) + } + + const data = await response.json() + return NextResponse.json(data) + } catch (error) { + const msg = error instanceof Error ? error.message : 'Unknown error' + return NextResponse.json({ error: msg }, { status: 500 }) + } +} diff --git a/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx new file mode 100644 index 0000000..fb8d623 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx @@ -0,0 +1,232 @@ +'use client' + +import React, { useState } from 'react' + +interface BannerResult { + banner_detected: boolean + banner_provider: string + banner_text: string + banner_checks?: { + violations: { code: string; text: string; severity: string }[] + passes: { code: string; text: string }[] + } + phases?: { + before_consent: { cookies: number; scripts: number; violations: string[] } + after_reject: { cookies: number; scripts: number; violations: string[] } + after_accept: { cookies: number; scripts: number; violations: string[] } + } +} + +export function BannerCheckTab() { + const [url, setUrl] = useState('') + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState('') + const [error, setError] = useState(null) + const [result, setResult] = useState(null) + + const handleScan = async (e: React.FormEvent) => { + e.preventDefault() + if (!url.trim()) return + + setLoading(true) + setError(null) + setResult(null) + setProgress('Cookie-Banner wird analysiert...') + + try { + const res = await fetch('/api/sdk/v1/agent/banner-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: url.trim() }), + }) + if (!res.ok) throw new Error(`Fehler: ${res.status}`) + const data = await res.json() + + if (data.scan_id) { + // Async polling + let attempts = 0 + while (attempts < 60) { + await new Promise(r => setTimeout(r, 3000)) + const poll = await fetch(`/api/sdk/v1/agent/banner-check?scan_id=${data.scan_id}`) + if (!poll.ok) { attempts++; continue } + const pollData = await poll.json() + if (pollData.progress) setProgress(pollData.progress) + if (pollData.status === 'completed' && pollData.result) { + setResult(pollData.result) + break + } + if (pollData.status === 'failed') throw new Error(pollData.error || 'Scan fehlgeschlagen') + attempts++ + } + } else { + setResult(data) + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Unbekannter Fehler') + } finally { + setLoading(false) + setProgress('') + } + } + + const violations = result?.banner_checks?.violations || [] + const passes = result?.banner_checks?.passes || [] + const total = violations.length + passes.length + + return ( +
+
+

Cookie-Banner Compliance Check

+

+ Playwright-basierter 3-Phasen-Test: Vor Interaktion, nach Ablehnen, nach Akzeptieren. + Prueft Dark Patterns, Pre-Consent-Cookies, Farbkontrast, Klick-Paritaet und 20+ weitere Kriterien. +

+
+ +
+ setUrl(e.target.value)} + placeholder="https://www.example.com/" + 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={loading} required + /> + +
+ + {progress && ( +
+ + + + + {progress} +
+ )} + + {error && ( +
{error}
+ )} + + {result && ( +
+ {/* Header */} +
+
+
+
+ + {result.banner_detected ? '๐Ÿ›ก๏ธ' : 'โš ๏ธ'} + +
+

+ {result.banner_detected + ? `Banner erkannt: ${result.banner_provider || 'Unbekannter Anbieter'}` + : 'Kein Cookie-Banner erkannt'} +

+ {total > 0 && ( +

+ {passes.length}/{total} Pruefungen bestanden +

+ )} +
+
+
+ {total > 0 && ( +
+
+
+
+ + {Math.round(passes.length / total * 100)}% + +
+ )} +
+
+ + {/* 3-Phase Summary */} + {result.phases && ( +
+ {[ + { label: 'Vor Consent', data: result.phases.before_consent, icon: '๐Ÿ”’' }, + { label: 'Nach Ablehnen', data: result.phases.after_reject, icon: '๐Ÿšซ' }, + { label: 'Nach Akzeptieren', data: result.phases.after_accept, icon: 'โœ…' }, + ].map(phase => ( +
+
{phase.icon}
+
{phase.label}
+
+ {phase.data.cookies} Cookies, {phase.data.scripts} Scripts +
+ {phase.data.violations.length > 0 && ( +
+ {phase.data.violations.length} Verstoesse +
+ )} +
+ ))} +
+ )} + + {/* Violations */} + {violations.length > 0 && ( +
+

+ Verstoesse ({violations.length}) +

+
+ {violations.map((v, i) => ( +
+ + + +
+
{v.text}
+
{v.code} | {v.severity}
+
+
+ ))} +
+
+ )} + + {/* Passes */} + {passes.length > 0 && ( +
+

+ Bestanden ({passes.length}) +

+
+ {passes.map((p, i) => ( +
+ + + +
{p.text}
+
+ ))} +
+
+ )} + + {!result.banner_detected && violations.length === 0 && passes.length === 0 && ( +
+ Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach ยง25 TDDDG Pflicht. +
+ )} +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/page.tsx b/admin-compliance/app/sdk/agent/page.tsx index 0e7da98..6e35bcd 100644 --- a/admin-compliance/app/sdk/agent/page.tsx +++ b/admin-compliance/app/sdk/agent/page.tsx @@ -7,9 +7,10 @@ import { AnalysisHistory } from './_components/AnalysisHistory' import { FollowUpQuestions } from './_components/FollowUpQuestions' import { ScanResult } from './_components/ScanResult' import { DocCheckTab } from './_components/DocCheckTab' +import { BannerCheckTab } from './_components/BannerCheckTab' type AnalysisMode = 'pre_launch' | 'post_launch' -type AnalysisTab = 'quick' | 'scan' | 'doc-check' +type AnalysisTab = 'quick' | 'scan' | 'doc-check' | 'banner-check' const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [ { id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '๐Ÿ“‹' }, @@ -20,6 +21,7 @@ const TABS: { id: AnalysisTab; label: string; desc: string }[] = [ { id: 'quick', label: 'Schnellanalyse', desc: 'Einzelne Seite klassifizieren + bewerten' }, { id: 'scan', label: 'Website-Scan', desc: 'Mehrere Seiten scannen + Dienstleister abgleichen' }, { id: 'doc-check', label: 'Dokumenten-Pruefung', desc: 'Einzelne Dokumente gezielt pruefen' }, + { id: 'banner-check', label: 'Banner-Check', desc: 'Cookie-Banner auf DSGVO-Konformitaet testen' }, ] export default function AgentPage() { @@ -224,8 +226,11 @@ export default function AgentPage() { {/* Doc Check Tab โ€” own component */} {tab === 'doc-check' && } + {/* Banner Check Tab โ€” own component */} + {tab === 'banner-check' && } + {/* URL Input (quick + scan only) */} - {tab !== 'doc-check' &&
+ {(tab === 'quick' || tab === 'scan') && setUrl(e.target.value)} placeholder={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" diff --git a/backend-compliance/compliance/api/agent_doc_check_routes.py b/backend-compliance/compliance/api/agent_doc_check_routes.py index b149b20..03de912 100644 --- a/backend-compliance/compliance/api/agent_doc_check_routes.py +++ b/backend-compliance/compliance/api/agent_doc_check_routes.py @@ -92,6 +92,26 @@ class DocCheckStatusResponse(BaseModel): error: str = "" +class BannerCheckRequest(BaseModel): + url: str + + +@router.post("/banner-check") +async def run_banner_check(req: BannerCheckRequest): + """Run cookie banner compliance check via consent-tester.""" + try: + async with httpx.AsyncClient(timeout=120.0) as client: + resp = await client.post( + f"{CONSENT_TESTER_URL}/scan", + json={"url": req.url, "timeout_per_phase": 10}, + ) + if resp.status_code == 200: + return resp.json() + return {"error": f"Consent-Tester: HTTP {resp.status_code}"} + except Exception as e: + return {"error": str(e)[:200]} + + @router.post("/doc-check") async def start_doc_check(req: DocCheckRequest): """Start async multi-URL document check."""