'use client' import { useState, useEffect, useCallback } from 'react' import { ArrowLeft, ExternalLink, BookOpen, Scale, FileText, Eye, CheckCircle2, Trash2, Pencil, Clock, ChevronLeft, SkipForward, GitMerge, Search, Landmark, } from 'lucide-react' import { CanonicalControl, EFFORT_LABELS, BACKEND_URL, SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, ObligationTypeBadge, GenerationStrategyBadge, ExtractionMethodBadge, RegulationCountBadge, VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, ObligationInfo, DocumentReference, MergedDuplicate, RegulationSummary, } from './helpers' interface SimilarControl { control_id: string title: string severity: string release_state: string tags: string[] license_rule: number | null verification_method: string | null category: string | null similarity: number } interface ParentLink { parent_control_id: string parent_title: string link_type: string confidence: number source_regulation: string | null source_article: string | null parent_citation: Record | null obligation: { text: string action: string object: string normative_strength: string } | null } interface TraceabilityData { control_id: string title: string is_atomic: boolean parent_links: ParentLink[] children: Array<{ control_id: string title: string category: string severity: string decomposition_method: string }> source_count: number // Extended provenance fields obligations?: ObligationInfo[] obligation_count?: number document_references?: DocumentReference[] merged_duplicates?: MergedDuplicate[] merged_duplicates_count?: number regulations_summary?: RegulationSummary[] } interface ControlDetailProps { ctrl: CanonicalControl onBack: () => void onEdit: () => void onDelete: (controlId: string) => void onReview: (controlId: string, action: string) => void onRefresh?: () => void onNavigateToControl?: (controlId: string) => void // Review mode navigation reviewMode?: boolean reviewIndex?: number reviewTotal?: number onReviewPrev?: () => void onReviewNext?: () => void } export function ControlDetail({ ctrl, onBack, onEdit, onDelete, onReview, onRefresh, onNavigateToControl, reviewMode, reviewIndex = 0, reviewTotal = 0, onReviewPrev, onReviewNext, }: ControlDetailProps) { const [similarControls, setSimilarControls] = useState([]) const [loadingSimilar, setLoadingSimilar] = useState(false) const [selectedDuplicates, setSelectedDuplicates] = useState>(new Set()) const [merging, setMerging] = useState(false) const [traceability, setTraceability] = useState(null) const [loadingTrace, setLoadingTrace] = useState(false) const loadTraceability = useCallback(async () => { setLoadingTrace(true) try { // Try provenance first (extended data), fall back to traceability let res = await fetch(`${BACKEND_URL}?endpoint=provenance&id=${ctrl.control_id}`) if (!res.ok) { res = await fetch(`${BACKEND_URL}?endpoint=traceability&id=${ctrl.control_id}`) } if (res.ok) { setTraceability(await res.json()) } } catch { /* ignore */ } finally { setLoadingTrace(false) } }, [ctrl.control_id]) useEffect(() => { loadSimilarControls() loadTraceability() setSelectedDuplicates(new Set()) // eslint-disable-next-line react-hooks/exhaustive-deps }, [ctrl.control_id]) const loadSimilarControls = async () => { setLoadingSimilar(true) try { const res = await fetch(`${BACKEND_URL}?endpoint=similar&id=${ctrl.control_id}`) if (res.ok) { setSimilarControls(await res.json()) } } catch { /* ignore */ } finally { setLoadingSimilar(false) } } const toggleDuplicate = (controlId: string) => { setSelectedDuplicates(prev => { const next = new Set(prev) if (next.has(controlId)) next.delete(controlId) else next.add(controlId) return next }) } const handleMergeDuplicates = async () => { if (selectedDuplicates.size === 0) return if (!confirm(`${selectedDuplicates.size} Controls als Duplikate markieren und Tags/Anchors in ${ctrl.control_id} zusammenfuehren?`)) return setMerging(true) try { // For each duplicate: mark as deprecated for (const dupId of selectedDuplicates) { await fetch(`${BACKEND_URL}?endpoint=update-control&id=${dupId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ release_state: 'deprecated' }), }) } // Refresh to show updated state if (onRefresh) onRefresh() setSelectedDuplicates(new Set()) loadSimilarControls() } catch { alert('Fehler beim Zusammenfuehren') } finally { setMerging(false) } } return (
{/* Header */}
{ctrl.control_id}

{ctrl.title}

{reviewMode && (
{reviewIndex + 1} / {reviewTotal}
)}
{/* Content */}
{/* Objective */}

Ziel

{ctrl.objective}

{/* Rationale */}

Begruendung

{ctrl.rationale}

{/* Quellennachweis (Rule 1 + 2) — dynamic label based on source_type */} {ctrl.source_citation && (

{ ctrl.source_citation.source_type === 'law' ? 'Gesetzliche Grundlage' : ctrl.source_citation.source_type === 'guideline' ? 'Behoerdliche Leitlinie' : 'Standard / Best Practice' }

{ctrl.source_citation.source_type === 'law' && ( Direkte gesetzliche Pflicht )} {ctrl.source_citation.source_type === 'guideline' && ( Aufsichtsbehoerdliche Empfehlung )} {(ctrl.source_citation.source_type === 'standard' || (!ctrl.source_citation.source_type && ctrl.license_rule === 2)) && ( Freiwilliger Standard )} {(!ctrl.source_citation.source_type && ctrl.license_rule === 1) && ( Noch nicht klassifiziert )}
{ctrl.source_citation.source ? (

{ctrl.source_citation.source} {ctrl.source_citation.article && ` — ${ctrl.source_citation.article}`} {ctrl.source_citation.paragraph && ` ${ctrl.source_citation.paragraph}`}

) : ctrl.generation_metadata?.source_regulation ? (

{String(ctrl.generation_metadata.source_regulation)}

) : null} {ctrl.source_citation.license && (

Lizenz: {ctrl.source_citation.license}

)} {ctrl.source_citation.license_notice && (

{ctrl.source_citation.license_notice}

)}
{ctrl.source_citation.url && ( Quelle )}
{ctrl.source_original_text && (
Originaltext anzeigen

{ctrl.source_original_text}

)}
)} {/* Rechtsgrundlagen / Traceability (atomic controls) */} {traceability && traceability.parent_links.length > 0 && (

Rechtsgrundlagen ({traceability.source_count} {traceability.source_count === 1 ? 'Quelle' : 'Quellen'})

{traceability.regulations_summary && traceability.regulations_summary.map(rs => ( {rs.regulation_code} ))} {loadingTrace && Laden...}
{traceability.parent_links.map((link, i) => (
{link.source_regulation && ( {link.source_regulation} )} {link.source_article && ( {link.source_article} )} {!link.source_regulation && link.parent_citation?.source && ( {link.parent_citation.source} {link.parent_citation.article && ` — ${link.parent_citation.article}`} )} {link.link_type === 'decomposition' ? 'Ableitung' : link.link_type === 'dedup_merge' ? 'Dedup' : link.link_type}

via{' '} {onNavigateToControl ? ( ) : ( {link.parent_control_id} )} {link.parent_title && ( — {link.parent_title} )}

{link.obligation && (

{link.obligation.normative_strength === 'must' ? 'MUSS' : link.obligation.normative_strength === 'should' ? 'SOLL' : 'KANN'} {link.obligation.text.slice(0, 200)} {link.obligation.text.length > 200 ? '...' : ''}

)}
))}
)} {/* Fallback: simple parent display when traceability not loaded yet */} {ctrl.parent_control_uuid && (!traceability || traceability.parent_links.length === 0) && !loadingTrace && (

Atomares Control

Abgeleitet aus Eltern-Control{' '} {ctrl.parent_control_id || ctrl.parent_control_uuid} {ctrl.parent_control_title && ( — {ctrl.parent_control_title} )}

)} {/* Document References (atomic controls) */} {traceability && traceability.is_atomic && traceability.document_references && traceability.document_references.length > 0 && (

Original-Dokumente ({traceability.document_references.length})

{traceability.document_references.map((dr, i) => (
{dr.regulation_code} {dr.article && {dr.article}} {dr.paragraph && {dr.paragraph}} {dr.confidence !== null && ( {(dr.confidence * 100).toFixed(0)}% )}
))}
)} {/* Obligations (rich controls) */} {traceability && !traceability.is_atomic && traceability.obligations && traceability.obligations.length > 0 && (

Abgeleitete Pflichten ({traceability.obligation_count ?? traceability.obligations.length})

{traceability.obligations.map((ob) => (
{ob.candidate_id} {ob.normative_strength === 'must' ? 'MUSS' : ob.normative_strength === 'should' ? 'SOLL' : 'KANN'} {ob.action && {ob.action}} {ob.object && → {ob.object}}

{ob.obligation_text.slice(0, 300)} {ob.obligation_text.length > 300 ? '...' : ''}

))}
)} {/* Merged Duplicates */} {traceability && traceability.merged_duplicates && traceability.merged_duplicates.length > 0 && (

Zusammengefuehrte Duplikate ({traceability.merged_duplicates_count ?? traceability.merged_duplicates.length})

{traceability.merged_duplicates.map((dup) => (
{onNavigateToControl ? ( ) : ( {dup.control_id} )} {dup.title} {dup.source_regulation && ( {dup.source_regulation} )}
))}
)} {/* Child controls (rich controls that have atomic children) */} {traceability && traceability.children.length > 0 && (

Abgeleitete Controls ({traceability.children.length})

{traceability.children.map((child) => (
{onNavigateToControl ? ( ) : ( {child.control_id} )} {child.title}
))}
)} {/* Impliziter Gesetzesbezug (Rule 3 — reformuliert, kein Originaltext) */} {!ctrl.source_citation && ctrl.open_anchors.length > 0 && (

Abgeleitet aus regulatorischen Anforderungen

Dieser Control wurde aus geschuetzten Quellen reformuliert (z.B. BSI Grundschutz, ISO 27001). Die konkreten Massnahmen leiten sich aus den Open-Source-Referenzen unten ab.

)} {/* Scope */} {(ctrl.scope.platforms?.length || ctrl.scope.components?.length || ctrl.scope.data_classes?.length) ? (

Geltungsbereich

{ctrl.scope.platforms?.length ? (
Plattformen: {ctrl.scope.platforms.join(', ')}
) : null} {ctrl.scope.components?.length ? (
Komponenten: {ctrl.scope.components.join(', ')}
) : null} {ctrl.scope.data_classes?.length ? (
Datenklassen: {ctrl.scope.data_classes.join(', ')}
) : null}
) : null} {/* Requirements */} {ctrl.requirements.length > 0 && (

Anforderungen

    {ctrl.requirements.map((r, i) => (
  1. {r}
  2. ))}
)} {/* Test Procedure */} {ctrl.test_procedure.length > 0 && (

Pruefverfahren

    {ctrl.test_procedure.map((s, i) => (
  1. {s}
  2. ))}
)} {/* Evidence — handles both {type, description} objects and plain strings */} {ctrl.evidence.length > 0 && (

Nachweise

{ctrl.evidence.map((ev, i) => (
{typeof ev === 'string' ? (
{ev}
) : (
{ev.type}: {ev.description}
)}
))}
)} {/* Meta */}
{ctrl.risk_score !== null &&
Risiko-Score: {ctrl.risk_score}
} {ctrl.implementation_effort &&
Aufwand: {EFFORT_LABELS[ctrl.implementation_effort] || ctrl.implementation_effort}
} {ctrl.tags.length > 0 && (
{ctrl.tags.map(t => ( {t} ))}
)}
{/* Open Anchors */}

Open-Source-Referenzen ({ctrl.open_anchors.length})

{ctrl.open_anchors.length > 0 ? (
{ctrl.open_anchors.map((anchor, i) => (
{anchor.framework} {anchor.ref} {anchor.url && ( Link )}
))}
) : (

Keine Referenzen vorhanden.

)}
{/* Generation Metadata (internal) */} {ctrl.generation_metadata && (

Generierungsdetails (intern)

{ctrl.generation_metadata.processing_path && (

Pfad: {String(ctrl.generation_metadata.processing_path)}

)} {ctrl.generation_metadata.decomposition_method && (

Methode: {String(ctrl.generation_metadata.decomposition_method)}

)} {ctrl.generation_metadata.pass0b_model && (

LLM: {String(ctrl.generation_metadata.pass0b_model)}

)} {ctrl.generation_metadata.obligation_type && (

Obligation-Typ: {String(ctrl.generation_metadata.obligation_type)}

)} {ctrl.generation_metadata.similarity_status && (

Similarity: {String(ctrl.generation_metadata.similarity_status)}

)} {Array.isArray(ctrl.generation_metadata.similar_controls) && (

Aehnliche Controls:

{(ctrl.generation_metadata.similar_controls as Array>).map((s, i) => (

{String(s.control_id)} — {String(s.title)} ({String(s.similarity)})

))}
)}
)} {/* Similar Controls (Dedup) */}

Aehnliche Controls

{loadingSimilar && Laden...}
{similarControls.length > 0 ? ( <>
{ctrl.control_id} — {ctrl.title} Behalten (Haupt-Control)
{similarControls.map(sim => (
toggleDuplicate(sim.control_id)} className="text-red-600" /> {sim.control_id} {sim.title} {(sim.similarity * 100).toFixed(1)}%
))}
{selectedDuplicates.size > 0 && ( )} ) : (

{loadingSimilar ? 'Suche aehnliche Controls...' : 'Keine aehnlichen Controls gefunden.'}

)}
{/* Review Actions */} {['needs_review', 'too_close', 'duplicate'].includes(ctrl.release_state) && (

Review erforderlich

{reviewMode && ( Review-Modus aktiv )}
)}
) }