'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { Shield, Search, ChevronRight, ChevronLeft, Filter, Lock, BookOpen, Plus, Zap, BarChart3, ListChecks, Trash2, ChevronsLeft, ChevronsRight, ArrowUpDown, Clock, RefreshCw, } from 'lucide-react' import { CanonicalControl, Framework, BACKEND_URL, EMPTY_CONTROL, SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, EvidenceTypeBadge, TargetAudienceBadge, GenerationStrategyBadge, ObligationTypeBadge, VERIFICATION_METHODS, CATEGORY_OPTIONS, EVIDENCE_TYPE_OPTIONS, TARGET_AUDIENCE_OPTIONS, } from './components/helpers' import { ControlForm } from './components/ControlForm' import { ControlDetail } from './components/ControlDetail' import { ReviewCompare } from './components/ReviewCompare' import { V1CompareView } from './components/V1CompareView' import { GeneratorModal } from './components/GeneratorModal' // ============================================================================= // TYPES // ============================================================================= interface ControlsMeta { total: number domains: Array<{ domain: string; count: number }> sources: Array<{ source: string; count: number }> no_source_count: number } // ============================================================================= // CONTROL LIBRARY PAGE — Server-Side Pagination // ============================================================================= const PAGE_SIZE = 50 export default function ControlLibraryPage() { const [frameworks, setFrameworks] = useState([]) const [controls, setControls] = useState([]) const [totalCount, setTotalCount] = useState(0) const [meta, setMeta] = useState(null) const [selectedControl, setSelectedControl] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filters const [searchQuery, setSearchQuery] = useState('') const [debouncedSearch, setDebouncedSearch] = useState('') const [severityFilter, setSeverityFilter] = useState('') const [domainFilter, setDomainFilter] = useState('') const [stateFilter, setStateFilter] = useState('') const [verificationFilter, setVerificationFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') const [evidenceTypeFilter, setEvidenceTypeFilter] = useState('') const [audienceFilter, setAudienceFilter] = useState('') const [sourceFilter, setSourceFilter] = useState('') const [typeFilter, setTypeFilter] = useState('') const [hideDuplicates, setHideDuplicates] = useState(true) const [sortBy, setSortBy] = useState<'id' | 'newest' | 'oldest' | 'source'>('id') // CRUD state const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list') const [saving, setSaving] = useState(false) // Generator state const [showGenerator, setShowGenerator] = useState(false) const [processedStats, setProcessedStats] = useState>>([]) const [showStats, setShowStats] = useState(false) // Pagination const [currentPage, setCurrentPage] = useState(1) // Review mode const [reviewMode, setReviewMode] = useState(false) const [reviewIndex, setReviewIndex] = useState(0) const [reviewItems, setReviewItems] = useState([]) const [reviewCount, setReviewCount] = useState(0) const [reviewTab, setReviewTab] = useState<'duplicates' | 'rule3'>('duplicates') const [reviewDuplicates, setReviewDuplicates] = useState([]) const [reviewRule3, setReviewRule3] = useState([]) // V1 Compare mode const [compareMode, setCompareMode] = useState(false) const [compareV1Control, setCompareV1Control] = useState(null) const [compareMatches, setCompareMatches] = useState | null similarity_score: number; match_rank: number; match_method: string }>>([]) // Debounce search const searchTimer = useRef | null>(null) useEffect(() => { if (searchTimer.current) clearTimeout(searchTimer.current) searchTimer.current = setTimeout(() => setDebouncedSearch(searchQuery), 400) return () => { if (searchTimer.current) clearTimeout(searchTimer.current) } }, [searchQuery]) // Build query params for backend const buildParams = useCallback((extra?: Record) => { const p = new URLSearchParams() if (severityFilter) p.set('severity', severityFilter) if (domainFilter) p.set('domain', domainFilter) if (stateFilter) p.set('release_state', stateFilter) if (verificationFilter) p.set('verification_method', verificationFilter) if (categoryFilter) p.set('category', categoryFilter) if (evidenceTypeFilter) p.set('evidence_type', evidenceTypeFilter) if (audienceFilter) p.set('target_audience', audienceFilter) if (sourceFilter) p.set('source', sourceFilter) if (typeFilter) p.set('control_type', typeFilter) if (hideDuplicates) p.set('exclude_duplicates', 'true') if (debouncedSearch) p.set('search', debouncedSearch) if (extra) for (const [k, v] of Object.entries(extra)) p.set(k, v) return p.toString() }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) // Load metadata (domains, sources — once + on refresh) const loadMeta = useCallback(async () => { try { const [fwRes, metaRes] = await Promise.all([ fetch(`${BACKEND_URL}?endpoint=frameworks`), fetch(`${BACKEND_URL}?endpoint=controls-meta`), ]) if (fwRes.ok) setFrameworks(await fwRes.json()) if (metaRes.ok) setMeta(await metaRes.json()) } catch { /* ignore */ } }, []) // Load controls page const loadControls = useCallback(async () => { try { setLoading(true) // Determine sort const sortField = sortBy === 'id' ? 'control_id' : sortBy === 'source' ? 'source' : 'created_at' const sortOrder = sortBy === 'newest' ? 'desc' : sortBy === 'oldest' ? 'asc' : 'asc' const offset = (currentPage - 1) * PAGE_SIZE const qs = buildParams({ sort: sortField, order: sortOrder, limit: String(PAGE_SIZE), offset: String(offset), }) const countQs = buildParams() const [ctrlRes, countRes] = await Promise.all([ fetch(`${BACKEND_URL}?endpoint=controls&${qs}`), fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`), ]) if (ctrlRes.ok) setControls(await ctrlRes.json()) if (countRes.ok) { const data = await countRes.json() setTotalCount(data.total || 0) } } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { setLoading(false) } }, [buildParams, sortBy, currentPage]) // Load review count const loadReviewCount = useCallback(async () => { try { const res = await fetch(`${BACKEND_URL}?endpoint=controls-count&release_state=needs_review`) if (res.ok) { const data = await res.json() setReviewCount(data.total || 0) } } catch { /* ignore */ } }, []) // Initial load useEffect(() => { loadMeta(); loadReviewCount() }, [loadMeta, loadReviewCount]) // Load controls when filters/page/sort change useEffect(() => { loadControls() }, [loadControls]) // Reset page when filters change useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) // Pagination const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) // Full reload (after CRUD) const fullReload = useCallback(async () => { await Promise.all([loadControls(), loadMeta(), loadReviewCount()]) }, [loadControls, loadMeta, loadReviewCount]) // CRUD handlers const handleCreate = async (data: typeof EMPTY_CONTROL) => { setSaving(true) try { const res = await fetch(`${BACKEND_URL}?endpoint=create-control`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) { const err = await res.json() alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`) return } await fullReload() setMode('list') } catch { alert('Netzwerkfehler') } finally { setSaving(false) } } const handleUpdate = async (data: typeof EMPTY_CONTROL) => { if (!selectedControl) return setSaving(true) try { const res = await fetch(`${BACKEND_URL}?endpoint=update-control&id=${selectedControl.control_id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(data), }) if (!res.ok) { const err = await res.json() alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`) return } await fullReload() setSelectedControl(null) setMode('list') } catch { alert('Netzwerkfehler') } finally { setSaving(false) } } const handleDelete = async (controlId: string) => { if (!confirm(`Control ${controlId} wirklich loeschen?`)) return try { const res = await fetch(`${BACKEND_URL}?id=${controlId}`, { method: 'DELETE' }) if (!res.ok && res.status !== 204) { alert('Fehler beim Loeschen') return } await fullReload() setSelectedControl(null) setMode('list') } catch { alert('Netzwerkfehler') } } const handleReview = async (controlId: string, action: string) => { try { const res = await fetch(`${BACKEND_URL}?endpoint=review&id=${controlId}`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ action }), }) if (res.ok) { await fullReload() if (reviewMode) { const remaining = reviewItems.filter(c => c.control_id !== controlId) setReviewItems(remaining) if (remaining.length > 0) { const nextIdx = Math.min(reviewIndex, remaining.length - 1) setReviewIndex(nextIdx) setSelectedControl(remaining[nextIdx]) } else { setReviewMode(false) setSelectedControl(null) setMode('list') } } else { setSelectedControl(null) setMode('list') } } } catch { /* ignore */ } } const [bulkProcessing, setBulkProcessing] = useState(false) const handleBulkReject = async (sourceState: string) => { const count = stateFilter === sourceState ? totalCount : reviewCount if (!confirm(`Alle ${count} Controls mit Status "${sourceState}" auf "deprecated" setzen? Diese Aktion kann nicht rueckgaengig gemacht werden.`)) return setBulkProcessing(true) try { const res = await fetch(`${BACKEND_URL}?endpoint=bulk-review`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ release_state: sourceState, action: 'reject' }), }) if (res.ok) { const data = await res.json() alert(`${data.affected_count} Controls auf "deprecated" gesetzt.`) await fullReload() } else { const err = await res.json() alert(`Fehler: ${err.error || err.details || 'Unbekannt'}`) } } catch { alert('Netzwerkfehler') } finally { setBulkProcessing(false) } } const loadProcessedStats = async () => { try { const res = await fetch(`${BACKEND_URL}?endpoint=processed-stats`) if (res.ok) { const data = await res.json() setProcessedStats(data.stats || []) } } catch { /* ignore */ } } const enterReviewMode = async () => { // Load review items from backend try { const res = await fetch(`${BACKEND_URL}?endpoint=controls&release_state=needs_review&limit=1000`) if (res.ok) { const items: CanonicalControl[] = await res.json() if (items.length > 0) { // Split into duplicate suspects vs rule 3 without anchor const dupes = items.filter(c => c.generation_metadata?.similar_controls && Array.isArray(c.generation_metadata.similar_controls) && (c.generation_metadata.similar_controls as unknown[]).length > 0 ) const rule3 = items.filter(c => !c.generation_metadata?.similar_controls || !Array.isArray(c.generation_metadata.similar_controls) || (c.generation_metadata.similar_controls as unknown[]).length === 0 ) setReviewDuplicates(dupes) setReviewRule3(rule3) // Start with duplicates tab if any, otherwise rule3 const startTab = dupes.length > 0 ? 'duplicates' : 'rule3' const startItems = startTab === 'duplicates' ? dupes : rule3 setReviewTab(startTab) setReviewItems(startItems) setReviewMode(true) setReviewIndex(0) setSelectedControl(startItems[0]) setMode('detail') } } } catch { /* ignore */ } } const switchReviewTab = (tab: 'duplicates' | 'rule3') => { const items = tab === 'duplicates' ? reviewDuplicates : reviewRule3 setReviewTab(tab) setReviewItems(items) setReviewIndex(0) if (items.length > 0) { setSelectedControl(items[0]) } } // Loading if (loading && controls.length === 0) { return (
) } if (error) { return (

{error}

) } // CREATE/EDIT MODE if (mode === 'create') { return setMode('list')} saving={saving} /> } if (mode === 'edit' && selectedControl) { return ( 0 ? selectedControl.open_anchors : [{ framework: '', ref: '', url: '' }], requirements: selectedControl.requirements.length > 0 ? selectedControl.requirements : [''], test_procedure: selectedControl.test_procedure.length > 0 ? selectedControl.test_procedure : [''], evidence: selectedControl.evidence.length > 0 ? selectedControl.evidence : [{ type: '', description: '' }], }} onSave={handleUpdate} onCancel={() => { setMode('detail') }} saving={saving} /> ) } // V1 COMPARE MODE if (compareMode && compareV1Control) { return ( { setCompareMode(false) }} onNavigateToControl={async (controlId: string) => { try { const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`) if (res.ok) { setCompareMode(false) setSelectedControl(await res.json()) setMode('detail') } } catch { /* ignore */ } }} /> ) } // DETAIL MODE if (mode === 'detail' && selectedControl) { const isDuplicateReview = reviewMode && reviewTab === 'duplicates' // Review tab bar (shown above the detail/compare view in review mode) const reviewTabBar = reviewMode ? (
) : null if (isDuplicateReview) { return (
{reviewTabBar}
{ setMode('list'); setSelectedControl(null); setReviewMode(false) }} onReview={handleReview} onEdit={() => setMode('edit')} reviewIndex={reviewIndex} reviewTotal={reviewItems.length} onReviewPrev={() => { const idx = Math.max(0, reviewIndex - 1) setReviewIndex(idx) setSelectedControl(reviewItems[idx]) }} onReviewNext={() => { const idx = Math.min(reviewItems.length - 1, reviewIndex + 1) setReviewIndex(idx) setSelectedControl(reviewItems[idx]) }} />
) } return (
{reviewTabBar}
{ setMode('list'); setSelectedControl(null); setReviewMode(false) }} onEdit={() => setMode('edit')} onDelete={handleDelete} onReview={handleReview} onRefresh={fullReload} onCompare={(ctrl, matches) => { setCompareV1Control(ctrl) setCompareMatches(matches) setCompareMode(true) }} onNavigateToControl={async (controlId: string) => { try { const res = await fetch(`${BACKEND_URL}?endpoint=control&id=${controlId}`) if (res.ok) { const data = await res.json() setSelectedControl(data) setMode('detail') } } catch { /* ignore */ } }} reviewMode={reviewMode} reviewIndex={reviewIndex} reviewTotal={reviewItems.length} onReviewPrev={() => { const idx = Math.max(0, reviewIndex - 1) setReviewIndex(idx) setSelectedControl(reviewItems[idx]) }} onReviewNext={() => { const idx = Math.min(reviewItems.length - 1, reviewIndex + 1) setReviewIndex(idx) setSelectedControl(reviewItems[idx]) }} />
) } // ========================================================================= // LIST VIEW // ========================================================================= return (
{/* Header */}

