From 02879a2c3abac6cd53f446ca54792fedd51efd05 Mon Sep 17 00:00:00 2001 From: Benjamin Admin Date: Sat, 6 Jun 2026 23:35:33 +0200 Subject: [PATCH] =?UTF-8?q?refactor:=20split=20cookie=5Fscreenshot=5Focr.p?= =?UTF-8?q?y=20(642=20=E2=86=92=20290=20+=20353=20LOC)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit CI hard-cap 500 LOC. cookie_screenshot_ocr.py war auf 642 gewachsen, also gesplittet: - cookie_screenshot_ocr_engines.py (353 LOC, NEU) OCR-Engine-Funktionen: _slice_screenshot, Vision-LLM (qwen2.5vl), PaddleOCR, Tesseract, parse_ocr_cookie_table, parse_vision_response, Konstanten VISION_MODEL/OLLAMA_URL/VISION_PROMPT. - cookie_screenshot_ocr.py (290 LOC, REWRITE) Orchestration: capture_cookie_evidence_slices, _ocr_one_slice, ocr_slices_extract_cookies, capture_cookie_screenshot, extract_cookies_via_vision, cookies_to_vendor_records. Re-Exports der Engine-Funktionen für Backward-Kompat. Einziger externer Importer (_phase_d1_vendors_raw.py) braucht keinen Code-Change — Public-API stabil. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../_components/ConditionBuilder.tsx | 232 ++++++++++ .../_components/RuleEditor.tsx | 414 +++++++++++++++++ .../_components/RuleList.tsx | 111 +++++ .../_hooks/useRuleEditorActions.ts | 183 ++++++++ .../app/sdk/template-rule-editor/_types.ts | 246 +++++++++++ .../app/sdk/template-rule-editor/page.tsx | 205 +++++++++ admin-compliance/lib/sdk/types/sdk-steps.ts | 14 + .../services/cookie_screenshot_ocr.py | 416 ++---------------- .../services/cookie_screenshot_ocr_engines.py | 353 +++++++++++++++ 9 files changed, 1790 insertions(+), 384 deletions(-) create mode 100644 admin-compliance/app/sdk/template-rule-editor/_components/ConditionBuilder.tsx create mode 100644 admin-compliance/app/sdk/template-rule-editor/_components/RuleEditor.tsx create mode 100644 admin-compliance/app/sdk/template-rule-editor/_components/RuleList.tsx create mode 100644 admin-compliance/app/sdk/template-rule-editor/_hooks/useRuleEditorActions.ts create mode 100644 admin-compliance/app/sdk/template-rule-editor/_types.ts create mode 100644 admin-compliance/app/sdk/template-rule-editor/page.tsx create mode 100644 backend-compliance/compliance/services/cookie_screenshot_ocr_engines.py diff --git a/admin-compliance/app/sdk/template-rule-editor/_components/ConditionBuilder.tsx b/admin-compliance/app/sdk/template-rule-editor/_components/ConditionBuilder.tsx new file mode 100644 index 00000000..42195fb5 --- /dev/null +++ b/admin-compliance/app/sdk/template-rule-editor/_components/ConditionBuilder.tsx @@ -0,0 +1,232 @@ +'use client' + +/** + * Strukturierter Editor fuer JSONB-Conditions: + * { kind: 'all'|'any', clauses: [{field, op, value}] } + * + * Wird im RuleEditor verwendet. Reine Praesentations-Komponente — Parent + * verwaltet State. + */ + +import type { + ClauseOperator, RuleClause, RuleCondition, +} from '../_types' +import { OPERATOR_LABELS, PROFILE_FIELDS } from '../_types' + +interface Props { + value: RuleCondition + onChange: (next: RuleCondition) => void + readOnly?: boolean +} + +export default function ConditionBuilder({ value, onChange, readOnly }: Props) { + const setKind = (kind: 'all' | 'any') => onChange({ ...value, kind }) + const setClause = (idx: number, clause: RuleClause) => { + const next = [...value.clauses] + next[idx] = clause + onChange({ ...value, clauses: next }) + } + const addClause = () => + onChange({ + ...value, + clauses: [ + ...value.clauses, + { field: PROFILE_FIELDS[0].key, op: 'eq', value: '' }, + ], + }) + const removeClause = (idx: number) => + onChange({ ...value, clauses: value.clauses.filter((_, i) => i !== idx) }) + + return ( +
+
+ Bedingung: + +
+ + {value.clauses.length === 0 && ( +
+ Keine Klauseln — Regel gilt für jedes Profil. +
+ )} + +
    + {value.clauses.map((clause, idx) => ( +
  • + setClause(idx, c)} + readOnly={!!readOnly} + /> + {!readOnly && ( + + )} +
  • + ))} +
+ + {!readOnly && ( + + )} +
+ ) +} + +function ClauseRow({ + clause, onChange, readOnly, +}: { + clause: RuleClause + onChange: (c: RuleClause) => void + readOnly: boolean +}) { + const field = PROFILE_FIELDS.find((f) => f.key === clause.field) || PROFILE_FIELDS[0] + const operators: ClauseOperator[] = + field.type === 'enum' + ? ['eq', 'neq', 'in', 'not_in', 'exists', 'truthy', 'falsy'] + : field.type === 'boolean' + ? ['truthy', 'falsy', 'eq', 'neq'] + : field.type === 'number' + ? ['eq', 'neq', 'gt', 'gte', 'lt', 'lte'] + : ['eq', 'neq', 'in', 'not_in', 'exists'] + + const requiresValue = !['exists', 'truthy', 'falsy'].includes(clause.op) + const multiValue = clause.op === 'in' || clause.op === 'not_in' + + return ( +
+ + + + +
+ {requiresValue && ( + onChange({ ...clause, value: v })} + readOnly={readOnly} + /> + )} +
+
+ ) +} + +function ValueInput({ + field, multi, value, onChange, readOnly, +}: { + field: typeof PROFILE_FIELDS[number] + multi: boolean + value: unknown + onChange: (v: unknown) => void + readOnly: boolean +}) { + if (field.type === 'enum' && field.options) { + if (multi) { + const selected = Array.isArray(value) ? (value as string[]) : [] + return ( + + ) + } + return ( + + ) + } + + if (field.type === 'number') { + return ( + onChange(Number(e.target.value))} + /> + ) + } + + if (field.type === 'boolean') { + return ( + + ) + } + + return ( + onChange(e.target.value)} + /> + ) +} diff --git a/admin-compliance/app/sdk/template-rule-editor/_components/RuleEditor.tsx b/admin-compliance/app/sdk/template-rule-editor/_components/RuleEditor.tsx new file mode 100644 index 00000000..9cd23796 --- /dev/null +++ b/admin-compliance/app/sdk/template-rule-editor/_components/RuleEditor.tsx @@ -0,0 +1,414 @@ +'use client' + +/** + * Rechte Spalte: Detail-Editor fuer die ausgewaehlte Regel. + * + * - Zeigt Live-Version + offenen Draft (falls vorhanden) + * - Erlaubt Draft-Edit (classification, conditions, source_citation, rationale) + * - Buttons: "Neuen Draft starten" (kopiert von Live), "Einreichen" (mit Pflicht + * change_summary-Modal), "Intern freigeben" (DSB), "Publish" (= Mandanten-Freigabe) + * - Versionshistorie + Approval-Trail unten als Akkordeon + */ + +import { useEffect, useMemo, useState } from 'react' +import type { + ApprovalHistoryEntry, Classification, Rule, RuleCondition, RuleVersion, +} from '../_types' +import { CLASSIFICATION_LABELS, STATUS_LABELS } from '../_types' +import ConditionBuilder from './ConditionBuilder' + +interface Props { + rule: Rule + versions: RuleVersion[] + history: ApprovalHistoryEntry[] + onCreateDraft: (payload: { + classification: Classification + conditions: RuleCondition + source_citation: string + rationale?: string | null + }) => Promise + onUpdateDraft: (versionId: string, patch: { + classification?: Classification + conditions?: RuleCondition + source_citation?: string + rationale?: string | null + }) => Promise + onSubmitForReview: (versionId: string, changeSummary: string) => Promise + onApprove: (versionId: string) => Promise + onPublish: (versionId: string) => Promise + onReject: (versionId: string, reason: string) => Promise +} + +export default function RuleEditor({ + rule, versions, history, + onCreateDraft, onUpdateDraft, + onSubmitForReview, onApprove, onPublish, onReject, +}: Props) { + const liveVersion = useMemo( + () => versions.find((v) => v.is_live) || null, + [versions], + ) + const draftVersion = useMemo( + () => versions.find((v) => ['draft', 'review'].includes(v.status)) || null, + [versions], + ) + + // Edit-State + const [classification, setClassification] = useState('required') + const [conditions, setConditions] = useState({ kind: 'all', clauses: [] }) + const [sourceCitation, setSourceCitation] = useState('') + const [rationale, setRationale] = useState('') + + // Modal-State + const [showSubmit, setShowSubmit] = useState(false) + const [changeSummary, setChangeSummary] = useState('') + const [showHistory, setShowHistory] = useState(false) + const [rejectReason, setRejectReason] = useState('') + const [showReject, setShowReject] = useState(false) + + // Sync Edit-State mit ausgewaehltem Version (Draft hat Vorrang) + const sourceVersion = draftVersion || liveVersion + useEffect(() => { + if (sourceVersion) { + setClassification(sourceVersion.classification) + setConditions(sourceVersion.conditions) + setSourceCitation(sourceVersion.source_citation) + setRationale(sourceVersion.rationale || '') + } + }, [sourceVersion?.id]) + + const isDraftMode = !!draftVersion && draftVersion.status === 'draft' + const isReviewMode = !!draftVersion && draftVersion.status === 'review' + const readOnly = !isDraftMode + + const handleCreateDraft = () => { + onCreateDraft({ + classification: liveVersion?.classification || 'recommended', + conditions: liveVersion?.conditions || { kind: 'all', clauses: [] }, + source_citation: liveVersion?.source_citation || '', + rationale: liveVersion?.rationale, + }) + } + + const handleSaveDraft = () => { + if (!draftVersion) return + onUpdateDraft(draftVersion.id, { + classification, conditions, source_citation: sourceCitation, rationale, + }) + } + + const handleSubmit = () => { + if (!draftVersion || !changeSummary.trim()) return + onSubmitForReview(draftVersion.id, changeSummary.trim()) + setShowSubmit(false) + setChangeSummary('') + } + + return ( +
+
+
+
+

{rule.title}

+
+ {rule.document_type} · {rule.rule_key} +
+
+
+ {liveVersion && ( + + Live: v{liveVersion.version_number} ( + {liveVersion.classification}) + + )} + {draftVersion && ( + + Draft v{draftVersion.version_number} · {STATUS_LABELS[draftVersion.status]} + + )} +
+
+
+ +
+ {!draftVersion && ( +
+ + Kein offener Draft. Starte einen neuen Draft, um die Regel zu ändern. + + +
+ )} + + {/* Klassifikation */} +
+ + +
+ + {/* Bedingung */} +
+ + +
+ + {/* Source Citation (Pflicht) */} +
+ + setSourceCitation(e.target.value)} + /> +
+ + {/* Rationale */} +
+ +