From ac42a0aaa01b542fd6966ed0a07ea296caddad9b Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Mar 2026 17:35:52 +0100 Subject: [PATCH] =?UTF-8?q?fix:=20Faceted=20Counts=20=E2=80=94=20NULL-Wert?= =?UTF-8?q?e=20einbeziehen=20+=20AbortController=20fuer=20Race=20Condition?= =?UTF-8?q?s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../app/sdk/control-library/page.tsx | 42 ++++++--- .../api/canonical_control_routes.py | 87 ++++++++++++------- 2 files changed, 87 insertions(+), 42 deletions(-) diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 0e7bb77..d658547 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -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(null) + const controlsAbortRef = useRef(null) + // Debounce search const searchTimer = useRef | 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 (ctrlRes.ok) setControls(await ctrlRes.json()) - if (countRes.ok) { - const data = await countRes.json() - setTotalCount(data.total || 0) + 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]) => ( ))} + {meta?.verification_method_counts?.['__none__'] ? : null}