Canonical Control Library

{meta?.total ?? totalCount} Security Controls

{reviewCount > 0 && ( <> )}
{/* Frameworks */} {frameworks.length > 0 && (
{frameworks[0]?.name} v{frameworks[0]?.version} {frameworks[0]?.description}
)} {/* Filters */}
setSearchQuery(e.target.value)} className="w-full pl-9 pr-4 py-2 text-sm border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-purple-500" />
|
{/* Processing Stats */} {showStats && processedStats.length > 0 && (

Verarbeitungsfortschritt

{processedStats.map((s, i) => (
{String(s.collection)}
{String(s.processed_chunks)} verarbeitet {String(s.direct_adopted)} direkt {String(s.llm_reformed)} reformuliert
))}
)}
{/* Generator Modal */} {showGenerator && ( setShowGenerator(false)} onComplete={() => fullReload()} /> )} {/* Pagination Header */}
{totalCount} Controls gefunden {totalCount !== (meta?.total ?? totalCount) && ` (von ${meta?.total} gesamt)`} {loading && Lade...} {stateFilter && ['needs_review', 'too_close', 'duplicate'].includes(stateFilter) && totalCount > 0 && ( )}
Seite {currentPage} von {totalPages}
{/* Control List */}
{controls.map((ctrl, idx) => { // Show source group header when sorting by source const prevSource = idx > 0 ? (controls[idx - 1].source_citation?.source || 'Ohne Quelle') : null const curSource = ctrl.source_citation?.source || 'Ohne Quelle' const showSourceHeader = sortBy === 'source' && curSource !== prevSource return (
{showSourceHeader && (
{curSource}
)}
) })} {controls.length === 0 && !loading && (
{totalCount === 0 && !debouncedSearch && !severityFilter && !domainFilter ? 'Noch keine Controls vorhanden. Klicke auf "Neues Control" um zu starten.' : 'Keine Controls gefunden.'}
)}
{/* Pagination Controls */} {totalPages > 1 && (
{/* Page numbers */} {Array.from({ length: totalPages }, (_, i) => i + 1) .filter(p => p === 1 || p === totalPages || Math.abs(p - currentPage) <= 2) .reduce<(number | 'dots')[]>((acc, p, i, arr) => { if (i > 0 && p - (arr[i - 1] as number) > 1) acc.push('dots') acc.push(p) return acc }, []) .map((p, i) => p === 'dots' ? ( ... ) : ( ) ) }
)}
) }