fix: Faceted Counts — NULL-Werte einbeziehen + AbortController fuer Race Conditions
All checks were successful
CI/CD / go-lint (push) Has been skipped
CI/CD / python-lint (push) Has been skipped
CI/CD / nodejs-lint (push) Has been skipped
CI/CD / test-go-ai-compliance (push) Successful in 34s
CI/CD / test-python-backend-compliance (push) Successful in 32s
CI/CD / test-python-document-crawler (push) Successful in 21s
CI/CD / test-python-dsms-gateway (push) Successful in 17s
CI/CD / validate-canonical-controls (push) Successful in 11s
CI/CD / Deploy (push) Successful in 2s

Backend: Facets zaehlen jetzt Controls OHNE Wert (z.B. "Ohne Nachweis")
als __none__. Filter unterstuetzen __none__ fuer verification_method,
category, evidence_type. Counts addieren sich immer zum Total.

Frontend: "Ohne X" Optionen in Dropdowns. AbortController verhindert
dass aeltere API-Antworten neuere ueberschreiben.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Benjamin Admin
2026-03-26 17:35:52 +01:00
parent 52e463a7c8
commit ac42a0aaa0
2 changed files with 87 additions and 42 deletions

View File

@@ -101,6 +101,10 @@ export default function ControlLibraryPage() {
similarity_score: number; match_rank: number; match_method: string
}>>([])
// Abort controllers for cancelling stale requests
const metaAbortRef = useRef<AbortController | null>(null)
const controlsAbortRef = useRef<AbortController | null>(null)
// Debounce search
const searchTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
useEffect(() => {
@@ -135,17 +139,25 @@ export default function ControlLibraryPage() {
} catch { /* ignore */ }
}, [])
// Load faceted metadata (reloads when filters change)
// Load faceted metadata (reloads when filters change, cancels stale requests)
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}` : ''}`)
if (res.ok) setMeta(await res.json())
} catch { /* ignore */ }
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])
// Load controls page
// Load controls page (cancels stale requests)
const loadControls = useCallback(async () => {
if (controlsAbortRef.current) controlsAbortRef.current.abort()
const controller = new AbortController()
controlsAbortRef.current = controller
try {
setLoading(true)
@@ -164,19 +176,22 @@ export default function ControlLibraryPage() {
const countQs = buildParams()
const [ctrlRes, countRes] = await Promise.all([
fetch(`${BACKEND_URL}?endpoint=controls&${qs}`),
fetch(`${BACKEND_URL}?endpoint=controls-count&${countQs}`),
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 {
setLoading(false)
if (!controller.signal.aborted) setLoading(false)
}
}, [buildParams, sortBy, currentPage])
@@ -701,6 +716,7 @@ export default function ControlLibraryPage() {
{Object.entries(VERIFICATION_METHODS).map(([k, v]) => (
<option key={k} value={k}>{v.label}{meta?.verification_method_counts?.[k] ? ` (${meta.verification_method_counts[k]})` : ''}</option>
))}
{meta?.verification_method_counts?.['__none__'] ? <option value="__none__">Ohne Nachweis ({meta.verification_method_counts['__none__']})</option> : null}
</select>
<select
value={categoryFilter}
@@ -711,6 +727,7 @@ export default function ControlLibraryPage() {
{CATEGORY_OPTIONS.map(c => (
<option key={c.value} value={c.value}>{c.label}{meta?.category_counts?.[c.value] ? ` (${meta.category_counts[c.value]})` : ''}</option>
))}
{meta?.category_counts?.['__none__'] ? <option value="__none__">Ohne Kategorie ({meta.category_counts['__none__']})</option> : null}
</select>
<select
value={evidenceTypeFilter}
@@ -721,6 +738,7 @@ export default function ControlLibraryPage() {
{EVIDENCE_TYPE_OPTIONS.map(c => (
<option key={c.value} value={c.value}>{c.label}{meta?.evidence_type_counts?.[c.value] ? ` (${meta.evidence_type_counts[c.value]})` : ''}</option>
))}
{meta?.evidence_type_counts?.['__none__'] ? <option value="__none__">Ohne Nachweisart ({meta.evidence_type_counts['__none__']})</option> : null}
</select>
<select
value={audienceFilter}

View File

@@ -347,12 +347,21 @@ async def list_controls(
query += " AND release_state = :rs"
params["rs"] = release_state
if verification_method:
if verification_method == "__none__":
query += " AND verification_method IS NULL"
else:
query += " AND verification_method = :vm"
params["vm"] = verification_method
if category:
if category == "__none__":
query += " AND category IS NULL"
else:
query += " AND category = :cat"
params["cat"] = category
if evidence_type:
if evidence_type == "__none__":
query += " AND evidence_type IS NULL"
else:
query += " AND evidence_type = :et"
params["et"] = evidence_type
if target_audience:
@@ -434,12 +443,21 @@ async def count_controls(
query += " AND release_state = :rs"
params["rs"] = release_state
if verification_method:
if verification_method == "__none__":
query += " AND verification_method IS NULL"
else:
query += " AND verification_method = :vm"
params["vm"] = verification_method
if category:
if category == "__none__":
query += " AND category IS NULL"
else:
query += " AND category = :cat"
params["cat"] = category
if evidence_type:
if evidence_type == "__none__":
query += " AND evidence_type IS NULL"
else:
query += " AND evidence_type = :et"
params["et"] = evidence_type
if target_audience:
@@ -506,12 +524,21 @@ async def controls_meta(
clauses.append("release_state = :rs")
p["rs"] = release_state
if verification_method and skip != "verification_method":
if verification_method == "__none__":
clauses.append("verification_method IS NULL")
else:
clauses.append("verification_method = :vm")
p["vm"] = verification_method
if category and skip != "category":
if category == "__none__":
clauses.append("category IS NULL")
else:
clauses.append("category = :cat")
p["cat"] = category
if evidence_type and skip != "evidence_type":
if evidence_type == "__none__":
clauses.append("evidence_type IS NULL")
else:
clauses.append("evidence_type = :et")
p["et"] = evidence_type
if target_audience and skip != "target_audience":
@@ -598,28 +625,28 @@ async def controls_meta(
GROUP BY severity ORDER BY severity
"""), p_sev).fetchall()
# Verification method facet
# Verification method facet (include NULLs as __none__)
w_vm, p_vm = _build_where(skip="verification_method")
vm_counts = db.execute(text(f"""
SELECT verification_method, count(*) as cnt
FROM canonical_controls WHERE {w_vm} AND verification_method IS NOT NULL
GROUP BY verification_method ORDER BY verification_method
SELECT COALESCE(verification_method, '__none__') as vm, count(*) as cnt
FROM canonical_controls WHERE {w_vm}
GROUP BY vm ORDER BY vm
"""), p_vm).fetchall()
# Category facet
# Category facet (include NULLs as __none__)
w_cat, p_cat = _build_where(skip="category")
cat_counts = db.execute(text(f"""
SELECT category, count(*) as cnt
FROM canonical_controls WHERE {w_cat} AND category IS NOT NULL
GROUP BY category ORDER BY cnt DESC
SELECT COALESCE(category, '__none__') as cat, count(*) as cnt
FROM canonical_controls WHERE {w_cat}
GROUP BY cat ORDER BY cnt DESC
"""), p_cat).fetchall()
# Evidence type facet
# Evidence type facet (include NULLs as __none__)
w_et, p_et = _build_where(skip="evidence_type")
et_counts = db.execute(text(f"""
SELECT evidence_type, count(*) as cnt
FROM canonical_controls WHERE {w_et} AND evidence_type IS NOT NULL
GROUP BY evidence_type ORDER BY evidence_type
SELECT COALESCE(evidence_type, '__none__') as et, count(*) as cnt
FROM canonical_controls WHERE {w_et}
GROUP BY et ORDER BY et
"""), p_et).fetchall()
# Release state facet