Files
breakpilot-compliance/admin-compliance/app/sdk/iace/[projectId]/benchmark/page.tsx
T
Benjamin Admin 8bb90d73e5
Build + Deploy / build-admin-compliance (push) Successful in 2m7s
Build + Deploy / build-backend-compliance (push) Successful in 3m34s
Build + Deploy / build-ai-sdk (push) Successful in 1m6s
Build + Deploy / build-developer-portal (push) Successful in 1m7s
Build + Deploy / build-tts (push) Successful in 1m58s
Build + Deploy / build-document-crawler (push) Successful in 57s
Build + Deploy / build-dsms-gateway (push) Successful in 34s
Build + Deploy / build-dsms-node (push) Successful in 29s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / loc-budget (push) Failing after 17s
CI / secret-scan (push) Has been skipped
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m28s
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / test-go (push) Successful in 42s
CI / test-python-backend (push) Successful in 37s
CI / test-python-document-crawler (push) Successful in 27s
CI / test-python-dsms-gateway (push) Successful in 22s
CI / validate-canonical-controls (push) Successful in 15s
Build + Deploy / trigger-orca (push) Successful in 3m10s
feat(iace): benchmark system + erklaerteil + dedup-fix
- Erklaerteil-Template fuer Risikobeurteilungen (risk_assessment_template.go)
  in PDF-Export, Markdown-Export und Frontend ReportPrintView eingebaut
- Ground Truth Benchmark-System: Datenmodell, Fuzzy-Matching-Engine,
  3 API Endpoints (import-gt, benchmark, benchmark/summary)
- Frontend Benchmark-Tab mit Score-Cards, Kategorie-Breakdown,
  Hazard-Vergleichstabelle (Zugeordnet/Fehlend/Extra), Business Impact
- Erster Benchmark: 13.3% Coverage (Baseline) gegen 60 GT-Eintraege
- Dedup-Fix: seenCat[cat] -> seenCatZone[cat+zone] erlaubt mehrere
  Gefaehrdungen pro Kategorie an verschiedenen Gefahrenstellen
- Komponenten-spezifische Hazard-Namen und Zone-basierte Zuordnung

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-13 01:02:33 +02:00

161 lines
6.4 KiB
TypeScript

