'use client' import { useState, useEffect, useCallback, useRef } from 'react' import { CanonicalControl, Framework, BACKEND_URL } from './helpers' export interface ControlsMeta { total: number domains: Array<{ domain: string; count: number }> sources: Array<{ source: string; count: number }> no_source_count: number type_counts?: { rich: number; atomic: number; eigenentwicklung: number } severity_counts?: Record verification_method_counts?: Record category_counts?: Record evidence_type_counts?: Record release_state_counts?: Record // Master-control mapping dimensions (only returned by the MC endpoint) use_case_counts?: Record regulations?: Array<{ source_regulation: string; count: number }> mapped_total?: number unmapped_count?: number } const PAGE_SIZE = 50 export function useControlLibraryState(backendUrlOverride?: string) { const backendUrl = backendUrlOverride || BACKEND_URL 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 [useCaseFilter, setUseCaseFilter] = useState('') const [primaryOnly, setPrimaryOnly] = useState(false) const [regulationFilter, setRegulationFilter] = useState('') const [mappedFilter, setMappedFilter] = 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 / UI state const [mode, setMode] = useState<'list' | 'detail' | 'create' | 'edit'>('list') const [saving, setSaving] = useState(false) const [showGenerator, setShowGenerator] = useState(false) const [processedStats, setProcessedStats] = useState>>([]) const [showStats, setShowStats] = useState(false) const [currentPage, setCurrentPage] = useState(1) const [bulkProcessing, setBulkProcessing] = useState(false) // Review state 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 state 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 metaAbortRef = useRef(null) const controlsAbortRef = useRef(null) 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 (useCaseFilter) p.set('use_case', useCaseFilter) if (primaryOnly) p.set('primary', '1') if (regulationFilter) p.set('source_regulation', regulationFilter) if (mappedFilter) p.set('mapped', mappedFilter) 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, useCaseFilter, primaryOnly, regulationFilter, mappedFilter, categoryFilter, evidenceTypeFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) const loadFrameworks = useCallback(async () => { try { const res = await fetch(`${backendUrl}?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(`${backendUrl}?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(`${backendUrl}?endpoint=controls&${qs}`, { signal: controller.signal }), fetch(`${backendUrl}?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(`${backendUrl}?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, useCaseFilter, primaryOnly, regulationFilter, mappedFilter, 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 loadProcessedStats = async () => { try { const res = await fetch(`${backendUrl}?endpoint=processed-stats`) if (res.ok) { const data = await res.json(); setProcessedStats(data.stats || []) } } catch { /* ignore */ } } const enterReviewMode = async () => { try { const res = await fetch(`${backendUrl}?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 { // Data frameworks, controls, totalCount, meta, selectedControl, setSelectedControl, loading, error, // Filters searchQuery, setSearchQuery, debouncedSearch, severityFilter, setSeverityFilter, domainFilter, setDomainFilter, stateFilter, setStateFilter, verificationFilter, setVerificationFilter, useCaseFilter, setUseCaseFilter, primaryOnly, setPrimaryOnly, regulationFilter, setRegulationFilter, mappedFilter, setMappedFilter, categoryFilter, setCategoryFilter, evidenceTypeFilter, setEvidenceTypeFilter, audienceFilter, setAudienceFilter, sourceFilter, setSourceFilter, typeFilter, setTypeFilter, hideDuplicates, setHideDuplicates, sortBy, setSortBy, // CRUD/UI mode, setMode, saving, setSaving, showGenerator, setShowGenerator, processedStats, showStats, setShowStats, currentPage, setCurrentPage, totalPages, bulkProcessing, setBulkProcessing, // Review reviewMode, setReviewMode, reviewIndex, setReviewIndex, reviewItems, setReviewItems, reviewCount, reviewTab, reviewDuplicates, reviewRule3, // V1 Compare compareMode, setCompareMode, compareV1Control, setCompareV1Control, compareMatches, setCompareMatches, // Actions fullReload, loadControls, loadMeta, loadFrameworks, loadReviewCount, loadProcessedStats, enterReviewMode, switchReviewTab, } }