feat: 5-tab agent UI — PDF export, compare, auth test, all proxies

- 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) <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-04-29 16:43:08 +02:00
parent 2c9cea74e3
commit b53b36fdc5
7 changed files with 383 additions and 105 deletions
@@ -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 })
}
}
@@ -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 })
}
}
@@ -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 })
}
}
@@ -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<string, AuthCheck>
findings_count: number
}
const CHECK_LABELS: Record<string, { label: string; icon: string }> = {
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 (
<div className="bg-red-50 border border-red-200 rounded-lg p-4">
<p className="text-sm font-medium text-red-800">Login fehlgeschlagen</p>
<p className="text-xs text-red-600 mt-1">{data.login_error || 'Credentials oder Formular nicht erkannt'}</p>
</div>
)
}
return (
<div className="space-y-4">
<div className="flex items-center gap-2">
<span className="w-3 h-3 rounded-full bg-green-500" />
<span className="text-sm font-medium text-gray-900">Erfolgreich eingeloggt</span>
<span className={`ml-auto text-xs px-2 py-1 rounded font-medium ${data.findings_count > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
{data.findings_count} fehlende Funktionen
</span>
</div>
<div className="space-y-2">
{Object.entries(data.checks).map(([key, check]) => {
const info = CHECK_LABELS[key] || { label: key, icon: '❓' }
return (
<div key={key} className={`flex items-center gap-3 p-3 rounded-lg border ${check.found ? 'bg-green-50 border-green-200' : 'bg-red-50 border-red-200'}`}>
<span className="text-lg">{info.icon}</span>
<div className="flex-1">
<p className={`text-sm font-medium ${check.found ? 'text-green-800' : 'text-red-800'}`}>
{check.found ? '✓' : '✗'} {info.label}
</p>
{check.text && <p className="text-xs text-gray-500 mt-0.5">{check.text}</p>}
</div>
<span className="text-[10px] text-gray-400">{check.legal_ref}</span>
</div>
)
})}
</div>
{data.findings_count > 0 && (
<div className="bg-red-50 border-l-4 border-red-500 p-3 text-xs text-red-700">
<strong>{data.findings_count} Pflichtfunktion(en) fehlen.</strong> Der Nutzer kann seine Rechte
nach DSGVO nicht vollstaendig ausueben.
</div>
)}
</div>
)
}
@@ -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<string, string> = {
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 (
<div className="space-y-4">
<div className="overflow-x-auto">
<table className="w-full text-sm border-collapse">
<thead>
<tr className="bg-gray-50">
<th className="text-left px-3 py-2 text-xs font-medium text-gray-500 w-44">Pruefung</th>
{sites.map((s, i) => (
<th key={i} className="text-center px-3 py-2 text-xs font-medium text-gray-700">
{s.domain}
</th>
))}
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
<tr>
<td className="px-3 py-2 text-gray-600">Risiko-Score</td>
{sites.map((s, i) => (
<td key={i} className="px-3 py-2 text-center">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${RISK_COLOR[s.risk_level] || 'text-gray-600 bg-gray-50'}`}>
{s.risk_level || '?'} ({s.risk_score}/100)
</span>
</td>
))}
</tr>
<tr>
<td className="px-3 py-2 text-gray-600">Findings</td>
{sites.map((s, i) => (
<td key={i} className={`px-3 py-2 text-center font-medium ${s.findings_count > 0 ? 'text-red-700' : 'text-green-700'}`}>
{s.findings_count}
</td>
))}
</tr>
<tr>
<td className="px-3 py-2 text-gray-600">Dienste erkannt</td>
{sites.map((s, i) => (
<td key={i} className="px-3 py-2 text-center text-gray-700">{s.services_count}</td>
))}
</tr>
{checks.map(check => (
<tr key={check.key}>
<td className="px-3 py-2 text-gray-600">{check.label}</td>
{sites.map((s, i) => {
const val = (s as any)[check.key]
const isInverted = check.key === 'has_google_fonts'
const good = isInverted ? !val : val
return (
<td key={i} className={`px-3 py-2 text-center font-medium ${good ? 'text-green-600' : 'text-red-600'}`}>
{good ? '✓' : '✗'}
</td>
)
})}
</tr>
))}
</tbody>
</table>
</div>
</div>
)
}
@@ -207,6 +207,35 @@ export function ScanResult({ data }: { data: ScanData }) {
</div>
</div>
)}
{/* PDF Export Button */}
<div className="pt-4 border-t flex gap-3">
<button
onClick={async () => {
try {
const res = await fetch('/api/sdk/v1/agent/scans/pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: '', scan_type: 'scan', analysis_mode: 'post_launch', result: data }),
})
if (res.ok) {
const blob = await res.blob()
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = 'compliance-report.pdf'
a.click()
URL.revokeObjectURL(url)
}
} catch (e) { console.error('PDF export failed:', e) }
}}
className="flex items-center gap-2 px-4 py-2 text-sm font-medium text-purple-700 bg-purple-50 border border-purple-200 rounded-lg hover:bg-purple-100 transition-colors"
>
<svg className="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M12 10v6m0 0l-3-3m3 3l3-3m2 8H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
</svg>
PDF herunterladen
</button>
</div>
</div>
)
}
+98 -94
View File
@@ -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<AnalysisMode>('post_launch')
const [tab, setTab] = useState<AnalysisTab>('quick')
const [urls, setUrls] = useState('')
const [mode, setMode] = useState<Mode>('post_launch')
const [tab, setTab] = useState<Tab>('quick')
const [scanLoading, setScanLoading] = useState(false)
const [scanError, setScanError] = useState<string | null>(null)
const [scanData, setScanData] = useState<any>(null)
const [scanHistory, setScanHistory] = useState<any[]>([])
const [consentLoading, setConsentLoading] = useState(false)
const [consentError, setConsentError] = useState<string | null>(null)
const [consentData, setConsentData] = useState<any>(null)
const [compareData, setCompareData] = useState<any>(null)
const [authData, setAuthData] = useState<any>(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
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', {
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({ url: url.trim(), mode }),
body: JSON.stringify(body),
})
if (!res.ok) throw new Error(`Scan fehlgeschlagen: ${res.status}`)
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))
} 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 : 'Unbekannter Fehler')
setScanError(e instanceof Error ? e.message : '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)
}
}
}
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 (
<div className="space-y-6 max-w-4xl">
@@ -91,12 +102,11 @@ export default function AgentPage() {
<p className="text-gray-500 mt-1">Analysiere Dokumente und Webseiten auf DSGVO-Konformitaet.</p>
</div>
{/* Mode Selection */}
{/* Mode */}
<div className="grid grid-cols-2 gap-3">
{MODES.map(m => (
<button key={m.id} onClick={() => setMode(m.id)}
className={`p-3 rounded-xl border-2 text-left transition-all ${
mode === m.id ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'}`}>
className={`p-3 rounded-xl border-2 text-left transition-all ${mode === m.id ? 'border-purple-500 bg-purple-50' : 'border-gray-200 hover:border-gray-300'}`}>
<div className="flex items-center gap-3">
<span className="text-xl">{m.icon}</span>
<div>
@@ -108,90 +118,84 @@ export default function AgentPage() {
))}
</div>
{/* Tab Selection + Info */}
{/* Tabs */}
<div>
<div className="flex border-b border-gray-200">
<div className="flex border-b border-gray-200 overflow-x-auto">
{TABS.map(t => (
<button key={t.id} onClick={() => setTab(t.id)}
className={`px-4 py-2.5 text-sm font-medium border-b-2 transition-colors ${
tab === t.id
? 'border-purple-500 text-purple-700'
: 'border-transparent text-gray-500 hover:text-gray-700'}`}>
className={`px-3 py-2.5 text-sm font-medium border-b-2 whitespace-nowrap transition-colors ${tab === t.id ? 'border-purple-500 text-purple-700' : 'border-transparent text-gray-500 hover:text-gray-700'}`}>
{t.label}
</button>
))}
</div>
<p className="text-xs text-gray-400 mt-2 px-1">{currentTab.info}</p>
<p className="text-xs text-gray-400 mt-2 px-1">{TABS.find(t => t.id === tab)?.info}</p>
</div>
{/* URL Input */}
<form onSubmit={handleSubmit} className="flex gap-3">
{/* Input */}
<form onSubmit={handleSubmit} className="space-y-3">
{tab === 'compare' ? (
<textarea value={urls} onChange={e => setUrls(e.target.value)}
placeholder="https://www.opodo.de&#10;https://www.booking.com&#10;https://www.expedia.de"
rows={3}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm"
disabled={isLoading} />
) : (
<input type="url" value={url} onChange={e => 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"
placeholder={tab === 'auth' ? 'https://www.example.com/login' : 'https://www.example.com/'}
className="w-full px-4 py-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-purple-500 text-sm"
disabled={isLoading} required />
<button type="submit" disabled={isLoading || !url.trim()}
)}
{tab === 'auth' && (
<div className="grid grid-cols-2 gap-3">
<input type="text" value={authUser} onChange={e => setAuthUser(e.target.value)}
placeholder="Email / Benutzername" autoComplete="off"
className="px-4 py-2 border border-gray-300 rounded-lg text-sm" />
<input type="password" value={authPass} onChange={e => setAuthPass(e.target.value)}
placeholder="Passwort" autoComplete="off"
className="px-4 py-2 border border-gray-300 rounded-lg text-sm" />
<p className="col-span-2 text-[10px] text-gray-400">Credentials werden NICHT gespeichert nur fuer diesen Test im Browser-Kontext.</p>
</div>
)}
<button type="submit" disabled={isLoading || (!url.trim() && tab !== 'compare') || (tab === 'compare' && !urls.trim())}
className="px-6 py-3 bg-purple-600 text-white rounded-lg hover:bg-purple-700 disabled:opacity-50 transition-colors flex items-center gap-2 text-sm font-medium">
{isLoading ? (
<><svg className="animate-spin w-4 h-4" fill="none" viewBox="0 0 24 24">
<circle className="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" strokeWidth="4" />
<path className="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z" />
</svg>{tab === 'consent' ? 'Teste Cookies...' : tab === 'scan' ? 'Scanne...' : 'Analysiere...'}</>
) : tab === 'consent' ? 'Cookie-Test starten' : tab === 'scan' ? 'Website scannen' : 'Analysieren'}
</svg>Analysiere...</>
) : TABS.find(t => t.id === tab)?.label || 'Starten'}
</button>
</form>
{/* Error */}
{currentError && (
<div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{currentError}</div>
)}
{currentError && <div className="bg-red-50 border border-red-200 rounded-lg p-4 text-sm text-red-700">{currentError}</div>}
{/* Quick Analysis Result */}
{/* Results */}
{tab === 'quick' && result && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm space-y-6">
<AnalysisResult result={result} />
{result.follow_up_questions.length > 0 && (
<div className="border-t pt-4">
<FollowUpQuestions questions={result.follow_up_questions} answers={result.follow_up_answers} onAnswer={answerFollowUp} />
</div>
<div className="border-t pt-4"><FollowUpQuestions questions={result.follow_up_questions} answers={result.follow_up_answers} onAnswer={answerFollowUp} /></div>
)}
</div>
)}
{/* Scan Result */}
{tab === 'scan' && scanData && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<ScanResult data={scanData} />
</div>
)}
{/* Consent Test Result */}
{tab === 'consent' && consentData && (
<div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm">
<ConsentTestResult data={consentData} />
</div>
)}
{tab === 'scan' && scanData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><ScanResult data={scanData} /></div>}
{tab === 'consent' && consentData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><ConsentTestResult data={consentData} /></div>}
{tab === 'compare' && compareData?.sites && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><CompareResult sites={compareData.sites} /></div>}
{tab === 'auth' && authData && <div className="bg-white border border-gray-200 rounded-xl p-6 shadow-sm"><AuthTestResult data={authData} /></div>}
{/* History */}
{tab === 'quick' && (
<AnalysisHistory history={history} onSelect={r => { setUrl(r.url); analyze(r.url, mode) }} />
)}
{tab === 'quick' && <AnalysisHistory history={history} onSelect={r => { setUrl(r.url); analyze(r.url, mode) }} />}
{tab === 'scan' && scanHistory.length > 0 && (
<div>
<h3 className="text-sm font-medium text-gray-700 mb-3">Letzte Scans</h3>
<div className="space-y-2">
{scanHistory.map((item, i) => (
<button key={i} onClick={() => setUrl(item.url)}
className="w-full text-left p-3 bg-white border border-gray-200 rounded-lg hover:border-purple-300 hover:bg-purple-50 transition-colors">
className="w-full text-left p-3 bg-white border border-gray-200 rounded-lg hover:border-purple-300 transition-colors">
<div className="flex items-center gap-3">
<span className="text-xs font-medium text-gray-500 w-8">{item.pages_scanned}p</span>
<span className="text-xs text-gray-500 w-8">{item.pages_scanned}p</span>
<span className="text-sm text-gray-700 truncate flex-1">{item.url}</span>
<span className={`text-xs px-2 py-0.5 rounded ${item.findings?.length > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>
{item.findings?.length || 0} Findings
</span>
<span className="text-xs text-gray-400">
{new Date(item.scanned_at).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' })}
</span>
<span className={`text-xs px-2 py-0.5 rounded ${item.findings?.length > 0 ? 'bg-red-100 text-red-700' : 'bg-green-100 text-green-700'}`}>{item.findings?.length || 0}</span>
</div>
</button>
))}