'use client'
import React, { useState } from 'react'
import { useParams } from 'next/navigation'
import { useBenchmark } from './_hooks/useBenchmark'
import { GTImportForm } from './_components/GTImportForm'
import { HazardComparisonTable } from './_components/HazardComparisonTable'
import { CategoryBreakdown } from './_components/CategoryBreakdown'
export default function BenchmarkPage() {
const { projectId } = useParams<{ projectId: string }>()
const { result, gtLoaded, gtEntryCount, loading, error, importGT, runBenchmark } = useBenchmark(projectId)
const [gtProjectId, setGtProjectId] = useState('')
const coveragePct = result ? Math.round(result.coverage_score * 100) : 0
const measurePct = result ? Math.round(result.measure_coverage * 100) : 0
return (
<div className="space-y-6 max-w-[1200px]">
{/* Header */}
<div>
<h1 className="text-lg font-bold text-gray-900 dark:text-white">Ground Truth Benchmark</h1>
<p className="text-sm text-gray-500 mt-1">
Vergleich der Engine-Ergebnisse mit einer professionellen Risikobeurteilung
</p>
</div>
{error && (
<div className="px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 rounded-lg text-sm text-red-600">
{error}
</div>
)}
{/* GT Import or Cross-Project Reference */}
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4">
<GTImportForm onImport={importGT} loading={loading} />
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Benchmark ausfuehren</h3>
<p className="text-xs text-gray-500 mb-3">
GT aus diesem Projekt verwenden, oder eine Projekt-ID mit importierter GT angeben.
</p>
<div className="space-y-2">
<input
type="text"
value={gtProjectId}
onChange={(e) => setGtProjectId(e.target.value)}
placeholder="GT-Projekt-ID (optional — leer = dieses Projekt)"
className="w-full text-xs border border-gray-300 dark:border-gray-600 rounded-md px-3 py-2 bg-gray-50 dark:bg-gray-900"
/>
<button
onClick={() => runBenchmark(gtProjectId || undefined)}
disabled={loading}
className="w-full px-4 py-2 text-sm font-medium bg-purple-600 hover:bg-purple-700 disabled:bg-gray-300 text-white rounded-md transition-colors"
>
{loading ? 'Vergleiche...' : 'Benchmark starten'}
</button>
</div>
{gtLoaded && !result && (
<div className="mt-3 px-3 py-2 bg-blue-50 dark:bg-blue-900/20 rounded text-xs text-blue-600">
{gtEntryCount} GT-Eintraege geladen. Klicke &quot;Benchmark starten&quot;.
</div>
)}
</div>
</div>
{/* Results */}
{result && (
<>
{/* Score Cards */}
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
<ScoreCard
label="Hazard Coverage"
value={`${coveragePct}%`}
sub={`${result.matched_pairs?.length || 0} / ${result.total_gt} erkannt`}
color={coveragePct >= 80 ? 'green' : coveragePct >= 50 ? 'yellow' : 'red'}
/>
<ScoreCard
label="Massnahmen-Coverage"
value={`${measurePct}%`}
sub="der zugeordneten Gefaehrdungen"
color={measurePct >= 80 ? 'green' : measurePct >= 50 ? 'yellow' : 'red'}
/>
<ScoreCard
label="GT Eintraege"
value={String(result.total_gt)}
sub="professionelle Beurteilung"
color="gray"
/>
<ScoreCard
label="Engine Eintraege"
value={String(result.total_engine)}
sub={`${result.extra_in_engine?.length || 0} zusaetzlich`}
color="gray"
/>
</div>
{/* Category Breakdown */}
<CategoryBreakdown breakdown={result.category_breakdown || []} />
{/* Hazard Comparison Table */}
<HazardComparisonTable
matched={result.matched_pairs || []}
missing={result.missing_from_engine || []}
extra={result.extra_in_engine || []}
/>
{/* Business Impact */}
<div className="bg-white dark:bg-gray-800 rounded-lg border border-gray-200 dark:border-gray-700 p-4">
<h3 className="text-sm font-semibold text-gray-700 dark:text-gray-300 mb-2">Business Impact</h3>
<div className="grid grid-cols-3 gap-4 text-center">
<div>
<div className="text-2xl font-bold text-gray-900 dark:text-white">2,5 Tage</div>
<div className="text-xs text-gray-500">Manueller Aufwand</div>
</div>
<div>
<div className="text-2xl font-bold text-purple-600">
{(coveragePct / 100 * 2.5).toFixed(1)} Tage
</div>
<div className="text-xs text-gray-500">Eingespart bei {coveragePct}% Coverage</div>
</div>
<div>
<div className="text-2xl font-bold text-green-600">
{Math.round(coveragePct / 100 * 2.5 * 8 * 100)} EUR
</div>
<div className="text-xs text-gray-500">Einsparung (100 EUR/h)</div>
</div>
</div>
</div>
</>
)}
</div>
)
}
function ScoreCard({ label, value, sub, color }: {
label: string; value: string; sub: string
color: 'green' | 'yellow' | 'red' | 'gray'
}) {
const colors = {
green: 'border-green-200 dark:border-green-800',
yellow: 'border-yellow-200 dark:border-yellow-800',
red: 'border-red-200 dark:border-red-800',
gray: 'border-gray-200 dark:border-gray-700',
}
const textColors = {
green: 'text-green-600', yellow: 'text-yellow-600',
red: 'text-red-600', gray: 'text-gray-900 dark:text-white',
}
return (
<div className={`bg-white dark:bg-gray-800 rounded-lg border-2 ${colors[color]} p-4 text-center`}>
<div className={`text-2xl font-bold ${textColors[color]}`}>{value}</div>
<div className="text-xs font-medium text-gray-700 dark:text-gray-300 mt-1">{label}</div>
<div className="text-[10px] text-gray-400 mt-0.5">{sub}</div>
</div>
)
}