diff --git a/admin-compliance/app/sdk/agent/_components/AgentTestTab.tsx b/admin-compliance/app/sdk/agent/_components/AgentTestTab.tsx deleted file mode 100644 index deb5d0f3..00000000 --- a/admin-compliance/app/sdk/agent/_components/AgentTestTab.tsx +++ /dev/null @@ -1,337 +0,0 @@ -'use client' - -/** - * AgentTestTab — Top-Level für den 5-URL-Test eines Specialist-Agents. - * Sections: - * 1. Agent-Wähler + 5 URL-Slots + Start-Button - * 2. Methodik-Erklärung (was wir tun, warum) - * 3. Live-Event-Log - * 4. Pro Slot: SlotCard (siehe AgentSlotCard.tsx) - */ - -import React, { useEffect, useMemo, useRef, useState } from 'react' - -import type { AgentInfo, RunResult, SlotOutput, StreamEvent } from './_agentTypes' -import { AgentSlotCard } from './AgentSlotCard' - -const STORAGE_KEY = 'agent-test-state-v1' -const MAX_SLOTS = 5 - -export function AgentTestTab() { - const [agents, setAgents] = useState([]) - const [agentId, setAgentId] = useState('') - const [urls, setUrls] = useState(['', '', '', '', '']) - const [running, setRunning] = useState(false) - const [runId, setRunId] = useState('') - const [events, setEvents] = useState([]) - const [result, setResult] = useState(null) - const [error, setError] = useState('') - const eventSrcRef = useRef(null) - - // Restore state from localStorage - useEffect(() => { - try { - const s = localStorage.getItem(STORAGE_KEY) - if (s) { - const parsed = JSON.parse(s) - if (parsed.agentId) setAgentId(parsed.agentId) - if (Array.isArray(parsed.urls)) { - const padded = [...parsed.urls.slice(0, MAX_SLOTS), - ...new Array(MAX_SLOTS).fill('')].slice(0, MAX_SLOTS) - setUrls(padded) - } - } - } catch { /* noop */ } - }, []) - useEffect(() => { - try { - localStorage.setItem(STORAGE_KEY, - JSON.stringify({ agentId, urls })) - } catch { /* quota */ } - }, [agentId, urls]) - - // Load agents - useEffect(() => { - fetch('/api/sdk/v1/specialist-agent/agents') - .then(r => r.json()) - .then(d => { - const list: AgentInfo[] = d.agents || [] - setAgents(list) - if (list.length && !agentId) setAgentId(list[0].agent_id) - }) - .catch(e => setError(`Agent-Liste fehlgeschlagen: ${e}`)) - // eslint-disable-next-line react-hooks/exhaustive-deps - }, []) - - const startTest = async () => { - setError('') - setResult(null) - setEvents([]) - const cleanUrls = urls.map(u => u.trim()).filter(Boolean) - if (!agentId) { setError('Kein Agent ausgewählt.'); return } - if (cleanUrls.length === 0) { setError('Mind. eine URL angeben.'); return } - setRunning(true) - try { - const r = await fetch('/api/sdk/v1/specialist-agent/test/start', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ agent_id: agentId, urls: cleanUrls }), - }) - if (!r.ok) { - const j = await r.json().catch(() => ({})) - throw new Error(j.error || `HTTP ${r.status}`) - } - const data = await r.json() - setRunId(data.run_id) - openStream(data.run_id) - pollResult(data.run_id) - } catch (e: any) { - setError(e.message || String(e)) - setRunning(false) - } - } - - const openStream = (rid: string) => { - try { eventSrcRef.current?.close() } catch { /* noop */ } - const es = new EventSource( - `/api/sdk/v1/specialist-agent/test/stream/${rid}`, - ) - eventSrcRef.current = es - es.onmessage = (ev) => { - try { - const data: StreamEvent = JSON.parse(ev.data) - setEvents(prev => [...prev, data]) - if (data.type === 'stream_close' || data.type === 'run_complete') { - try { es.close() } catch { /* noop */ } - } - } catch { /* noop */ } - } - es.onerror = () => { try { es.close() } catch { /* noop */ } } - } - - const pollResult = async (rid: string) => { - for (let i = 0; i < 360; i++) { - try { - const r = await fetch( - `/api/sdk/v1/specialist-agent/run/${rid}/result`, - ) - if (r.ok) { - const d: RunResult = await r.json() - if (d.finished) { - setResult(d); setRunning(false); return - } - } - } catch { /* noop */ } - await new Promise(s => setTimeout(s, 2000)) - } - setRunning(false) - } - - const slotOutputs = useMemo(() => { - if (!result) return [] - const items: { slot: string; output: SlotOutput }[] = [] - for (const slot of Object.keys(result.results)) { - items.push({ slot, output: result.results[slot] }) - } - return items.sort((a, b) => a.slot.localeCompare(b.slot)) - }, [result]) - - const selectedAgent = agents.find(a => a.agent_id === agentId) - - return ( -
- - - - - {running && events.length > 0 && } - - {slotOutputs.length > 0 && ( -
- {slotOutputs.map(({ slot, output }) => ( - - ))} -
- )} -
- ) -} - -function InputCard({ - agents, agentId, setAgentId, selectedAgent, urls, setUrls, - running, runId, startTest, error, -}: { - agents: AgentInfo[] - agentId: string - setAgentId: (s: string) => void - selectedAgent?: AgentInfo - urls: string[] - setUrls: (urls: string[]) => void - running: boolean - runId: string - startTest: () => void - error: string -}) { - return ( -
-

Agent-Test (max. {MAX_SLOTS} URLs)

-
-
- - -
- {selectedAgent && ( -
- Doc-Type: {selectedAgent.doc_type} -
- )} -
-
- {urls.map((u, i) => ( -
- - URL{i + 1} - - { - const next = [...urls]; next[i] = e.target.value - setUrls(next) - }} - placeholder="https://example.com/impressum" - className="flex-1 border rounded px-2 py-1 text-sm font-mono" - /> -
- ))} -
-
- - {runId && ( - - Run-ID: {runId} - - )} -
- {error && ( -
- {error} -
- )} -
- ) -} - -function MethodikInfo() { - return ( -
- - Methodik — wie geprüft wird - -
    -
  1. - Pattern-Checks — deterministische Regex-Tests - gegen Pflichtangaben-Schema (z.B. § 5 TMG/DDG). Schnell, - reproduzierbar. Hinweis: diese Pattern-IDs (z.B. - IMP-MC-001) sind interne Test-IDs, - nicht die Master-Control-IDs aus der Datenbank. BreakPilot hat - 313k Atomic-Controls → 13.588 dedup. Master-Controls; davon - ~1.778 für dieses Compliance-Agent-Tool ausgewählt. Die formale - Verknüpfung Pattern-Check → Master-Control folgt in einem - späteren Schritt (Sprint 1.12). -
  2. -
  3. - Knowledge-Base — kuratierte Patterns aus - anonymisierten Mandanten-FAQs. -
  4. -
  5. - Auto-Learning-Pattern-Library — Labels die - der LLM-Validator gefunden hat (z.B. „Telefonnr." statt - „Telefon") werden persistiert. Beim nächsten Run sind sie - deterministisch erkennbar — der LLM wird seltener gerufen. -
  6. -
  7. - Semantic-Validator (LLM) — nur bei - missing-Pflichtangabe: ein Aufruf des Self-Hosted-LLM - (qwen3.5:35b-a3b auf macmini) prüft ob die - Angabe doch da ist, nur unter abweichendem Label. Bei - Treffer wird HIGH→LOW demoted und „Umbenennen zu Standard" - empfohlen. -
  8. -
  9. - LLM-Eskalation (Fallback) — wenn der - Validator unsicher bleibt: OVH 120b, dann anonymisierter - Claude-Cloud-Call. Aktuell deaktiviert (OVH-Key leer). -
  10. -
  11. - Cross-Placement-Agent — erkennt deplatzierten - Content (Copyright, Disclaimer, WEEE im Impressum) + - empfiehlt Footer-Reiter „Legal". -
  12. -
-

- Disclaimer: keine Aussagen wie „rechtssicher" oder „konform" — - nur Findings + Empfehlungen + Herleitung. Verbotene Begriffe - werden vom Linter aus Agent-Outputs entfernt. -

-
- ) -} - -function EventLog({ events }: { events: StreamEvent[] }) { - return ( -
-
- {events.slice(-30).map((ev, i) => ( -
- [{ev.type}]{' '} - {ev.slot && {ev.slot}}{' '} - {ev.severity && ( - {ev.severity} - )}{' '} - {ev.title || ev.error || ev.label || ev.model || ev.url || ''} - {ev.word_count !== undefined && ( - - {' '}({ev.word_count} Wörter) - - )} -
- ))} -
-
- ) -} - -function severityColor(sev: string) { - return sev === 'HIGH' ? 'text-red-600 font-semibold' : - sev === 'MEDIUM' ? 'text-amber-600 font-semibold' : - sev === 'LOW' ? 'text-blue-600' : 'text-gray-600' -} diff --git a/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx deleted file mode 100644 index 3a9cdc3e..00000000 --- a/admin-compliance/app/sdk/agent/_components/BannerCheckTab.tsx +++ /dev/null @@ -1,374 +0,0 @@ -'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 -} - -interface BannerResult { - banner_detected: boolean - banner_provider: string - banner_checks?: { - violations: { code: string; text: string; severity: string }[] - has_impressum_link?: boolean - has_dse_link?: boolean - } - structured_checks?: CheckItem[] - completeness_pct?: number - correctness_pct?: number - phases?: { - before_consent: { cookies: string[]; scripts: string[]; tracking_services: string[]; violations: any[] } - after_reject: { cookies: string[]; scripts: string[]; new_tracking: string[]; violations: any[] } - after_accept: { cookies: string[]; scripts: string[]; new_tracking: string[]; undocumented: string[] } - } - email_status?: string -} - -const CATEGORIES = [ - { id: 'all', label: 'Alle Kategorien' }, - { id: 'necessary', label: 'Notwendig' }, - { id: 'statistics', label: 'Statistik' }, - { id: 'marketing', label: 'Marketing' }, - { id: 'functional', label: 'Funktional' }, - { id: 'preferences', label: 'Praeferenzen' }, -] - -export function BannerCheckTab() { - const [url, setUrl] = useState(() => - typeof window !== 'undefined' ? localStorage.getItem('banner-check-url') || '' : '' - ) - const [loading, setLoading] = useState(false) - const [progress, setProgress] = useState('') - const [error, setError] = useState(null) - const [result, setResult] = useState(() => { - if (typeof window === 'undefined') return null - try { const s = localStorage.getItem('banner-check-result'); return s ? JSON.parse(s) : null } catch { return null } - }) - const [categories, setCategories] = useState(['all']) - const [useAgent, setUseAgent] = useState(false) - const [mcResults, setMcResults] = useState(null) - const [history, setHistory] = useState<{ url: string; date: string; provider: string; violations: number; pct: number; resultKey: string }[]>(() => { - if (typeof window === 'undefined') return [] - try { return JSON.parse(localStorage.getItem('banner-check-history') || '[]') } catch { return [] } - }) - - // Persist URL - React.useEffect(() => { localStorage.setItem('banner-check-url', url) }, [url]) - - const toggleCategory = (id: string) => { - if (id === 'all') { - setCategories(['all']) - return - } - setCategories(prev => { - const without = prev.filter(c => c !== 'all' && c !== id) - const next = prev.includes(id) ? without : [...without, id] - return next.length === 0 ? ['all'] : next - }) - } - - const handleScan = async (e: React.FormEvent) => { - e.preventDefault() - if (!url.trim()) return - - setLoading(true) - setError(null) - setResult(null) - setProgress('Cookie-Banner wird analysiert...') - - const selectedCategories = categories.includes('all') ? [] : categories - - try { - const res = await fetch('/api/sdk/v1/agent/banner-check', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ url: url.trim(), categories: selectedCategories }), - }) - if (!res.ok) throw new Error(`Fehler: ${res.status}`) - const data = await res.json() - setResult(data) - localStorage.setItem('banner-check-result', JSON.stringify(data)) - - // If agent mode: also run cookie doc-check with 381 MCs - if (useAgent) { - setProgress('KI-Agent prueft Cookie-Richtlinie (381 MCs)...') - try { - const mcRes = await fetch('/api/sdk/v1/agent/doc-check', { - method: 'POST', - headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ - entries: [{ doc_type: 'cookie', label: 'Cookie-Richtlinie', url: url.trim() }], - recipient: 'dsb@breakpilot.local', - use_agent: true, - }), - }) - if (mcRes.ok) { - const { check_id } = await mcRes.json() - if (check_id) { - for (let i = 0; i < 60; i++) { - await new Promise(r => setTimeout(r, 3000)) - const poll = await fetch(`/api/sdk/v1/agent/doc-check?check_id=${check_id}`) - if (!poll.ok) continue - const pd = await poll.json() - if (pd.progress) setProgress(`KI-Agent: ${pd.progress}`) - if (pd.status === 'completed' && pd.result) { setMcResults(pd.result); break } - if (pd.status === 'failed') break - } - } - } - } catch { /* agent check is optional */ } - } - - // Add to history with persistent result - const violations = data.structured_checks?.filter((c: CheckItem) => !c.passed && !c.skipped).length || 0 - const resultKey = `banner-check-result-${Date.now()}` - try { localStorage.setItem(resultKey, JSON.stringify(data)) } catch { /* quota */ } - const entry = { - url: url.trim(), - date: new Date().toISOString(), - provider: data.banner_provider || 'Unbekannt', - violations, - pct: data.completeness_pct ?? 0, - resultKey, - } - const updated = [entry, ...history].slice(0, 30) - setHistory(updated) - localStorage.setItem('banner-check-history', JSON.stringify(updated)) - } catch (e) { - setError(e instanceof Error ? e.message : 'Unbekannter Fehler') - } finally { - setLoading(false) - setProgress('') - } - } - - const loadFromHistory = (entry: { url: string; resultKey?: string }) => { - setUrl(entry.url) - if (entry.resultKey) { - try { - const saved = localStorage.getItem(entry.resultKey) - if (saved) { setResult(JSON.parse(saved)); return } - } catch {} - } - // Fallback: load last result - try { - const last = localStorage.getItem('banner-check-result') - if (last) setResult(JSON.parse(last)) - } catch {} - } - - const structuredChecks = result?.structured_checks || [] - const hasStructured = structuredChecks.length > 0 - const compPct = result?.completeness_pct ?? 0 - const corrPct = result?.correctness_pct ?? 0 - - const checklistResults = hasStructured ? [{ - label: `Cookie-Banner: ${result?.banner_provider || 'Unbekannt'}`, - url: url, - doc_type: 'banner', - word_count: 0, - completeness_pct: compPct, - correctness_pct: corrPct, - checks: structuredChecks, - findings_count: structuredChecks.filter(c => !c.passed && !c.skipped).length, - error: '', - }] : [] - - 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 36 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 - /> - -
- -
- {CATEGORIES.map(cat => ( - - ))} -
-
- - {progress && ( -
- - - - - {progress} -
- )} - - {error && ( -
{error}
- )} - - {result && ( -
- {result.phases && ( -
-
-
- {result.banner_detected ? '🛡️' : '⚠️'} -
-

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

-

3-Phasen-Analyse: Cookies und Scripts vor/nach Interaktion

-
-
-
-
- - - -
-
- )} - - {hasStructured && ( -
- -
- )} - - {result.email_status && ( -
- - E-Mail: {result.email_status === 'sent' ? 'Gesendet' : result.email_status} -
- )} - - {/* MC Agent Results (Cookie-Richtlinie) */} - {mcResults?.results && ( -
-

KI-Agent: Cookie-Richtlinie (381 MCs)

- -
- )} - - {!result.banner_detected && !hasStructured && ( -
-

- Kein Cookie-Banner auf dieser Seite gefunden. Falls Cookies gesetzt werden, ist ein Banner nach §25 TDDDG Pflicht. -

-
- )} -
- )} - - {/* History */} - {history.length > 0 && ( -
-

Letzte Banner-Checks

-
- {history.map((h, i) => ( - - ))} -
-
- )} -
- ) -} - -function PhaseBox({ label, icon, cookies, scripts, violations }: { - label: string; icon: string; cookies: number; scripts: number; violations: number -}) { - return ( -
-
{icon}
-
{label}
-
{cookies} Cookies, {scripts} Scripts
- {violations > 0 &&
{violations} Verstoesse
} -
- ) -} diff --git a/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx b/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx index 63844945..ddcfe718 100644 --- a/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx +++ b/admin-compliance/app/sdk/agent/_components/ComplianceCheckTab.tsx @@ -1,7 +1,6 @@ 'use client' import React, { useState, useCallback, useRef } from 'react' -import { ComplianceResultTabs } from './ComplianceResultTabs' import { DocumentRow } from './DocumentRow' import { PreScanWizard, useScanContext, isContextComplete } from './PreScanWizard' import { DOCUMENT_TYPES, type DocTypeId } from './_document_types' @@ -13,7 +12,7 @@ import { import { useCompanyOrigin } from './_useCompanyOrigin' -export function ComplianceCheckTab() { +export function ComplianceCheckTab({ onComplete }: { onComplete?: () => void } = {}) { const [docs, setDocs] = useState(initState) const { companyName, setCompanyName, originDomain, setOriginDomain } = useCompanyOrigin() const [scanContext, setScanContext] = useScanContext() @@ -243,21 +242,16 @@ export function ComplianceCheckTab() { } } - const loadFromHistory = (entry: HistoryEntry) => { - if (entry.resultKey) { - try { - const saved = localStorage.getItem(entry.resultKey) - if (saved) { setResults(JSON.parse(saved)); return } - } catch { /* ignore */ } - } - try { - const last = localStorage.getItem(STORAGE_KEY_RESULTS) - if (last) setResults(JSON.parse(last)) - } catch { /* ignore */ } - } - const contextReady = isContextComplete(scanContext) + // Nach Abschluss eines Checks (loading true→false mit Ergebnis) die + // Snapshot-Historie unten neu laden — der frische Snapshot erscheint oben. + const prevLoading = useRef(false) + React.useEffect(() => { + if (prevLoading.current && !loading && results) onComplete?.() + prevLoading.current = loading + }, [loading, results, onComplete]) + return (
{/* Info box */} @@ -390,37 +384,12 @@ export function ComplianceCheckTab() {
{error}
)} - {/* Results — strukturierte Themen-Tabs (Impressum, …) + Roh-Checkliste */} - {results && results.results && ( - - )} - - {/* History */} - {history.length > 0 && ( -
-

Letzte Compliance-Checks

-
- {history.map((h, i) => ( - - ))} -
+ {/* Nach Abschluss: Hinweis auf die Historie unten. Die eigentlichen + Ergebnisse leben in der Snapshot-Detail-Seite (oberster Eintrag). */} + {results && results.results && !loading && ( +
+ Check abgeschlossen — das Ergebnis steht unten in der Historie (oberster, farblich + markierter Eintrag). Klick ihn an, um die Auswertung zu öffnen.
)}
diff --git a/admin-compliance/app/sdk/agent/_components/ScanResult.tsx b/admin-compliance/app/sdk/agent/_components/ScanResult.tsx deleted file mode 100644 index 6003395d..00000000 --- a/admin-compliance/app/sdk/agent/_components/ScanResult.tsx +++ /dev/null @@ -1,325 +0,0 @@ -'use client' - -import React, { useState } from 'react' -import { TextReference } from './TextReference' - -interface ServiceInfo { - name: string - category: string - provider: string - country: string - eu_adequate: boolean - requires_consent: boolean - legal_ref: string - in_dse: boolean - status: string -} - -interface TextRef { - found: boolean - source_url: string - document_type: string - section_heading: string - section_number: string - parent_section: string - paragraph_index: number - original_text: string - issue: string - correction_type: string - correction_text: string - insert_after: string -} - -interface ScanFinding { - code: string - doc_title?: string - severity: string - text: string - correction: string - text_reference: TextRef | null -} - -interface DiscoveredDocument { - title: string - completeness_pct: number - word_count?: number - url?: string -} - -interface ScanData { - pages_scanned: number - pages_list: string[] - services: ServiceInfo[] - findings: ScanFinding[] - discovered_documents?: DiscoveredDocument[] - ai_detected: boolean - chatbot_detected: boolean - chatbot_provider: string - missing_pages: Record - email_status: string -} - -const STATUS_ICON: Record = { - ok: { icon: '\u2713', color: 'text-green-600' }, - undocumented: { icon: '\u2717', color: 'text-red-600' }, - outdated: { icon: '~', color: 'text-yellow-600' }, -} - -const SEV_STYLE: Record = { - HIGH: { bg: 'bg-red-50 border-red-200', text: 'text-red-800', dot: 'bg-red-500' }, - MEDIUM: { bg: 'bg-yellow-50 border-yellow-200', text: 'text-yellow-800', dot: 'bg-yellow-500' }, - LOW: { bg: 'bg-blue-50 border-blue-200', text: 'text-blue-800', dot: 'bg-blue-500' }, - CRITICAL: { bg: 'bg-red-100 border-red-300', text: 'text-red-900', dot: 'bg-red-700' }, -} - -export function ScanResult({ data }: { data: ScanData }) { - const [expandedCorrection, setExpandedCorrection] = useState(null) - const [expandedDoc, setExpandedDoc] = useState(null) - - const undocCount = data.services.filter(s => s.status === 'undocumented').length - const okCount = data.services.filter(s => s.status === 'ok').length - const highCount = data.findings.filter(f => f.severity === 'HIGH' || f.severity === 'CRITICAL').length - const docs = data.discovered_documents || [] - - // Group findings by doc_title - const docFindings: Record = {} - const generalFindings: ScanFinding[] = [] - for (const f of data.findings) { - if (f.doc_title) { - if (!docFindings[f.doc_title]) docFindings[f.doc_title] = [] - docFindings[f.doc_title].push(f) - } else { - generalFindings.push(f) - } - } - - return ( -
- {/* Summary Bar */} -
-
-

{data.pages_scanned}

-

Seiten

-
-
-

{okCount}

-

Dokumentiert

-
-
-

{undocCount}

-

Nicht in DSE

-
-
-

{docs.length}

-

Dokumente

-
-
- - {/* Scanned Pages */} - {data.pages_list?.length > 0 && ( -
- - {data.pages_scanned} Seiten gescannt - -
    - {data.pages_list.map((p, i) => { - const isMissing = data.missing_pages[p] - return ( -
  • - {isMissing ? '\u2717' : '\u2713'} {p} -
  • - ) - })} -
-
- )} - - {/* Services Table */} - {data.services.length > 0 && ( -
-

Dienstleister (SOLL/IST)

-
- - - - - - - - - - - {data.services.map((s, i) => { - const st = STATUS_ICON[s.status] || STATUS_ICON.ok - return ( - - - - - - - ) - })} - -
StatusDienstLandIn DSE
{st.icon} - {s.name} - {s.provider} - {s.country}{s.in_dse ? '\u2713' : Nein}
-
-
- )} - - {/* === Document-Centric View === */} - {docs.length > 0 && ( -
-

- Rechtliche Dokumente ({docs.length}) -

-
- {docs.map((doc, i) => { - const isExpanded = expandedDoc === doc.title - const findings = docFindings[doc.title] || [] - const pct = doc.completeness_pct - const barColor = pct >= 80 ? 'bg-green-500' : pct >= 50 ? 'bg-yellow-500' : 'bg-red-500' - const statusLabel = pct >= 80 ? 'OK' : pct >= 50 ? 'Lueckenhaft' : 'Mangelhaft' - const statusColor = pct >= 80 ? 'text-green-700 bg-green-50' : pct >= 50 ? 'text-yellow-700 bg-yellow-50' : 'text-red-700 bg-red-50' - - return ( -
- - - {isExpanded && ( -
- {findings.length > 0 ? ( - findings.map((f, fi) => { - const sev = SEV_STYLE[f.severity] || SEV_STYLE.MEDIUM - return ( -
- - {f.text} -
- ) - }) - ) : ( -

Alle Pflichtangaben vorhanden.

- )} - {doc.url && ( - - Dokument oeffnen - - )} -
- )} -
- ) - })} -
-
- )} - - {/* General Findings (not associated with a specific document) */} - {generalFindings.length > 0 && ( -
-

- Allgemeine Findings ({generalFindings.length}) -

-
- {generalFindings.map((f, i) => { - const sev = SEV_STYLE[f.severity] || SEV_STYLE.MEDIUM - const corrKey = `gen-${i}` - const isExp = expandedCorrection === corrKey - return ( -
-
- - {f.severity} - -

{f.text}

-
- {/* Text Reference (original text + position + correction) */} - {f.text_reference && ( - - )} - {/* Fallback: correction without text reference */} - {!f.text_reference && f.correction && ( -
- - {isExp && ( -
-
{f.correction}
- -
- )} -
- )} -
- ) - })} -
-
- )} - {/* PDF Export Button */} -
- -
-
- ) -} diff --git a/admin-compliance/app/sdk/agent/_components/SnapshotHistoryList.tsx b/admin-compliance/app/sdk/agent/_components/SnapshotHistoryList.tsx new file mode 100644 index 00000000..287178fb --- /dev/null +++ b/admin-compliance/app/sdk/agent/_components/SnapshotHistoryList.tsx @@ -0,0 +1,83 @@ +'use client' + +/** + * SnapshotHistoryList — Check-Historie aus gespeicherten Snapshots. + * + * Neuester Snapshot oben + farblich abgesetzt. Klick → Detail-Seite mit den + * Ergebnissen (/sdk/agent/snapshots/{id}). `refreshKey` neu setzen, um nach + * einem frisch gelaufenen Compliance-Check neu zu laden. + */ + +import React, { useEffect, useState } from 'react' +import Link from 'next/link' + +interface SnapMeta { + id: string + check_id?: string + site_domain?: string + site_label?: string + created_at?: string +} + +export function SnapshotHistoryList( + { refreshKey = 0, limit = 50 }: { refreshKey?: number; limit?: number }, +) { + const [snaps, setSnaps] = useState([]) + const [loading, setLoading] = useState(true) + + useEffect(() => { + let cancelled = false + setLoading(true) + fetch(`/api/sdk/v1/agent/snapshots?limit=${limit}`) + .then(r => r.json()) + .then(d => { if (!cancelled) setSnaps(d.snapshots || []) }) + .catch(() => { if (!cancelled) setSnaps([]) }) + .finally(() => { if (!cancelled) setLoading(false) }) + return () => { cancelled = true } + }, [refreshKey, limit]) + + return ( +
+
+

Historie

+ {!loading && snaps.length > 0 && ( + {snaps.length} Checks + )} +
+ + {loading ? ( +
Lade Historie…
+ ) : snaps.length === 0 ? ( +
+ Noch keine Checks — starte oben einen Compliance-Check. +
+ ) : ( +
+ {snaps.map((s, i) => ( + + {i === 0 && ( + + aktuellster + + )} + + {s.site_label || s.site_domain || 'unbekannt'} + + {s.site_domain} + + {(s.created_at || '').slice(0, 16).replace('T', ' ')} + + + + ))} +
+ )} +
+ ) +} diff --git a/admin-compliance/app/sdk/agent/page.tsx b/admin-compliance/app/sdk/agent/page.tsx index 9b463ace..14d337c0 100644 --- a/admin-compliance/app/sdk/agent/page.tsx +++ b/admin-compliance/app/sdk/agent/page.tsx @@ -1,194 +1,24 @@ 'use client' import React, { useState } from 'react' -import { ScanResult } from './_components/ScanResult' import { ComplianceCheckTab } from './_components/ComplianceCheckTab' -import { BannerCheckTab } from './_components/BannerCheckTab' import { ComplianceFAQ } from './_components/ComplianceFAQ' -import { AgentTestTab } from './_components/AgentTestTab' - -type AnalysisTab = 'scan' | 'compliance-check' | 'banner-check' | 'agent-test' | 'impressum-check' | 'doc-check' - -const TABS: { id: AnalysisTab; label: string; desc: string }[] = [ - { id: 'scan', label: 'Website-Scan', desc: 'Rechtliche Dokumente finden + Dienstleister erkennen' }, - { id: 'compliance-check', label: 'Compliance-Check', desc: 'Alle rechtlichen Dokumente zusammen pruefen' }, - { id: 'banner-check', label: 'Banner-Check', desc: 'Cookie-Banner auf DSGVO-Konformitaet testen' }, - { id: 'agent-test', label: 'Agent-Test', desc: 'Specialist-Agent gegen 5 URLs isoliert testen' }, -] +import { SnapshotHistoryList } from './_components/SnapshotHistoryList' export default function AgentPage() { - const [url, setUrl] = useState(() => typeof window !== 'undefined' ? localStorage.getItem('agent-scan-url') || '' : '') - const [tab, setTab] = useState(() => (typeof window !== 'undefined' ? localStorage.getItem('agent-scan-tab') as AnalysisTab : null) || 'compliance-check') - const [scanLoading, setScanLoading] = useState(false) - const [scanError, setScanError] = useState(null) - const [scanData, setScanData] = useState(() => { - if (typeof window === 'undefined') return null - try { const s = localStorage.getItem('agent-scan-result'); return s ? JSON.parse(s) : null } catch { return null } - }) - 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; resultKey: string }[]>(() => { - if (typeof window === 'undefined') return [] - try { return JSON.parse(localStorage.getItem('agent-scan-history') || '[]') } catch { return [] } - }) - - React.useEffect(() => { localStorage.setItem('agent-scan-url', url) }, [url]) - React.useEffect(() => { localStorage.setItem('agent-scan-tab', tab) }, [tab]) - - // Resume polling if scan was in progress - React.useEffect(() => { - if (!activeScanId || scanData?.services) return - let cancelled = false - setScanLoading(true) - setScanProgress('Scan laeuft noch...') - const poll = async () => { - while (!cancelled) { - await new Promise(r => setTimeout(r, 5000)) - try { - const res = await fetch(`/api/sdk/v1/agent/scan?scan_id=${activeScanId}`) - if (!res.ok) continue - const data = await res.json() - if (data.progress) setScanProgress(data.progress) - if (data.status === 'completed' && data.result) { - setScanData(data.result); setScanProgress(''); setScanLoading(false) - localStorage.setItem('agent-scan-result', JSON.stringify(data.result)) - localStorage.removeItem('agent-scan-id'); setActiveScanId('') - _addToHistory(data.result); return - } - 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'); setActiveScanId(''); return - } - } catch {} - } - } - poll() - return () => { cancelled = true } - }, []) // 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, 30) - setScanHistory(updated); localStorage.setItem('agent-scan-history', JSON.stringify(updated)) - } - - 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) - 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 (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 navigateToCheck = (targetTab: AnalysisTab, checkUrl: string) => { - 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) - } - - const discoveredDocs = scanData?.discovered_documents || [] - const scannedUrl = scanData?.url || url + // Nach einem abgeschlossenen Check die Historie unten neu laden. + const [historyKey, setHistoryKey] = useState(0) return (

Compliance Agent

-

Analysiere Webseiten und Dokumente auf DSGVO-Konformitaet.

+

Webseiten + Dokumente auf DSGVO-Konformität prüfen.

-
- {TABS.map(t => ( - - ))} -
+ setHistoryKey(k => k + 1)} /> - {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.

-
-
- 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 /> - -
- {scanProgress &&
{scanProgress}
} - {scanError &&
{scanError}
} - {scanData && ( -
-

Jetzt pruefen

-
- - - {discoveredDocs.map((doc: any, i: number) => ( - - ))} -
-
- )} - {scanData?.services &&
} - {scanHistory.length > 0 && ( -
-

Letzte Scans

-
- {scanHistory.map((h, i) => ( - - ))} -
-
- )} -
- )} - - {tab === 'compliance-check' && } - {tab === 'banner-check' && } - {tab === 'agent-test' && } +
diff --git a/admin-compliance/app/sdk/agent/snapshots/page.tsx b/admin-compliance/app/sdk/agent/snapshots/page.tsx index 479a1716..54bae44f 100644 --- a/admin-compliance/app/sdk/agent/snapshots/page.tsx +++ b/admin-compliance/app/sdk/agent/snapshots/page.tsx @@ -1,37 +1,14 @@ 'use client' /** - * Check-Historie — listet gespeicherte Snapshots (alle Sites/Module). - * Ein DSB/Mitarbeiter kann jeden früheren Check öffnen, ohne neuen Check - * zu starten. Daten kommen aus den Snapshot-Rohdaten. + * Check-Historie (eigene Route) — listet gespeicherte Snapshots. + * Identische Liste wie unter /sdk/agent, nur als Vollseite. */ -import React, { useEffect, useState } from 'react' -import Link from 'next/link' - -interface SnapMeta { - id: string - check_id?: string - site_domain?: string - site_label?: string - created_at?: string - replay_count?: number -} +import React from 'react' +import { SnapshotHistoryList } from '../_components/SnapshotHistoryList' export default function SnapshotHistory() { - const [snaps, setSnaps] = useState([]) - const [loading, setLoading] = useState(true) - - useEffect(() => { - let cancelled = false - fetch('/api/sdk/v1/agent/snapshots?limit=50') - .then(r => r.json()) - .then(d => { if (!cancelled) setSnaps(d.snapshots || []) }) - .catch(() => { if (!cancelled) setSnaps([]) }) - .finally(() => { if (!cancelled) setLoading(false) }) - return () => { cancelled = true } - }, []) - return (
@@ -41,31 +18,7 @@ export default function SnapshotHistory() { ansehbar, ohne neuen Check zu starten.

- - {loading ? ( -
Lade Historie…
- ) : snaps.length === 0 ? ( -
Keine gespeicherten Checks gefunden.
- ) : ( -
- {snaps.map(s => ( - - - {s.site_label || s.site_domain || 'unbekannt'} - - {s.site_domain} - - {(s.created_at || '').slice(0, 16).replace('T', ' ')} - - - - ))} -
- )} +
) }