'use client' import { useState, useEffect, useMemo, useCallback } from 'react' import { Shield, Search, ChevronRight, ChevronLeft, Filter, Lock, BookOpen, Plus, Zap, BarChart3, ListChecks, ChevronsLeft, ChevronsRight, } from 'lucide-react' import { CanonicalControl, Framework, BACKEND_URL, EMPTY_CONTROL, SeverityBadge, StateBadge, LicenseRuleBadge, VerificationMethodBadge, CategoryBadge, TargetAudienceBadge, getDomain, VERIFICATION_METHODS, CATEGORY_OPTIONS, TARGET_AUDIENCE_OPTIONS, } from './components/helpers' import { ControlForm } from './components/ControlForm' import { ControlDetail } from './components/ControlDetail' import { GeneratorModal } from './components/GeneratorModal' // ============================================================================= // CONTROL LIBRARY PAGE // ============================================================================= export default function ControlLibraryPage() { const [frameworks, setFrameworks] = useState([]) const [controls, setControls] = useState([]) const [selectedControl, setSelectedControl] = useState(null) const [loading, setLoading] = useState(true) const [error, setError] = useState(null) // Filters const [searchQuery, setSearchQuery] = useState('') const [severityFilter, setSeverityFilter] = useState('') const [domainFilter, setDomainFilter] = useState('') const [stateFilter, setStateFilter] = useState('') const [verificationFilter, setVerificationFilter] = useState('') const [categoryFilter, setCategoryFilter] = useState('') const [audienceFilter, setAudienceFilter] = useState('') // 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) const PAGE_SIZE = 50 // Review mode const [reviewMode, setReviewMode] = useState(false) const [reviewIndex, setReviewIndex] = useState(0) // Load data const loadData = useCallback(async () => { try { setLoading(true) const [fwRes, ctrlRes] = await Promise.all([ fetch(`${BACKEND_URL}?endpoint=frameworks`), fetch(`${BACKEND_URL}?endpoint=controls`), ]) if (fwRes.ok) setFrameworks(await fwRes.json()) if (ctrlRes.ok) setControls(await ctrlRes.json()) } catch (err) { setError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { setLoading(false) } }, []) useEffect(() => { loadData() }, [loadData]) // Derived: unique domains const domains = useMemo(() => { const set = new Set(controls.map(c => getDomain(c.control_id))) return Array.from(set).sort() }, [controls]) // Filtered controls const filteredControls = useMemo(() => { return controls.filter(c => { if (severityFilter && c.severity !== severityFilter) return false if (domainFilter && getDomain(c.control_id) !== domainFilter) return false if (stateFilter && c.release_state !== stateFilter) return false if (verificationFilter && c.verification_method !== verificationFilter) return false if (categoryFilter && c.category !== categoryFilter) return false if (audienceFilter && c.target_audience !== audienceFilter) return false if (searchQuery) { const q = searchQuery.toLowerCase() return ( c.control_id.toLowerCase().includes(q) || c.title.toLowerCase().includes(q) || c.objective.toLowerCase().includes(q) || c.tags.some(t => t.toLowerCase().includes(q)) ) } return true }) }, [controls, severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, searchQuery]) // Reset page when filters change useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, searchQuery]) // Pagination const totalPages = Math.max(1, Math.ceil(filteredControls.length / PAGE_SIZE)) const paginatedControls = useMemo(() => { const start = (currentPage - 1) * PAGE_SIZE return filteredControls.slice(start, start + PAGE_SIZE) }, [filteredControls, currentPage]) // Review queue items const reviewItems = useMemo(() => { return controls.filter(c => ['needs_review', 'too_close', 'duplicate'].includes(c.release_state)) }, [controls]) // 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 loadData() 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 loadData() 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 loadData() 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 loadData() if (reviewMode) { const remaining = controls.filter(c => ['needs_review', 'too_close', 'duplicate'].includes(c.release_state) && c.control_id !== controlId ) 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 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 = () => { if (reviewItems.length === 0) return setReviewMode(true) setReviewIndex(0) setSelectedControl(reviewItems[0]) setMode('detail') } // Loading if (loading) { 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} /> ) } // DETAIL MODE if (mode === 'detail' && selectedControl) { return ( { setMode('list'); setSelectedControl(null); setReviewMode(false) }} onEdit={() => setMode('edit')} onDelete={handleDelete} onReview={handleReview} onRefresh={loadData} 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

{controls.length} unabhaengig formulierte Security Controls —{' '} {controls.reduce((sum, c) => sum + c.open_anchors.length, 0)} Open-Source-Referenzen

{reviewItems.length > 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={() => loadData()} /> )} {/* Pagination Header */}
{filteredControls.length} Controls gefunden {filteredControls.length !== controls.length && ` (von ${controls.length} gesamt)`} Seite {currentPage} von {totalPages}
{/* Control List */}
{paginatedControls.map(ctrl => ( ))} {filteredControls.length === 0 && (
{controls.length === 0 ? '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' ? ( ... ) : ( ) ) }
)}
) }