From b53b36fdc553090c48ff0ada1f9bb699a36efa66 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 29 Apr 2026 16:43:08 +0200 Subject: [PATCH] =?UTF-8?q?feat:=205-tab=20agent=20UI=20=E2=80=94=20PDF=20?= =?UTF-8?q?export,=20compare,=20auth=20test,=20all=20proxies?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 5 tabs: Schnellanalyse, Website-Scan, Cookie-Test, Vergleich, Login-Test - PDF download button in ScanResult - CompareResult: side-by-side compliance comparison table - AuthTestResult: 5 post-login checks with legal refs - API proxies: /scans/pdf, /compare, /authenticated-scan - Compare: textarea for 2-5 URLs, parallel scanning Co-Authored-By: Claude Opus 4.6 (1M context) --- .../sdk/v1/agent/authenticated-scan/route.ts | 20 ++ .../app/api/sdk/v1/agent/compare/route.ts | 20 ++ .../app/api/sdk/v1/agent/scans/pdf/route.ts | 36 +++ .../sdk/agent/_components/AuthTestResult.tsx | 73 ++++++ .../sdk/agent/_components/CompareResult.tsx | 96 ++++++++ .../app/sdk/agent/_components/ScanResult.tsx | 29 +++ admin-compliance/app/sdk/agent/page.tsx | 214 +++++++++--------- 7 files changed, 383 insertions(+), 105 deletions(-) create mode 100644 admin-compliance/app/api/sdk/v1/agent/authenticated-scan/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/agent/compare/route.ts create mode 100644 admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts create mode 100644 admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx create mode 100644 admin-compliance/app/sdk/agent/_components/CompareResult.tsx diff --git a/admin-compliance/app/api/sdk/v1/agent/authenticated-scan/route.ts b/admin-compliance/app/api/sdk/v1/agent/authenticated-scan/route.ts new file mode 100644 index 0000000..6094940 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/authenticated-scan/route.ts @@ -0,0 +1,20 @@ +import { NextRequest, NextResponse } from 'next/server' +const CONSENT_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_URL}/authenticated-scan`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(120000), + }) + if (!response.ok) { + return NextResponse.json({ error: `Auth-Test: ${response.status}` }, { status: response.status }) + } + return NextResponse.json(await response.json()) + } catch (error) { + return NextResponse.json({ error: 'Auth-Test fehlgeschlagen' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/compare/route.ts b/admin-compliance/app/api/sdk/v1/agent/compare/route.ts new file mode 100644 index 0000000..7db5f22 --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/compare/route.ts @@ -0,0 +1,20 @@ +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.text() + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/compare`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(300000), + }) + if (!response.ok) { + return NextResponse.json({ error: `Backend: ${response.status}` }, { status: response.status }) + } + return NextResponse.json(await response.json()) + } catch (error) { + return NextResponse.json({ error: 'Vergleich fehlgeschlagen' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts b/admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts new file mode 100644 index 0000000..eabc01b --- /dev/null +++ b/admin-compliance/app/api/sdk/v1/agent/scans/pdf/route.ts @@ -0,0 +1,36 @@ +/** + * PDF Export Proxy + * POST /api/sdk/v1/agent/scans/pdf โ†’ backend /api/compliance/agent/scans/pdf + */ + +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.text() + + const response = await fetch(`${BACKEND_URL}/api/compliance/agent/scans/pdf`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + signal: AbortSignal.timeout(30000), + }) + + if (!response.ok) { + return NextResponse.json({ error: 'PDF generation failed' }, { status: response.status }) + } + + const pdfBytes = await response.arrayBuffer() + return new NextResponse(pdfBytes, { + headers: { + 'Content-Type': 'application/pdf', + 'Content-Disposition': 'attachment; filename="compliance-report.pdf"', + }, + }) + } catch (error) { + console.error('PDF proxy error:', error) + return NextResponse.json({ error: 'PDF generation failed' }, { status: 503 }) + } +} diff --git a/admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx b/admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx new file mode 100644 index 0000000..10b364f --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/AuthTestResult.tsx @@ -0,0 +1,73 @@ +'use client' + +import React from 'react' + +interface AuthCheck { + found: boolean + text: string + legal_ref: string +} + +interface AuthData { + url: string + authenticated: boolean + login_error: string + checks: Record + findings_count: number +} + +const CHECK_LABELS: Record = { + cancel_subscription: { label: 'Kuendigungsbutton (2 Klicks)', icon: '๐Ÿšซ' }, + delete_account: { label: 'Konto loeschen', icon: '๐Ÿ—‘๏ธ' }, + export_data: { label: 'Daten exportieren', icon: '๐Ÿ“ฅ' }, + consent_settings: { label: 'Einwilligungen widerrufen', icon: 'โš™๏ธ' }, + profile_visible: { label: 'Profildaten einsehen', icon: '๐Ÿ‘ค' }, +} + +export function AuthTestResult({ data }: { data: AuthData }) { + if (!data.authenticated) { + return ( +
+

Login fehlgeschlagen

+

{data.login_error || 'Credentials oder Formular nicht erkannt'}

+
+ ) + } + + return ( +
+
+ + Erfolgreich eingeloggt + 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}> + {data.findings_count} fehlende Funktionen + +
+ +
+ {Object.entries(data.checks).map(([key, check]) => { + const info = CHECK_LABELS[key] || { label: key, icon: 'โ“' } + return ( +
+ {info.icon} +
+

+ {check.found ? 'โœ“' : 'โœ—'} {info.label} +

+ {check.text &&

{check.text}

} +
+ {check.legal_ref} +
+ ) + })} +
+ + {data.findings_count > 0 && ( +
+ {data.findings_count} Pflichtfunktion(en) fehlen. Der Nutzer kann seine Rechte + nach DSGVO nicht vollstaendig ausueben. +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/CompareResult.tsx b/admin-compliance/app/sdk/agent/_components/CompareResult.tsx new file mode 100644 index 0000000..8f23b00 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/CompareResult.tsx @@ -0,0 +1,96 @@ +'use client' + +import React from 'react' + +interface SiteResult { + url: string + domain: string + risk_level: string + risk_score: number + findings_count: number + services_count: number + has_impressum: boolean + has_datenschutz: boolean + has_cookie_banner: boolean + has_google_fonts: boolean + scan_status: string +} + +const RISK_COLOR: Record = { + MINIMAL: 'text-green-700 bg-green-50', + LOW: 'text-yellow-700 bg-yellow-50', + LIMITED: 'text-orange-700 bg-orange-50', + HIGH: 'text-red-700 bg-red-50', + UNACCEPTABLE: 'text-red-900 bg-red-100', +} + +export function CompareResult({ sites }: { sites: SiteResult[] }) { + if (!sites.length) return null + + const checks = [ + { key: 'has_datenschutz', label: 'Datenschutzerklaerung' }, + { key: 'has_impressum', label: 'Impressum' }, + { key: 'has_cookie_banner', label: 'Cookie-Banner' }, + { key: 'has_google_fonts', label: 'Google Fonts (lokal?)' }, + ] + + return ( +
+
+ + + + + {sites.map((s, i) => ( + + ))} + + + + + + {sites.map((s, i) => ( + + ))} + + + + {sites.map((s, i) => ( + + ))} + + + + {sites.map((s, i) => ( + + ))} + + {checks.map(check => ( + + + {sites.map((s, i) => { + const val = (s as any)[check.key] + const isInverted = check.key === 'has_google_fonts' + const good = isInverted ? !val : val + return ( + + ) + })} + + ))} + +
Pruefung + {s.domain} +
Risiko-Score + + {s.risk_level || '?'} ({s.risk_score}/100) + +
Findings 0 ? 'text-red-700' : 'text-green-700'}`}> + {s.findings_count} +
Dienste erkannt{s.services_count}
{check.label} + {good ? 'โœ“' : 'โœ—'} +
+
+
+ ) +} diff --git a/admin-compliance/app/sdk/agent/_components/ScanResult.tsx b/admin-compliance/app/sdk/agent/_components/ScanResult.tsx index 0bf9664..e187590 100644 --- a/admin-compliance/app/sdk/agent/_components/ScanResult.tsx +++ b/admin-compliance/app/sdk/agent/_components/ScanResult.tsx @@ -207,6 +207,35 @@ export function ScanResult({ data }: { data: ScanData }) { )} + {/* PDF Export Button */} +
+ +
) } diff --git a/admin-compliance/app/sdk/agent/page.tsx b/admin-compliance/app/sdk/agent/page.tsx index 404fee3..ff88614 100644 --- a/admin-compliance/app/sdk/agent/page.tsx +++ b/admin-compliance/app/sdk/agent/page.tsx @@ -7,82 +7,93 @@ import { AnalysisHistory } from './_components/AnalysisHistory' import { FollowUpQuestions } from './_components/FollowUpQuestions' import { ScanResult } from './_components/ScanResult' import { ConsentTestResult } from './_components/ConsentTestResult' +import { CompareResult } from './_components/CompareResult' +import { AuthTestResult } from './_components/AuthTestResult' -type AnalysisMode = 'pre_launch' | 'post_launch' -type AnalysisTab = 'quick' | 'scan' | 'consent' +type Mode = 'pre_launch' | 'post_launch' +type Tab = 'quick' | 'scan' | 'consent' | 'compare' | 'auth' -const MODES: { id: AnalysisMode; label: string; desc: string; icon: string }[] = [ - { id: 'pre_launch', label: 'Internes Dokument', desc: 'Vor Veroeffentlichung pruefen', icon: '๐Ÿ“‹' }, - { id: 'post_launch', label: 'Live-Website', desc: 'Bereits online analysieren', icon: '๐ŸŒ' }, +const MODES = [ + { id: 'pre_launch' as Mode, label: 'Internes Dokument', desc: 'Vor Veroeffentlichung', icon: '๐Ÿ“‹' }, + { id: 'post_launch' as Mode, label: 'Live-Website', desc: 'Bereits online', icon: '๐ŸŒ' }, ] -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 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.' }, +const TABS = [ + { id: 'quick' as Tab, label: 'Schnellanalyse', info: 'Einzelne URL klassifizieren und bewerten.' }, + { id: 'scan' as Tab, label: 'Website-Scan', info: '5-10 Seiten scannen, Dienstleister abgleichen, Pflichtinhalte pruefen.' }, + { id: 'consent' as Tab, label: 'Cookie-Test', info: 'Testet mit Browser was VOR und NACH Cookie-Einwilligung geladen wird.' }, + { id: 'compare' as Tab, label: 'Vergleich', info: '2-5 Websites parallel scannen und Compliance vergleichen.' }, + { id: 'auth' as Tab, label: 'Login-Test', info: 'Nach Login pruefen: Kuendigung, Daten loeschen, Export, Einwilligungen.' }, ] export default function AgentPage() { const [url, setUrl] = useState('') - const [mode, setMode] = useState('post_launch') - const [tab, setTab] = useState('quick') + const [urls, setUrls] = useState('') + const [mode, setMode] = useState('post_launch') + const [tab, setTab] = useState('quick') const [scanLoading, setScanLoading] = useState(false) 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 [compareData, setCompareData] = useState(null) + const [authData, setAuthData] = useState(null) + const [authUser, setAuthUser] = useState('') + const [authPass, setAuthPass] = useState('') const { analyze, answerFollowUp, loading, error, result, history } = useAgentAnalysis() const handleSubmit = async (e: React.FormEvent) => { e.preventDefault() - if (!url.trim()) return + setScanLoading(true) + setScanError(null) - if (tab === 'quick') { - analyze(url.trim(), mode) - } else if (tab === 'scan') { - setScanLoading(true) - setScanError(null) - setScanData(null) - try { - const res = await fetch('/api/sdk/v1/agent/scan', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: url.trim(), mode }), - }) - if (!res.ok) throw new Error(`Scan fehlgeschlagen: ${res.status}`) - const data = await res.json() + try { + if (tab === 'quick') { + setScanLoading(false) + analyze(url.trim(), mode) + return + } + + let endpoint = '' + let body: any = {} + + if (tab === 'scan') { + endpoint = '/api/sdk/v1/agent/scan' + body = { url: url.trim(), mode } + } else if (tab === 'consent') { + endpoint = '/api/sdk/v1/agent/consent-test' + body = { url: url.trim() } + } else if (tab === 'compare') { + endpoint = '/api/sdk/v1/agent/compare' + body = { urls: urls.split('\n').map(u => u.trim()).filter(Boolean), mode } + } else if (tab === 'auth') { + endpoint = '/api/sdk/v1/agent/authenticated-scan' + body = { url: url.trim(), username: authUser, password: authPass } + } + + const res = await fetch(endpoint, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify(body), + }) + if (!res.ok) throw new Error(`Fehlgeschlagen: ${res.status}`) + const data = await res.json() + + if (tab === 'scan') { setScanData(data) setScanHistory(prev => [{ url: url.trim(), ...data, scanned_at: new Date().toISOString() }, ...prev].slice(0, 20)) - } catch (e) { - setScanError(e instanceof Error ? e.message : 'Unbekannter Fehler') - } 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) - } + } else if (tab === 'consent') setConsentData(data) + else if (tab === 'compare') setCompareData(data) + else if (tab === 'auth') setAuthData(data) + } catch (e) { + setScanError(e instanceof Error ? e.message : 'Fehler') + } finally { + setScanLoading(false) } } - 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)! + const isLoading = tab === 'quick' ? loading : scanLoading + const currentError = tab === 'quick' ? error : scanError return (
@@ -91,12 +102,11 @@ export default function AgentPage() {

Analysiere Dokumente und Webseiten auf DSGVO-Konformitaet.

- {/* Mode Selection */} + {/* Mode */}
{MODES.map(m => ( ))}
-

{currentTab.info}

+

{TABS.find(t => t.id === tab)?.info}

- {/* URL Input */} -
- setUrl(e.target.value)} - 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 /> -