From 2dee62fa6fe6be6f3ce5e1812b0925f7714937b3 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Thu, 26 Mar 2026 14:33:00 +0100 Subject: [PATCH] feat: Eigenentwicklung-Filter im Typ-Dropdown mit Counts Backend: control_type=eigenentwicklung in list_controls + count_controls, type_counts (rich/atomic/eigenentwicklung) in controls-meta Endpoint. Frontend: Typ-Dropdown zeigt Eigenentwicklung mit Anzahl. Co-Authored-By: Claude Opus 4.6 --- .../app/sdk/control-library/page.tsx | 10 ++++-- .../api/canonical_control_routes.py | 33 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index e473b29..3655f77 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -27,6 +27,11 @@ interface ControlsMeta { domains: Array<{ domain: string; count: number }> sources: Array<{ source: string; count: number }> no_source_count: number + type_counts?: { + rich: number + atomic: number + eigenentwicklung: number + } } // ============================================================================= @@ -743,8 +748,9 @@ export default function ControlLibraryPage() { className="text-sm border border-gray-300 rounded-lg px-2 py-1.5 focus:outline-none focus:ring-2 focus:ring-purple-500" > - - + + + | diff --git a/backend-compliance/compliance/api/canonical_control_routes.py b/backend-compliance/compliance/api/canonical_control_routes.py index de13988..07abe38 100644 --- a/backend-compliance/compliance/api/canonical_control_routes.py +++ b/backend-compliance/compliance/api/canonical_control_routes.py @@ -368,6 +368,11 @@ async def list_controls( query += " AND decomposition_method = 'pass0b'" elif control_type == "rich": query += " AND (decomposition_method IS NULL OR decomposition_method != 'pass0b')" + elif control_type == "eigenentwicklung": + query += """ AND generation_strategy = 'ungrouped' + AND (pipeline_version = '1' OR pipeline_version IS NULL) + AND source_citation IS NULL + AND parent_control_uuid IS NULL""" if search: query += " AND (control_id ILIKE :q OR title ILIKE :q OR objective ILIKE :q)" params["q"] = f"%{search}%" @@ -450,6 +455,11 @@ async def count_controls( query += " AND decomposition_method = 'pass0b'" elif control_type == "rich": query += " AND (decomposition_method IS NULL OR decomposition_method != 'pass0b')" + elif control_type == "eigenentwicklung": + query += """ AND generation_strategy = 'ungrouped' + AND (pipeline_version = '1' OR pipeline_version IS NULL) + AND source_citation IS NULL + AND parent_control_uuid IS NULL""" if search: query += " AND (control_id ILIKE :q OR title ILIKE :q OR objective ILIKE :q)" params["q"] = f"%{search}%" @@ -484,11 +494,34 @@ async def controls_meta(): WHERE source_citation IS NULL OR source_citation->>'source' IS NULL OR source_citation->>'source' = '' """)).scalar() + # Type counts for filter dropdown + atomic_count = db.execute(text(""" + SELECT count(*) FROM canonical_controls WHERE decomposition_method = 'pass0b' + """)).scalar() or 0 + + eigenentwicklung_count = db.execute(text(""" + SELECT count(*) FROM canonical_controls + WHERE generation_strategy = 'ungrouped' + AND (pipeline_version = '1' OR pipeline_version IS NULL) + AND source_citation IS NULL + AND parent_control_uuid IS NULL + """)).scalar() or 0 + + rich_count = db.execute(text(""" + SELECT count(*) FROM canonical_controls + WHERE (decomposition_method IS NULL OR decomposition_method != 'pass0b') + """)).scalar() or 0 + return { "total": total, "domains": [{"domain": r[0], "count": r[1]} for r in domains], "sources": [{"source": r[0], "count": r[1]} for r in sources], "no_source_count": no_source, + "type_counts": { + "rich": rich_count, + "atomic": atomic_count, + "eigenentwicklung": eigenentwicklung_count, + }, }