'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 } const PAGE_SIZE = 50 export function useControlLibraryState() { 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 / 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 (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 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 { // Data frameworks, controls, totalCount, meta, selectedControl, setSelectedControl, loading, error, // Filters 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, // 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, } }