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
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:
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user