Files
breakpilot-compliance/admin-compliance/app/sdk/agent/_components/ScanResult.tsx
Benjamin Admin 0f1fae61a6 feat: Website-Scan tab in agent UI — service table, SOLL/IST, corrections
- Tab system: Schnellanalyse (single page) + Website-Scan (multi-page)
- ScanResult component: service comparison table, severity-colored findings
- Expandable correction suggestions with copy button (pre-launch mode)
- API proxy route for /agent/scan endpoint

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-28 15:52:40 +02:00

171 lines
7.0 KiB
TypeScript

'use client'
import React, { useState } from 'react'
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 ScanFinding {
code: string
severity: string
text: string
correction: string
}
interface ScanData {
pages_scanned: number
services: ServiceInfo[]
findings: ScanFinding[]
ai_detected: boolean
chatbot_detected: boolean
chatbot_provider: string
missing_pages: Record<string, number>
email_status: string
}
const STATUS_ICON: Record<string, { icon: string; color: string }> = {
ok: { icon: '✓', color: 'text-green-600' },
undocumented: { icon: '✗', color: 'text-red-600' },
outdated: { icon: '~', color: 'text-yellow-600' },
}
const SEV_STYLE: Record<string, { bg: string; text: string }> = {
HIGH: { bg: 'bg-red-50 border-red-200', text: 'text-red-800' },
MEDIUM: { bg: 'bg-yellow-50 border-yellow-200', text: 'text-yellow-800' },
LOW: { bg: 'bg-blue-50 border-blue-200', text: 'text-blue-800' },
}
export function ScanResult({ data }: { data: ScanData }) {
const [expandedCorrection, setExpandedCorrection] = useState<string | null>(null)
const undocCount = data.services.filter(s => s.status === 'undocumented').length
const okCount = data.services.filter(s => s.status === 'ok').length
const outdatedCount = data.services.filter(s => s.status === 'outdated').length
const highCount = data.findings.filter(f => f.severity === 'HIGH').length
return (
<div className="space-y-5">
{/* Summary Bar */}
<div className="grid grid-cols-4 gap-3">
<div className="bg-gray-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-gray-900">{data.pages_scanned}</p>
<p className="text-xs text-gray-500">Seiten gescannt</p>
</div>
<div className="bg-green-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-green-700">{okCount}</p>
<p className="text-xs text-gray-500">Dokumentiert</p>
</div>
<div className="bg-red-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-red-700">{undocCount}</p>
<p className="text-xs text-gray-500">Nicht in DSE</p>
</div>
<div className="bg-yellow-50 rounded-lg p-3 text-center">
<p className="text-2xl font-bold text-yellow-700">{outdatedCount}</p>
<p className="text-xs text-gray-500">Veraltet</p>
</div>
</div>
{/* AI / Chatbot Detection */}
<div className="flex gap-3">
<span className={`px-3 py-1 rounded-full text-xs font-medium ${data.ai_detected ? 'bg-purple-100 text-purple-800' : 'bg-gray-100 text-gray-600'}`}>
{data.ai_detected ? 'KI erkannt' : 'Keine KI erkannt'}
</span>
<span className={`px-3 py-1 rounded-full text-xs font-medium ${data.chatbot_detected ? 'bg-blue-100 text-blue-800' : 'bg-gray-100 text-gray-600'}`}>
{data.chatbot_detected ? `Chatbot: ${data.chatbot_provider}` : 'Kein Chatbot'}
</span>
</div>
{/* Services Table */}
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">Dienstleister-Abgleich (SOLL/IST)</h4>
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Status</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Dienst</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">Land</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">EU</th>
<th className="px-3 py-2 text-left text-xs font-medium text-gray-500">In DSE</th>
</tr>
</thead>
<tbody className="divide-y divide-gray-100">
{data.services.map((s, i) => {
const st = STATUS_ICON[s.status] || STATUS_ICON.ok
return (
<tr key={i} className={s.status === 'undocumented' ? 'bg-red-50' : ''}>
<td className={`px-3 py-2 font-bold ${st.color}`}>{st.icon}</td>
<td className="px-3 py-2">
<span className="font-medium text-gray-900">{s.name}</span>
<span className="text-gray-400 text-xs ml-2">{s.category}</span>
</td>
<td className="px-3 py-2 text-gray-600">{s.country}</td>
<td className="px-3 py-2">{s.eu_adequate ? '✓' : '✗'}</td>
<td className="px-3 py-2">{s.in_dse ? 'Ja' : <span className="text-red-600 font-medium">Nein</span>}</td>
</tr>
)
})}
</tbody>
</table>
</div>
</div>
{/* Findings */}
{data.findings.length > 0 && (
<div>
<h4 className="text-sm font-medium text-gray-700 mb-2">
Findings ({data.findings.length}, davon {highCount} kritisch)
</h4>
<div className="space-y-2">
{data.findings.map((f, i) => {
const sev = SEV_STYLE[f.severity] || SEV_STYLE.MEDIUM
const isExpanded = expandedCorrection === f.code
return (
<div key={i} className={`border rounded-lg p-3 ${sev.bg}`}>
<div className="flex items-start gap-2">
<span className={`text-xs font-bold px-2 py-0.5 rounded ${sev.text} bg-white`}>
{f.severity}
</span>
<p className="text-sm text-gray-800 flex-1">{f.text}</p>
</div>
{f.correction && (
<div className="mt-2">
<button
onClick={() => setExpandedCorrection(isExpanded ? null : f.code)}
className="text-xs text-purple-600 hover:text-purple-800 font-medium"
>
{isExpanded ? '▼ Korrekturvorschlag ausblenden' : '▶ Korrekturvorschlag anzeigen'}
</button>
{isExpanded && (
<div className="mt-2 bg-white border border-gray-200 rounded-lg p-3 relative">
<pre className="text-xs text-gray-700 whitespace-pre-wrap font-sans">{f.correction}</pre>
<button
onClick={() => navigator.clipboard.writeText(f.correction)}
className="absolute top-2 right-2 text-xs bg-gray-100 hover:bg-gray-200 px-2 py-1 rounded"
title="Kopieren"
>
Kopieren
</button>
</div>
)}
</div>
)}
</div>
)
})}
</div>
</div>
)}
</div>
)
}