From 05d98ea95fdfb39cd2c18c91cd6d53ded8d6fec7 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sun, 10 May 2026 09:09:27 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20New=20tab=20structure=20=E2=80=94=20Dis?= =?UTF-8?q?covery=20Scan,=20Doc-Check,=20Banner,=20Impressum?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed Schnellanalyse tab. New 4-tab structure: 1. Website-Scan (Discovery): Finds legal documents + services, shows "Jetzt pruefen" buttons that navigate to specialized tabs with pre-filled URLs. 2. Dokumenten-Pruefung: DSI, AGB, Cookie, Widerruf checks (existing) 3. Banner-Check: Cookie banner 46-check deep verification (existing) 4. Impressum-Check (NEW): §5 TMG / §18 MStV with 16 checks, own tab with URL input, history, email report. Uses existing impressum_checks.py via doc-check endpoint. Tab cross-navigation: Scan → "Jetzt pruefen" → opens target tab with URL pre-filled via localStorage handoff. Removed: Mode selector (pre/post launch), Schnellanalyse, useAgentAnalysis hook import, AnalysisResult/FollowUpQuestions. Co-Authored-By: Claude Opus 4.6 (1M context) --- .../agent/_components/ImpressumCheckTab.tsx | 168 ++++++++ admin-compliance/app/sdk/agent/page.tsx | 378 +++++++++--------- 2 files changed, 346 insertions(+), 200 deletions(-) create mode 100644 admin-compliance/app/sdk/agent/_components/ImpressumCheckTab.tsx diff --git a/admin-compliance/app/sdk/agent/_components/ImpressumCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/ImpressumCheckTab.tsx new file mode 100644 index 0000000..ec401f4 --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/ImpressumCheckTab.tsx @@ -0,0 +1,168 @@ +'use client' + +import React, { useState } from 'react' +import { ChecklistView } from './ChecklistView' + +interface CheckItem { + id: string; label: string; passed: boolean; severity: string + matched_text: string; level?: number; parent?: string | null + skipped?: boolean; hint?: string +} + +export function ImpressumCheckTab() { + const [url, setUrl] = useState(() => + typeof window !== 'undefined' ? localStorage.getItem('impressum-check-url') || '' : '' + ) + const [loading, setLoading] = useState(false) + const [progress, setProgress] = useState('') + const [error, setError] = useState(null) + const [results, setResults] = useState(() => { + if (typeof window === 'undefined') return null + try { const s = localStorage.getItem('impressum-check-results'); return s ? JSON.parse(s) : null } catch { return null } + }) + const [history, setHistory] = useState<{ url: string; date: string; findings: number; pct: number; resultKey: string }[]>(() => { + if (typeof window === 'undefined') return [] + try { return JSON.parse(localStorage.getItem('impressum-check-history') || '[]') } catch { return [] } + }) + + React.useEffect(() => { localStorage.setItem('impressum-check-url', url) }, [url]) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + if (!url.trim()) return + + setLoading(true) + setError(null) + setResults(null) + setProgress('Impressum wird geprueft...') + + try { + const startRes = await fetch('/api/sdk/v1/agent/doc-check', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + entries: [{ doc_type: 'impressum', label: 'Impressum', url: url.trim() }], + recipient: 'dsb@breakpilot.local', + }), + }) + if (!startRes.ok) throw new Error(`Fehler: ${startRes.status}`) + const { check_id } = await startRes.json() + if (!check_id) throw new Error('Keine Check-ID erhalten') + + let attempts = 0 + while (attempts < 120) { + await new Promise(r => setTimeout(r, 3000)) + const pollRes = await fetch(`/api/sdk/v1/agent/doc-check?check_id=${check_id}`) + if (!pollRes.ok) { attempts++; continue } + const pollData = await pollRes.json() + if (pollData.progress) setProgress(pollData.progress) + if (pollData.status === 'completed' && pollData.result) { + setResults(pollData.result) + setProgress('') + localStorage.setItem('impressum-check-results', JSON.stringify(pollData.result)) + const resultKey = `impressum-result-${Date.now()}` + try { localStorage.setItem(resultKey, JSON.stringify(pollData.result)) } catch {} + const total = pollData.result.total_findings || 0 + const pct = pollData.result.results?.[0]?.completeness_pct || 0 + const entry = { url: url.trim(), date: new Date().toISOString(), findings: total, pct, resultKey } + const updated = [entry, ...history].slice(0, 30) + setHistory(updated) + localStorage.setItem('impressum-check-history', JSON.stringify(updated)) + break + } + if (pollData.status === 'failed') throw new Error(pollData.error || 'Pruefung fehlgeschlagen') + attempts++ + } + } catch (e) { + setError(e instanceof Error ? e.message : 'Unbekannter Fehler') + setProgress('') + } finally { + setLoading(false) + } + } + + return ( +
+
+

Impressum-Check (§5 TMG / §18 MStV)

+

+ Prueft 16 Pflichtangaben: Anbietername, Anschrift, Kontaktdaten, Handelsregister, + USt-IdNr., Vertretungsberechtigte, V.i.S.d.P., Streitbeilegung. +

+
+ +
+ setUrl(e.target.value)} + placeholder="https://www.example.com/impressum" + 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}
} + + {results?.results && ( +
+ + {results.email_status && ( +
+ + E-Mail: {results.email_status === 'sent' ? 'Gesendet' : results.email_status} +
+ )} +
+ )} + + {history.length > 0 && ( +
+

Letzte Impressum-Checks

+
+ {history.map((h, i) => ( + + ))} +
+
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/page.tsx b/admin-compliance/app/sdk/agent/page.tsx index a96e470..f8d78f6 100644 --- a/admin-compliance/app/sdk/agent/page.tsx +++ b/admin-compliance/app/sdk/agent/page.tsx @@ -1,35 +1,24 @@ 'use client' import React, { useState } from 'react' -import { useAgentAnalysis } from './_hooks/useAgentAnalysis' -import { AnalysisResult } from './_components/AnalysisResult' -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' +import { ImpressumCheckTab } from './_components/ImpressumCheckTab' import { ComplianceFAQ } from './_components/ComplianceFAQ' -type AnalysisMode = 'pre_launch' | 'post_launch' -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: '📋' }, - { id: 'post_launch', label: 'Live-Website', desc: 'Bereits online analysieren', icon: '🌐' }, -] +type AnalysisTab = 'scan' | 'doc-check' | 'banner-check' | 'impressum-check' 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: 'scan', label: 'Website-Scan', desc: 'Rechtliche Dokumente finden + Dienstleister erkennen' }, + { id: 'doc-check', label: 'Dokumenten-Pruefung', desc: 'DSI, AGB, Cookie-Richtlinie inhaltlich pruefen' }, { id: 'banner-check', label: 'Banner-Check', desc: 'Cookie-Banner auf DSGVO-Konformitaet testen' }, + { id: 'impressum-check', label: 'Impressum-Check', desc: 'Impressum auf §5 TMG Pflichtangaben pruefen' }, ] export default function AgentPage() { - // Restore state from localStorage on mount const [url, setUrl] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('agent-scan-url') || '' : '') - const [mode, setMode] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('agent-scan-mode') as AnalysisMode : null) || 'post_launch') - const [tab, setTab] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('agent-scan-tab') as AnalysisTab : null) || 'quick') + const [tab, setTab] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('agent-scan-tab') as AnalysisTab : null) || 'scan') const [scanLoading, setScanLoading] = useState(false) const [scanError, setScanError] = useState(null) const [scanData, setScanData] = useState(() => { @@ -38,19 +27,15 @@ export default function AgentPage() { }) const [scanProgress, setScanProgress] = useState('') const [activeScanId, setActiveScanId] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('agent-scan-id') || '' : '') - const [scanHistory, setScanHistory] = useState<{ url: string; date: string; findings: number; docs: number }[]>(() => { + const [scanHistory, setScanHistory] = useState<{ url: string; date: string; findings: number; docs: number; resultKey: string }[]>(() => { if (typeof window === 'undefined') return [] try { return JSON.parse(localStorage.getItem('agent-scan-history') || '[]') } catch { return [] } }) - const { analyze, answerFollowUp, loading, error, result, history } = useAgentAnalysis() - // Persist state to localStorage React.useEffect(() => { localStorage.setItem('agent-scan-url', url) }, [url]) - React.useEffect(() => { localStorage.setItem('agent-scan-mode', mode) }, [mode]) React.useEffect(() => { localStorage.setItem('agent-scan-tab', tab) }, [tab]) - React.useEffect(() => { if (scanData?.services) localStorage.setItem('agent-scan-result', JSON.stringify(scanData)) }, [scanData]) - // Resume polling if scan was in progress when page was left + // Resume polling if scan was in progress React.useEffect(() => { if (!activeScanId || scanData?.services) return let cancelled = false @@ -74,15 +59,8 @@ export default function AgentPage() { _addToHistory(data.result) return } - if (data.status === 'failed') { - setScanError(data.error || 'Scan fehlgeschlagen') - setScanProgress('') - setScanLoading(false) - localStorage.removeItem('agent-scan-id') - setActiveScanId('') - return - } - if (data.status === 'not_found') { + if (data.status === 'failed' || data.status === 'not_found') { + if (data.status === 'failed') setScanError(data.error || 'Scan fehlgeschlagen') setScanProgress('') setScanLoading(false) localStorage.removeItem('agent-scan-id') @@ -97,125 +75,97 @@ export default function AgentPage() { }, []) // eslint-disable-line react-hooks/exhaustive-deps const _addToHistory = (result: any) => { + const resultKey = `scan-result-${Date.now()}` + try { localStorage.setItem(resultKey, JSON.stringify(result)) } catch {} const entry = { url: url || result.url || '', date: new Date().toISOString(), findings: result.findings?.length || 0, docs: result.discovered_documents?.length || 0, + resultKey, } - const updated = [entry, ...scanHistory].slice(0, 50) + const updated = [entry, ...scanHistory].slice(0, 30) setScanHistory(updated) localStorage.setItem('agent-scan-history', JSON.stringify(updated)) } - const _loadFromHistory = (entry: { url: string }) => { - setUrl(entry.url) - setTab('scan') - // Load saved result if same URL - try { - const saved = localStorage.getItem('agent-scan-result') - if (saved) { - const parsed = JSON.parse(saved) - if (parsed.url === entry.url) { - setScanData(parsed) - } - } - } catch {} - } - - const handleSubmit = async (e: React.FormEvent) => { + const handleScan = async (e: React.FormEvent) => { e.preventDefault() if (!url.trim()) return + setScanLoading(true) + setScanError(null) + setScanData(null) + setScanProgress('Scan wird gestartet...') + try { + const startRes = await fetch('/api/sdk/v1/agent/scan', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ url: url.trim(), mode: 'post_launch' }), + }) + if (!startRes.ok) throw new Error(`Scan konnte nicht gestartet werden: ${startRes.status}`) + const { scan_id } = await startRes.json() + if (!scan_id) throw new Error('Keine Scan-ID erhalten') + setActiveScanId(scan_id) + localStorage.setItem('agent-scan-id', scan_id) - if (tab === 'quick') { - analyze(url.trim(), mode) - } else { - setScanLoading(true) - setScanError(null) - setScanData(null) - setScanProgress('Scan wird gestartet...') - try { - // Step 1: Start async scan - const startRes = await fetch('/api/sdk/v1/agent/scan', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: url.trim(), mode }), - }) - if (!startRes.ok) throw new Error(`Scan konnte nicht gestartet werden: ${startRes.status}`) - const { scan_id } = await startRes.json() - if (!scan_id) throw new Error('Keine Scan-ID erhalten') - setActiveScanId(scan_id) - localStorage.setItem('agent-scan-id', scan_id) - - // Step 2: Poll for results - let attempts = 0 - const maxAttempts = 120 // 10 min at 5s intervals - while (attempts < maxAttempts) { - await new Promise(r => setTimeout(r, 5000)) - const pollRes = await fetch(`/api/sdk/v1/agent/scan?scan_id=${scan_id}`) - if (!pollRes.ok) { attempts++; continue } - const pollData = await pollRes.json() - - if (pollData.progress) { - setScanProgress(pollData.progress) - } - - if (pollData.status === 'completed' && pollData.result) { - setScanData(pollData.result) - setScanProgress('') - localStorage.setItem('agent-scan-result', JSON.stringify(pollData.result)) - localStorage.removeItem('agent-scan-id') - setActiveScanId('') - _addToHistory(pollData.result) - break - } - if (pollData.status === 'failed') { - throw new Error(pollData.error || 'Scan fehlgeschlagen') - } - attempts++ + let attempts = 0 + while (attempts < 120) { + await new Promise(r => setTimeout(r, 5000)) + const pollRes = await fetch(`/api/sdk/v1/agent/scan?scan_id=${scan_id}`) + if (!pollRes.ok) { attempts++; continue } + const pollData = await pollRes.json() + if (pollData.progress) setScanProgress(pollData.progress) + if (pollData.status === 'completed' && pollData.result) { + setScanData(pollData.result) + setScanProgress('') + localStorage.setItem('agent-scan-result', JSON.stringify(pollData.result)) + localStorage.removeItem('agent-scan-id') + setActiveScanId('') + _addToHistory(pollData.result) + break } - if (attempts >= maxAttempts) throw new Error('Scan-Timeout (10 Minuten)') - } catch (e) { - setScanError(e instanceof Error ? e.message : 'Unbekannter Fehler') - setScanProgress('') - } finally { - setScanLoading(false) + if (pollData.status === 'failed') throw new Error(pollData.error || 'Scan fehlgeschlagen') + attempts++ } + if (attempts >= 120) throw new Error('Scan-Timeout (10 Minuten)') + } catch (e) { + setScanError(e instanceof Error ? e.message : 'Unbekannter Fehler') + setScanProgress('') + } finally { + setScanLoading(false) } } - const isLoading = tab === 'quick' ? loading : scanLoading - const currentError = tab === 'quick' ? error : scanError + // Navigate to a specialized tab with a pre-filled URL + const navigateToCheck = (targetTab: AnalysisTab, checkUrl: string) => { + // Store the URL in the target tab's localStorage key + const keyMap: Record = { + 'doc-check': 'doc-check-prefill-url', + 'banner-check': 'banner-check-url', + 'impressum-check': 'impressum-check-url', + } + if (keyMap[targetTab]) { + localStorage.setItem(keyMap[targetTab], checkUrl) + } + setTab(targetTab) + } + + // Extract discovered documents for quick-action buttons + const discoveredDocs = scanData?.discovered_documents || [] + const scannedUrl = scanData?.url || url return (

Compliance Agent

-

Analysiere Dokumente und Webseiten auf DSGVO-Konformitaet.

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

Analysiere Webseiten und Dokumente auf DSGVO-Konformitaet.

{/* Tab Selection */} -
+
{TABS.map(t => (
- {/* Doc Check Tab — own component */} - {tab === 'doc-check' && } + {/* Website-Scan Tab */} + {tab === 'scan' && ( +
+
+

Website-Scan (Discovery)

+

+ Findet alle rechtlichen Dokumente (DSI, AGB, Impressum, Cookie, Widerruf), + erkennt eingesetzte Drittdienste und prueft ob sie in der DSE dokumentiert sind. +

+
- {/* Banner Check Tab — own component */} - {tab === 'banner-check' && } +
+ 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={scanLoading} required /> + +
- {/* URL Input (quick + scan only) */} - {(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" - disabled={isLoading} required /> - -
} + {scanProgress && ( +
+ + + + + {scanProgress} +
+ )} - {/* Scan Progress */} - {scanProgress && tab === 'scan' && ( -
- - - - - {scanProgress} -
- )} + {scanError && ( +
{scanError}
+ )} - {/* Error */} - {currentError && ( -
{currentError}
- )} + {/* Quick Action Buttons — navigate to specialized tabs */} + {scanData && ( +
+

Jetzt pruefen

+
+ + + {discoveredDocs.map((doc: any, i: number) => ( + + ))} +
+
+ )} - {/* Quick Analysis Result */} - {tab === 'quick' && result && ( -
- - {result.follow_up_questions.length > 0 && ( -
- + {/* Full Scan Result */} + {scanData?.services && ( +
+ +
+ )} + + {/* Scan History */} + {scanHistory.length > 0 && ( +
+

Letzte Scans

+
+ {scanHistory.map((h, i) => ( + + ))} +
)}
)} - {/* Scan Result — only render when we have a complete response with services */} - {tab === 'scan' && scanData && scanData.services && ( -
- -
- )} + {/* Specialized Tabs */} + {tab === 'doc-check' && } + {tab === 'banner-check' && } + {tab === 'impressum-check' && } - {/* History (quick only) */} - {tab === 'quick' && ( - { setUrl(r.url); analyze(r.url, mode) }} /> - )} - - {/* Scan History */} - {tab === 'scan' && scanHistory.length > 0 && ( -
-

Letzte Scans

-
- {scanHistory.map((h, i) => ( - - ))} -
-
- )} {/* FAQ */}