From 230fbeb4907c402653be03faaf252176bfcffa35 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Wed, 25 Mar 2026 08:18:00 +0100 Subject: [PATCH] feat: Dreistufenmodell normative Verbindlichkeit + Duplikat-Filter + Auto-Deploy - Source-Type-Klassifikation (58 Regulierungen: law/guideline/framework) - Backfill-Endpoint POST /controls/backfill-normative-strength - exclude_duplicates Filter fuer Control-Library (Backend + Proxy + UI-Toggle) - MkDocs-Kapitel: Normative Verbindlichkeit mit Mermaid-Diagrammen - scripts/deploy.sh: Auto-Push + Mac Mini rebuild + Coolify health monitoring - 26 Unit Tests fuer Klassifikations-Logik Co-Authored-By: Claude Opus 4.6 --- .../app/api/sdk/v1/canonical/route.ts | 4 +- .../app/sdk/control-library/page.tsx | 15 +- .../api/canonical_control_routes.py | 109 ++++++++++ .../data/source_type_classification.py | 204 ++++++++++++++++++ .../tests/test_source_type_classification.py | 102 +++++++++ .../sdk-modules/normative-verbindlichkeit.md | 201 +++++++++++++++++ mkdocs.yml | 1 + scripts/deploy.sh | 164 ++++++++++++++ 8 files changed, 796 insertions(+), 4 deletions(-) create mode 100644 backend-compliance/compliance/data/source_type_classification.py create mode 100644 backend-compliance/tests/test_source_type_classification.py create mode 100644 docs-src/services/sdk-modules/normative-verbindlichkeit.md create mode 100755 scripts/deploy.sh diff --git a/admin-compliance/app/api/sdk/v1/canonical/route.ts b/admin-compliance/app/api/sdk/v1/canonical/route.ts index 3076e13..5b1b608 100644 --- a/admin-compliance/app/api/sdk/v1/canonical/route.ts +++ b/admin-compliance/app/api/sdk/v1/canonical/route.ts @@ -27,7 +27,7 @@ export async function GET(request: NextRequest) { case 'controls': { const controlParams = new URLSearchParams() const passthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', - 'target_audience', 'source', 'search', 'control_type', 'sort', 'order', 'limit', 'offset'] + 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates', 'sort', 'order', 'limit', 'offset'] for (const key of passthrough) { const val = searchParams.get(key) if (val) controlParams.set(key, val) @@ -40,7 +40,7 @@ export async function GET(request: NextRequest) { case 'controls-count': { const countParams = new URLSearchParams() const countPassthrough = ['severity', 'domain', 'release_state', 'verification_method', 'category', - 'target_audience', 'source', 'search', 'control_type'] + 'target_audience', 'source', 'search', 'control_type', 'exclude_duplicates'] for (const key of countPassthrough) { const val = searchParams.get(key) if (val) countParams.set(key, val) diff --git a/admin-compliance/app/sdk/control-library/page.tsx b/admin-compliance/app/sdk/control-library/page.tsx index 0ea5bb4..07549c9 100644 --- a/admin-compliance/app/sdk/control-library/page.tsx +++ b/admin-compliance/app/sdk/control-library/page.tsx @@ -54,6 +54,7 @@ export default function ControlLibraryPage() { 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 state @@ -96,10 +97,11 @@ export default function ControlLibraryPage() { 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, audienceFilter, sourceFilter, typeFilter, debouncedSearch]) + }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch]) // Load metadata (domains, sources — once + on refresh) const loadMeta = useCallback(async () => { @@ -167,7 +169,7 @@ export default function ControlLibraryPage() { useEffect(() => { loadControls() }, [loadControls]) // Reset page when filters change - useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, typeFilter, debouncedSearch, sortBy]) + useEffect(() => { setCurrentPage(1) }, [severityFilter, domainFilter, stateFilter, verificationFilter, categoryFilter, audienceFilter, sourceFilter, typeFilter, hideDuplicates, debouncedSearch, sortBy]) // Pagination const totalPages = Math.max(1, Math.ceil(totalCount / PAGE_SIZE)) @@ -623,6 +625,15 @@ export default function ControlLibraryPage() { +