refactor(admin): split dsfa, audit-llm, quality pages

Extract components and hooks from oversized page files (563/561/520 LOC)
into colocated _components/ and _hooks/ subdirectories. All three
page.tsx files are now thin orchestrators under 300 LOC each
(dsfa: 216, audit-llm: 121, quality: 163). Zero behavior changes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Sharang Parnerkar
2026-04-16 13:20:17 +02:00
parent 653fa07f57
commit 519ffdc8dc
16 changed files with 1246 additions and 1206 deletions

View File

@@ -0,0 +1,128 @@
'use client'
import { StatCard } from './StatCard'
import { formatNumber, type ComplianceReport } from './types'
export function ComplianceTab({ complianceReport }: { complianceReport: ComplianceReport }) {
return (
<div>
{/* Summary Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<StatCard label="Requests" value={formatNumber(complianceReport.total_requests)} />
<StatCard
label="PII-Vorfaelle"
value={formatNumber(complianceReport.pii_incidents)}
highlight={complianceReport.pii_incidents > 0}
/>
<StatCard
label="PII-Rate"
value={`${(complianceReport.pii_rate * 100).toFixed(1)}%`}
highlight={complianceReport.pii_rate > 0.05}
/>
<StatCard label="Redaction-Rate" value={`${(complianceReport.redaction_rate * 100).toFixed(1)}%`} />
</div>
{complianceReport.policy_violations > 0 && (
<div className="mb-6 p-4 bg-red-50 border border-red-200 rounded-xl">
<div className="flex items-center gap-2 text-red-700 font-semibold">
<svg className="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2}
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z" />
</svg>
{complianceReport.policy_violations} Policy-Verletzungen im Zeitraum
</div>
</div>
)}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
{/* PII Categories */}
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">PII-Kategorien</h3>
{Object.entries(complianceReport.top_pii_categories || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine PII erkannt</p>
) : (
<div className="space-y-2">
{Object.entries(complianceReport.top_pii_categories).sort((a, b) => b[1] - a[1]).map(([cat, count]) => (
<div key={cat} className="flex items-center justify-between py-1">
<span className="text-sm text-gray-700">{cat}</span>
<span className="px-2 py-0.5 bg-red-50 text-red-700 rounded text-xs font-mono">{count}</span>
</div>
))}
</div>
)}
</div>
{/* Namespace Breakdown */}
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Namespace-Analyse</h3>
{Object.entries(complianceReport.namespace_breakdown || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine Namespace-Daten</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100">
<th className="text-left py-2 text-gray-500 font-medium">Namespace</th>
<th className="text-right py-2 text-gray-500 font-medium">Requests</th>
<th className="text-right py-2 text-gray-500 font-medium">PII</th>
</tr>
</thead>
<tbody>
{Object.entries(complianceReport.namespace_breakdown).map(([ns, data]) => (
<tr key={ns} className="border-b border-gray-50">
<td className="py-2 font-mono text-xs">{ns}</td>
<td className="py-2 text-right">{formatNumber(data.requests)}</td>
<td className="py-2 text-right">
{data.pii_incidents > 0 ? (
<span className="text-red-600 font-medium">{data.pii_incidents}</span>
) : (
<span className="text-gray-400">0</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</div>
{/* User Breakdown */}
{Object.entries(complianceReport.user_breakdown || {}).length > 0 && (
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Top-Nutzer</h3>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-100">
<th className="text-left py-2 text-gray-500 font-medium">User-ID</th>
<th className="text-right py-2 text-gray-500 font-medium">Requests</th>
<th className="text-right py-2 text-gray-500 font-medium">PII-Vorfaelle</th>
</tr>
</thead>
<tbody>
{Object.entries(complianceReport.user_breakdown)
.sort((a, b) => b[1].requests - a[1].requests)
.slice(0, 10)
.map(([userId, data]) => (
<tr key={userId} className="border-b border-gray-50">
<td className="py-2 font-mono text-xs">{userId}</td>
<td className="py-2 text-right">{formatNumber(data.requests)}</td>
<td className="py-2 text-right">
{data.pii_incidents > 0 ? (
<span className="text-red-600 font-medium">{data.pii_incidents}</span>
) : (
<span className="text-gray-400">0</span>
)}
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
)}
</div>
)
}

View File

@@ -0,0 +1,90 @@
'use client'
import { formatDate, formatNumber, formatDuration, type LLMLogEntry } from './types'
interface Props {
logEntries: LLMLogEntry[]
logFilter: { model: string; pii: string }
onFilterChange: (filter: { model: string; pii: string }) => void
}
export function LLMLogTab({ logEntries, logFilter, onFilterChange }: Props) {
return (
<div>
{/* Filters */}
<div className="flex gap-3 mb-4">
<input
type="text"
placeholder="Model filtern..."
value={logFilter.model}
onChange={e => onFilterChange({ ...logFilter, model: e.target.value })}
className="border border-gray-300 rounded-lg px-3 py-2 text-sm w-48"
/>
<select
value={logFilter.pii}
onChange={e => onFilterChange({ ...logFilter, pii: e.target.value })}
className="border border-gray-300 rounded-lg px-3 py-2 text-sm"
>
<option value="">Alle PII-Status</option>
<option value="true">PII erkannt</option>
<option value="false">Kein PII</option>
</select>
</div>
{/* Table */}
<div className="overflow-x-auto bg-white rounded-xl border border-gray-200">
<table className="w-full text-sm">
<thead>
<tr className="border-b border-gray-200 bg-gray-50">
<th className="text-left px-4 py-3 font-medium text-gray-600">Zeitpunkt</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">User</th>
<th className="text-left px-4 py-3 font-medium text-gray-600">Model</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Tokens</th>
<th className="text-center px-4 py-3 font-medium text-gray-600">PII</th>
<th className="text-right px-4 py-3 font-medium text-gray-600">Dauer</th>
<th className="text-center px-4 py-3 font-medium text-gray-600">Status</th>
</tr>
</thead>
<tbody>
{logEntries.length === 0 ? (
<tr>
<td colSpan={7} className="text-center py-8 text-gray-400">
Keine Log-Eintraege im gewaehlten Zeitraum
</td>
</tr>
) : logEntries.map(entry => (
<tr key={entry.id} className="border-b border-gray-100 hover:bg-gray-50">
<td className="px-4 py-3 text-gray-500">{formatDate(entry.created_at)}</td>
<td className="px-4 py-3 font-mono text-xs">{entry.user_id?.slice(0, 8)}...</td>
<td className="px-4 py-3">
<span className="px-2 py-0.5 bg-blue-50 text-blue-700 rounded text-xs font-medium">
{entry.model}
</span>
</td>
<td className="px-4 py-3 text-right font-mono">{formatNumber(entry.total_tokens)}</td>
<td className="px-4 py-3 text-center">
{entry.pii_detected ? (
<span className="px-2 py-0.5 bg-red-50 text-red-700 rounded text-xs font-medium">
{entry.redacted ? 'Redacted' : 'Erkannt'}
</span>
) : (
<span className="text-gray-400 text-xs">-</span>
)}
</td>
<td className="px-4 py-3 text-right text-gray-500">{formatDuration(entry.duration_ms)}</td>
<td className="px-4 py-3 text-center">
<span className={`px-2 py-0.5 rounded text-xs font-medium ${
entry.status === 'success' ? 'bg-green-50 text-green-700' : 'bg-red-50 text-red-700'
}`}>
{entry.status}
</span>
</td>
</tr>
))}
</tbody>
</table>
</div>
<div className="mt-2 text-xs text-gray-400">{logEntries.length} Eintraege</div>
</div>
)
}

View File

@@ -0,0 +1,10 @@
'use client'
export function StatCard({ label, value, highlight }: { label: string; value: string; highlight?: boolean }) {
return (
<div className={`rounded-xl border p-4 ${highlight ? 'border-red-200 bg-red-50' : 'border-gray-200 bg-white'}`}>
<div className="text-sm text-gray-500">{label}</div>
<div className={`text-2xl font-bold mt-1 ${highlight ? 'text-red-700' : 'text-gray-900'}`}>{value}</div>
</div>
)
}

View File

@@ -0,0 +1,84 @@
'use client'
import { StatCard } from './StatCard'
import { formatNumber, formatDuration, type UsageStats } from './types'
export function UsageTab({ usageStats }: { usageStats: UsageStats }) {
return (
<div>
{/* Stats Cards */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4 mb-6">
<StatCard label="Requests gesamt" value={formatNumber(usageStats.total_requests)} />
<StatCard label="Tokens gesamt" value={formatNumber(usageStats.total_tokens)} />
<StatCard label="Avg. Dauer" value={formatDuration(usageStats.avg_duration_ms)} />
<StatCard
label="PII-Rate"
value={`${(usageStats.pii_detection_rate * 100).toFixed(1)}%`}
highlight={usageStats.pii_detection_rate > 0.1}
/>
</div>
{/* Token Breakdown */}
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Model-Nutzung</h3>
{Object.entries(usageStats.models_used || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine Daten</p>
) : (
<div className="space-y-3">
{Object.entries(usageStats.models_used).sort((a, b) => b[1] - a[1]).map(([model, count]) => (
<div key={model} className="flex items-center justify-between">
<span className="text-sm text-gray-700">{model}</span>
<div className="flex items-center gap-3">
<div className="w-32 h-2 bg-gray-100 rounded-full overflow-hidden">
<div
className="h-full bg-purple-500 rounded-full"
style={{ width: `${(count / usageStats.total_requests) * 100}%` }}
/>
</div>
<span className="text-sm font-mono text-gray-500 w-16 text-right">{formatNumber(count)}</span>
</div>
</div>
))}
</div>
)}
</div>
<div className="bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Provider-Verteilung</h3>
{Object.entries(usageStats.providers_used || {}).length === 0 ? (
<p className="text-gray-400 text-sm">Keine Daten</p>
) : (
<div className="space-y-3">
{Object.entries(usageStats.providers_used).sort((a, b) => b[1] - a[1]).map(([provider, count]) => (
<div key={provider} className="flex items-center justify-between">
<span className="text-sm text-gray-700 capitalize">{provider}</span>
<span className="text-sm font-mono text-gray-500">{formatNumber(count)}</span>
</div>
))}
</div>
)}
</div>
</div>
{/* Token Details */}
<div className="mt-6 bg-white rounded-xl border border-gray-200 p-5">
<h3 className="font-semibold text-gray-900 mb-4">Token-Aufschluesselung</h3>
<div className="grid grid-cols-3 gap-4">
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">{formatNumber(usageStats.total_prompt_tokens)}</div>
<div className="text-sm text-gray-500">Prompt Tokens</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-gray-900">{formatNumber(usageStats.total_completion_tokens)}</div>
<div className="text-sm text-gray-500">Completion Tokens</div>
</div>
<div className="text-center">
<div className="text-2xl font-bold text-purple-600">{formatNumber(usageStats.total_tokens)}</div>
<div className="text-sm text-gray-500">Gesamt</div>
</div>
</div>
</div>
</div>
)
}

View File

@@ -0,0 +1,75 @@
export interface LLMLogEntry {
id: string
user_id: string
namespace: string
model: string
provider: string
prompt_tokens: number
completion_tokens: number
total_tokens: number
pii_detected: boolean
pii_categories: string[]
redacted: boolean
duration_ms: number
status: string
created_at: string
}
export interface UsageStats {
total_requests: number
total_tokens: number
total_prompt_tokens: number
total_completion_tokens: number
models_used: Record<string, number>
providers_used: Record<string, number>
avg_duration_ms: number
pii_detection_rate: number
period_start: string
period_end: string
}
export interface ComplianceReport {
total_requests: number
pii_incidents: number
pii_rate: number
redaction_rate: number
policy_violations: number
top_pii_categories: Record<string, number>
namespace_breakdown: Record<string, { requests: number; pii_incidents: number }>
user_breakdown: Record<string, { requests: number; pii_incidents: number }>
period_start: string
period_end: string
}
export type TabId = 'llm-log' | 'usage' | 'compliance'
export const API_BASE = '/api/sdk/v1/audit-llm'
export function formatDate(iso: string): string {
return new Date(iso).toLocaleString('de-DE', {
day: '2-digit', month: '2-digit', year: 'numeric',
hour: '2-digit', minute: '2-digit',
})
}
export function formatNumber(n: number): string {
return n.toLocaleString('de-DE')
}
export function formatDuration(ms: number): string {
if (ms < 1000) return `${ms}ms`
return `${(ms / 1000).toFixed(1)}s`
}
export function getDateRange(period: string): { from: string; to: string } {
const now = new Date()
const to = now.toISOString().slice(0, 10)
const from = new Date(now)
switch (period) {
case '7d': from.setDate(from.getDate() - 7); break
case '30d': from.setDate(from.getDate() - 30); break
case '90d': from.setDate(from.getDate() - 90); break
default: from.setDate(from.getDate() - 7)
}
return { from: from.toISOString().slice(0, 10), to }
}