'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { CanonicalControl, Framework, BACKEND_URL } from './helpers' import { ControlsMeta, ControlFormData } from './types' const PAGE_SIZE = 50 export function useControlLibrary() { 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 }>>([]) const [bulkProcessing, setBulkProcessing] = useState(false) // Abort controllers const metaAbortRef = useRef(null) const controlsAbortRef = useRef(null) // 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]) 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]) const loadFrameworks = useCallback(async () => { try { const res = await fetch(`${BACKEND_URL}?endpoint=frameworks`) if (res.ok) setFrameworks(await res.json()) } catch { /* ignore */ } }, []) const loadMeta = useCallback(async () => { if (metaAbortRef.current) metaAbortRef.current.abort() const controller = new AbortController() metaAbortRef.current = controller try { const qs = buildParams() const res = await fetch(`${BACKEND_URL}?endpoint=controls-meta${qs ? `&${qs}` : ''}`, { signal: controller.signal }) if (res.ok && !controller.signal.aborted) setMeta(await res.json()) } catch (e) { if (e instanceof DOMException && e.name === 'AbortError') return } }, [buildParams]) const loadControls = useCallback(async () => { if (controlsAbortRef.current) controlsAbortRef.current.abort() const controller = new AbortController() controlsAbortRef.current = controller try { setLoading(true) 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}`, { signal: controller.signal }), fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`, { signal: controller.signal }), ]) if (!controller.signal.aborted) { if (ctrlRes.ok) setControls(await ctrlRes.json()) if (countRes.ok) { const data = await countRes.json() setTotalCount(data.total || 0) } } } catch (err) { if (err instanceof DOMException && err.name === 'AbortError') return setError(err instanceof Error ? err.message : 'Fehler beim Laden') } finally { if (!controller.signal.aborted) setLoading(false) } }, [buildParams, sortBy, currentPage]) 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 */ } }, []) useEffect(() => { loadFrameworks(); loadReviewCount() }, [loadFrameworks, loadReviewCount]) useEffect(() => { loadMeta() }, [loadMeta]) useEffect(() => { loadControls() }, [loadControls]) useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) const fullReload = useCallback(async () => { await Promise.all([loadControls(), loadMeta(), loadFrameworks(), loadReviewCount()]) }, [loadControls, loadMeta, loadFrameworks, loadReviewCount]) const handleCreate = async (data: ControlFormData) => { 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: ControlFormData) => { 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 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 () => { 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) { 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) 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]) } return { // State frameworks, controls, totalCount, meta, selectedControl, setSelectedControl, loading, error, searchQuery, setSearchQuery, debouncedSearch, severityFilter, setSeverityFilter, domainFilter, setDomainFilter, stateFilter, setStateFilter, verificationFilter, setVerificationFilter, categoryFilter, setCategoryFilter, evidenceTypeFilter, setEvidenceTypeFilter, audienceFilter, setAudienceFilter, sourceFilter, setSourceFilter, typeFilter, setTypeFilter, hideDuplicates, setHideDuplicates, sortBy, setSortBy, mode, setMode, saving, showGenerator, setShowGenerator, processedStats, showStats, setShowStats, currentPage, setCurrentPage, totalPages, reviewMode, setReviewMode, reviewIndex, setReviewIndex, reviewItems, setReviewItems, reviewCount, reviewTab, setReviewTab, reviewDuplicates, reviewRule3, bulkProcessing, compareMode, setCompareMode, compareV1Control, setCompareV1Control, compareMatches, setCompareMatches, // Actions fullReload, loadControls, loadMeta, loadFrameworks, loadReviewCount, loadProcessedStats, handleCreate, handleUpdate, handleDelete, handleReview, handleBulkReject, enterReviewMode, switchReviewTab, } }