02879a2c3a
CI / detect-changes (push) Successful in 7s
CI / branch-name (push) Has been skipped
CI / guardrail-integrity (push) Has been skipped
CI / secret-scan (push) Has been skipped
CI / dep-audit (push) Has been skipped
CI / sbom-scan (push) Has been skipped
CI / build-sha-integrity (push) Failing after 4s
CI / validate-canonical-controls (push) Successful in 11s
CI / loc-budget (push) Failing after 14s
CI / go-lint (push) Has been skipped
CI / python-lint (push) Has been skipped
CI / nodejs-lint (push) Has been skipped
CI / nodejs-build (push) Successful in 2m19s
CI / test-go (push) Has been skipped
CI / iace-gt-coverage (push) Has been skipped
CI / test-python-backend (push) Successful in 29s
CI / test-python-document-crawler (push) Has been skipped
CI / test-python-dsms-gateway (push) Has been skipped
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) <noreply@anthropic.com>
233 lines
6.7 KiB
TypeScript
233 lines
6.7 KiB
TypeScript
'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 (
|
||
<div className="space-y-2">
|
||
<div className="flex items-center gap-2">
|
||
<span className="text-xs text-gray-600">Bedingung:</span>
|
||
<select
|
||
className="text-xs px-2 py-1 border border-gray-300 rounded"
|
||
value={value.kind}
|
||
disabled={readOnly}
|
||
onChange={(e) => setKind(e.target.value as 'all' | 'any')}
|
||
>
|
||
<option value="all">ALLE Klauseln müssen zutreffen (AND)</option>
|
||
<option value="any">MIND. EINE Klausel trifft zu (OR)</option>
|
||
</select>
|
||
</div>
|
||
|
||
{value.clauses.length === 0 && (
|
||
<div className="text-xs text-gray-500 italic px-1">
|
||
Keine Klauseln — Regel gilt für jedes Profil.
|
||
</div>
|
||
)}
|
||
|
||
<ul className="space-y-1">
|
||
{value.clauses.map((clause, idx) => (
|
||
<li key={idx} className="flex items-start gap-1 p-1.5 bg-gray-50 rounded border border-gray-200">
|
||
<ClauseRow
|
||
clause={clause}
|
||
onChange={(c) => setClause(idx, c)}
|
||
readOnly={!!readOnly}
|
||
/>
|
||
{!readOnly && (
|
||
<button
|
||
className="text-xs px-1.5 py-0.5 text-rose-700 hover:bg-rose-50 rounded"
|
||
onClick={() => removeClause(idx)}
|
||
title="Klausel entfernen"
|
||
>
|
||
×
|
||
</button>
|
||
)}
|
||
</li>
|
||
))}
|
||
</ul>
|
||
|
||
{!readOnly && (
|
||
<button
|
||
className="text-xs px-2 py-1 border border-gray-300 rounded text-gray-700 hover:bg-gray-50"
|
||
onClick={addClause}
|
||
>
|
||
+ Klausel hinzufügen
|
||
</button>
|
||
)}
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<div className="flex-1 grid grid-cols-12 gap-1 items-center text-xs">
|
||
<select
|
||
className="col-span-4 px-1 py-0.5 border border-gray-300 rounded bg-white truncate"
|
||
value={clause.field}
|
||
disabled={readOnly}
|
||
onChange={(e) => onChange({ ...clause, field: e.target.value })}
|
||
>
|
||
{PROFILE_FIELDS.map((f) => (
|
||
<option key={f.key} value={f.key}>{f.label} ({f.key})</option>
|
||
))}
|
||
</select>
|
||
|
||
<select
|
||
className="col-span-3 px-1 py-0.5 border border-gray-300 rounded bg-white"
|
||
value={clause.op}
|
||
disabled={readOnly}
|
||
onChange={(e) => onChange({ ...clause, op: e.target.value as ClauseOperator })}
|
||
>
|
||
{operators.map((op) => (
|
||
<option key={op} value={op}>{OPERATOR_LABELS[op]}</option>
|
||
))}
|
||
</select>
|
||
|
||
<div className="col-span-5">
|
||
{requiresValue && (
|
||
<ValueInput
|
||
field={field}
|
||
multi={multiValue}
|
||
value={clause.value}
|
||
onChange={(v) => onChange({ ...clause, value: v })}
|
||
readOnly={readOnly}
|
||
/>
|
||
)}
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|
||
|
||
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 (
|
||
<select
|
||
multiple
|
||
className="w-full px-1 py-0.5 border border-gray-300 rounded bg-white h-16"
|
||
value={selected}
|
||
disabled={readOnly}
|
||
onChange={(e) => {
|
||
const opts = Array.from(e.target.selectedOptions, (o) => o.value)
|
||
onChange(opts)
|
||
}}
|
||
>
|
||
{field.options.map((o) => (
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
))}
|
||
</select>
|
||
)
|
||
}
|
||
return (
|
||
<select
|
||
className="w-full px-1 py-0.5 border border-gray-300 rounded bg-white"
|
||
value={typeof value === 'string' ? value : ''}
|
||
disabled={readOnly}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
>
|
||
<option value="">— wählen —</option>
|
||
{field.options.map((o) => (
|
||
<option key={o.value} value={o.value}>{o.label}</option>
|
||
))}
|
||
</select>
|
||
)
|
||
}
|
||
|
||
if (field.type === 'number') {
|
||
return (
|
||
<input
|
||
type="number"
|
||
className="w-full px-1 py-0.5 border border-gray-300 rounded"
|
||
value={typeof value === 'number' ? value : 0}
|
||
disabled={readOnly}
|
||
onChange={(e) => onChange(Number(e.target.value))}
|
||
/>
|
||
)
|
||
}
|
||
|
||
if (field.type === 'boolean') {
|
||
return (
|
||
<select
|
||
className="w-full px-1 py-0.5 border border-gray-300 rounded bg-white"
|
||
value={value ? 'true' : 'false'}
|
||
disabled={readOnly}
|
||
onChange={(e) => onChange(e.target.value === 'true')}
|
||
>
|
||
<option value="true">true</option>
|
||
<option value="false">false</option>
|
||
</select>
|
||
)
|
||
}
|
||
|
||
return (
|
||
<input
|
||
type="text"
|
||
className="w-full px-1 py-0.5 border border-gray-300 rounded"
|
||
value={typeof value === 'string' ? value : ''}
|
||
disabled={readOnly}
|
||
onChange={(e) => onChange(e.target.value)}
|
||
/>
|
||
)
|
||
}
|