All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 39s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 20s
CI/CD / test-python-dsms-gateway (push) Successful in 16s
CI/CD / validate-canonical-controls (push) Successful in 9s
CI/CD / Deploy (push) Successful in 4s
863 v1-Controls (manuell geschrieben, ohne Rechtsgrundlage) werden als "Eigenentwicklung" gekennzeichnet und automatisch mit regulatorischen Controls (DSGVO, NIS2, OWASP etc.) per Embedding-Similarity abgeglichen. Backend: - Migration 080: v1_control_matches Tabelle (Cross-Reference) - v1_enrichment.py: Batch-Matching via BGE-M3 + Qdrant (Threshold 0.75) - 3 neue API-Endpoints: enrich-v1-matches, v1-matches, v1-enrichment-stats - 6 Tests (dry-run, execution, matches, pagination, detection) Frontend: - Orange "Eigenentwicklung"-Badge statt grauem "v1" (wenn kein Source) - "Regulatorische Abdeckung"-Sektion im ControlDetail mit Match-Karten - Side-by-Side V1CompareView (Eigenentwicklung vs. regulatorisch gedeckt) - Prev/Next Navigation durch alle Matches Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
'use client'
|
|
|
|
import { useState, useEffect } from 'react'
|
|
import {
|
|
ArrowLeft, ChevronLeft, SkipForward, Scale,
|
|
} from 'lucide-react'
|
|
import { CanonicalControl, BACKEND_URL } from './helpers'
|
|
import { ControlPanel } from './ReviewCompare'
|
|
|
|
interface V1Match {
|
|
matched_control_id: string
|
|
matched_title: string
|
|
matched_objective: string
|
|
matched_severity: string
|
|
matched_category: string
|
|
matched_source: string | null
|
|
matched_article: string | null
|
|
matched_source_citation: Record<string, string> | null
|
|
similarity_score: number
|
|
match_rank: number
|
|
match_method: string
|
|
}
|
|
|
|
interface V1CompareViewProps {
|
|
v1Control: CanonicalControl
|
|
matches: V1Match[]
|
|
onBack: () => void
|
|
onNavigateToControl?: (controlId: string) => void
|
|
}
|
|
|
|
export function V1CompareView({ v1Control, matches, onBack, onNavigateToControl }: V1CompareViewProps) {
|
|
const [currentMatchIndex, setCurrentMatchIndex] = useState(0)
|
|
const [matchedControl, setMatchedControl] = useState<CanonicalControl | null>(null)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const currentMatch = matches[currentMatchIndex]
|
|
|
|
// Load the full matched control when index changes
|
|
useEffect(() => {
|
|
if (!currentMatch) return
|
|
const load = async () => {
|
|
setLoading(true)
|
|
try {
|
|
const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${encodeURIComponent(currentMatch.matched_control_id)}`)
|
|
if (res.ok) {
|
|
setMatchedControl(await res.json())
|
|
} else {
|
|
setMatchedControl(null)
|
|
}
|
|
} catch {
|
|
setMatchedControl(null)
|
|
} finally {
|
|
setLoading(false)
|
|
}
|
|
}
|
|
load()
|
|
}, [currentMatch])
|
|
|
|
return (
|
|
<div className="flex flex-col h-full">
|
|
{/* Header */}
|
|
<div className="border-b border-gray-200 bg-white px-6 py-3 flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<button onClick={onBack} className="text-gray-400 hover:text-gray-600">
|
|
<ArrowLeft className="w-5 h-5" />
|
|
</button>
|
|
<div>
|
|
<div className="flex items-center gap-2">
|
|
<Scale className="w-4 h-4 text-orange-500" />
|
|
<span className="text-sm font-semibold text-gray-900">V1-Vergleich</span>
|
|
{currentMatch && (
|
|
<span className={`text-xs font-medium px-2 py-0.5 rounded-full ${
|
|
currentMatch.similarity_score >= 0.85 ? 'bg-green-100 text-green-700' :
|
|
currentMatch.similarity_score >= 0.80 ? 'bg-yellow-100 text-yellow-700' :
|
|
'bg-gray-100 text-gray-600'
|
|
}`}>
|
|
{(currentMatch.similarity_score * 100).toFixed(1)}% Aehnlichkeit
|
|
</span>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div className="flex items-center gap-2">
|
|
{/* Navigation */}
|
|
<div className="flex items-center gap-1">
|
|
<button
|
|
onClick={() => setCurrentMatchIndex(Math.max(0, currentMatchIndex - 1))}
|
|
disabled={currentMatchIndex === 0}
|
|
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
|
|
>
|
|
<ChevronLeft className="w-4 h-4" />
|
|
</button>
|
|
<span className="text-xs text-gray-500 font-medium">
|
|
{currentMatchIndex + 1} / {matches.length}
|
|
</span>
|
|
<button
|
|
onClick={() => setCurrentMatchIndex(Math.min(matches.length - 1, currentMatchIndex + 1))}
|
|
disabled={currentMatchIndex >= matches.length - 1}
|
|
className="p-1 text-gray-400 hover:text-gray-600 disabled:opacity-30"
|
|
>
|
|
<SkipForward className="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
|
|
{/* Navigate to matched control */}
|
|
{onNavigateToControl && matchedControl && (
|
|
<button
|
|
onClick={() => { onBack(); onNavigateToControl(matchedControl.control_id) }}
|
|
className="px-3 py-1.5 text-sm text-purple-600 border border-purple-300 rounded-lg hover:bg-purple-50"
|
|
>
|
|
Zum Control
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
|
|
{/* Source info bar */}
|
|
{currentMatch && (currentMatch.matched_source || currentMatch.matched_article) && (
|
|
<div className="px-6 py-2 bg-blue-50 border-b border-blue-200 flex items-center gap-2 text-sm">
|
|
<Scale className="w-3.5 h-3.5 text-blue-600" />
|
|
{currentMatch.matched_source && (
|
|
<span className="font-semibold text-blue-900">{currentMatch.matched_source}</span>
|
|
)}
|
|
{currentMatch.matched_article && (
|
|
<span className="text-blue-700">{currentMatch.matched_article}</span>
|
|
)}
|
|
</div>
|
|
)}
|
|
|
|
{/* Side-by-Side Panels */}
|
|
<div className="flex-1 flex overflow-hidden">
|
|
{/* Left: V1 Eigenentwicklung */}
|
|
<div className="w-1/2 border-r border-gray-200 overflow-y-auto">
|
|
<ControlPanel ctrl={v1Control} label="Eigenentwicklung" highlight />
|
|
</div>
|
|
|
|
{/* Right: Regulatory match */}
|
|
<div className="w-1/2 overflow-y-auto">
|
|
{loading ? (
|
|
<div className="flex items-center justify-center h-full">
|
|
<div className="animate-spin rounded-full h-6 w-6 border-2 border-purple-600 border-t-transparent" />
|
|
</div>
|
|
) : matchedControl ? (
|
|
<ControlPanel ctrl={matchedControl} label="Regulatorisch gedeckt" />
|
|
) : (
|
|
<div className="flex items-center justify-center h-full text-gray-400 text-sm">
|
|
Control konnte nicht geladen werden
|
|
</div>
|
|
)}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)
|
|
}
|