// Rules management view — visual, non-technical rule builder for alerts. const { useState: useRuleState, useMemo: useRuleMemo, useEffect: useRuleEffect } = React; const RULE_TEMPLATES = { threshold: { name: "", description: "", rule_type: "threshold", severity: "medium", priority: 5, condition: { operator: "gt", field: "amount", value: 10000 }, actions: [{ action: "create_alert", params: {} }], lookback_seconds: 86400, aggregation_key: null, tags: [], }, list_match: { name: "", description: "", rule_type: "list_match", severity: "high", priority: 4, condition: { operator: "in", field: "originator_country", value: ["XX", "YY"] }, actions: [{ action: "create_alert", params: {} }], lookback_seconds: 86400, aggregation_key: null, tags: [], }, velocity: { name: "", description: "", rule_type: "velocity", severity: "high", priority: 3, condition: { operator: "gt", field: "amount", value: 50000 }, actions: [{ action: "create_alert", params: {} }], lookback_seconds: 86400, aggregation_key: "originator_id", tags: [], }, anomaly: { name: "", description: "", rule_type: "anomaly", severity: "high", priority: 3, condition: { operator: "gt", field: "risk_score", value: 75 }, actions: [{ action: "create_alert", params: {} }], lookback_seconds: 86400, aggregation_key: null, tags: [], }, composite: { name: "", description: "", rule_type: "composite", severity: "critical", priority: 1, condition: { operator: "and", conditions: [ { operator: "gt", field: "amount", value: 50000 }, { operator: "in", field: "originator_country", value: ["XX", "YY"] }, ], }, actions: [{ action: "create_alert", params: {} }], lookback_seconds: 86400, aggregation_key: null, tags: [], }, }; const TRANSACTION_TYPES = ["wire", "ach", "card", "atm", "crypto", "cash"]; const FIELD_OPTIONS = [ { value: "amount", label: "Amount", type: "number", placeholder: "e.g. 10000" }, { value: "risk_score", label: "Risk score", type: "number", placeholder: "0-100" }, { value: "transaction_type", label: "Transaction type", type: "txn_type", placeholder: "e.g. card" }, { value: "originator_country", label: "Originator country", type: "country", placeholder: "e.g. US" }, { value: "beneficiary_country", label: "Beneficiary country", type: "country", placeholder: "e.g. US" }, { value: "originator_id", label: "Originator ID", type: "text", placeholder: "e.g. C123456" }, { value: "beneficiary_id", label: "Beneficiary ID", type: "text", placeholder: "e.g. M123456" }, ]; const OPERATORS_BY_TYPE = { number: [ { value: "gt", label: "is greater than" }, { value: "gte", label: "is greater than or equal to" }, { value: "lt", label: "is less than" }, { value: "lte", label: "is less than or equal to" }, { value: "eq", label: "equals" }, { value: "ne", label: "does not equal" }, { value: "between", label: "is between" }, ], text: [ { value: "eq", label: "equals" }, { value: "ne", label: "does not equal" }, { value: "in", label: "is one of" }, { value: "not_in", label: "is not one of" }, { value: "contains", label: "contains" }, { value: "regex", label: "matches pattern" }, ], country: [ { value: "eq", label: "equals" }, { value: "ne", label: "does not equal" }, { value: "in", label: "is one of" }, { value: "not_in", label: "is not one of" }, ], txn_type: [ { value: "in", label: "is one of" }, { value: "not_in", label: "is not one of" }, ], }; const ACTION_OPTIONS = [ { value: "create_alert", label: "Create alert", needsParam: false }, { value: "add_score", label: "Add risk score", needsParam: true, paramName: "score", paramLabel: "Score to add", paramType: "number" }, { value: "tag", label: "Tag transaction", needsParam: true, paramName: "tag", paramLabel: "Tag", paramType: "text" }, { value: "escalate", label: "Escalate", needsParam: false }, { value: "block", label: "Block", needsParam: false }, ]; const STATUS_PILL = { active: "ok", draft: "med", paused: "warn", archived: "low", }; function getFieldType(field) { return FIELD_OPTIONS.find(f => f.value === field)?.type || "text"; } function getFieldLabel(field) { return FIELD_OPTIONS.find(f => f.value === field)?.label || field; } function getOperatorLabel(op) { for (const type of Object.values(OPERATORS_BY_TYPE)) { const found = type.find(o => o.value === op); if (found) return found.label; } return op; } function formatValue(value, operator) { if (value == null) return "—"; if (Array.isArray(value)) return value.join(", "); if (typeof value === "object") return JSON.stringify(value); return String(value); } function conditionToSentence(condition) { if (!condition) return "No condition"; if (condition.operator === "and" || condition.operator === "or") { const parts = (condition.conditions || []).map(conditionToSentence).filter(Boolean); if (parts.length === 0) return "No condition"; if (parts.length === 1) return parts[0]; return parts.join(` ${condition.operator.toUpperCase()} `); } if (condition.operator === "not") { const sub = condition.conditions?.[0]; return sub ? `NOT (${conditionToSentence(sub)})` : "NOT (empty)"; } return `${getFieldLabel(condition.field)} ${getOperatorLabel(condition.operator)} ${formatValue(condition.value, condition.operator)}`; } function isListOperator(op) { return op === "in" || op === "not_in"; } function isBetweenOperator(op) { return op === "between"; } function parseListValue(raw) { return String(raw || "").split(",").map(s => s.trim()).filter(Boolean); } function serializeListValue(value) { return Array.isArray(value) ? value.join(", ") : String(value || ""); } function ensureNumberArray(value) { if (Array.isArray(value) && value.length === 2) return [Number(value[0]) || 0, Number(value[1]) || 0]; return [0, 0]; } function ConditionRow({ condition, onChange, onRemove, showRemove }) { const fieldType = getFieldType(condition.field); const operators = OPERATORS_BY_TYPE[fieldType] || OPERATORS_BY_TYPE.text; const updateField = (field) => { const newType = getFieldType(field); const newOperators = OPERATORS_BY_TYPE[newType] || OPERATORS_BY_TYPE.text; const newOp = newOperators.find(o => o.value === condition.operator)?.value ? condition.operator : newOperators[0].value; onChange({ ...condition, field, operator: newOp, value: isListOperator(newOp) ? [] : (isBetweenOperator(newOp) ? [0, 0] : "") }); }; const updateOperator = (operator) => { let value = condition.value; if (isListOperator(operator) && !Array.isArray(value)) value = []; if (isBetweenOperator(operator) && !Array.isArray(value)) value = [0, 0]; if (!isListOperator(operator) && !isBetweenOperator(operator) && Array.isArray(value)) value = ""; onChange({ ...condition, operator, value }); }; const renderValueInput = () => { if (fieldType === "txn_type") { const selected = Array.isArray(condition.value) ? condition.value : (condition.value ? [condition.value] : []); return (
{TRANSACTION_TYPES.map(t => { const checked = selected.includes(t); return ( ); })}
); } if (isListOperator(condition.operator)) { return ( onChange({ ...condition, value: parseListValue(e.target.value) })} placeholder="comma-separated values" /> ); } if (isBetweenOperator(condition.operator)) { const [min, max] = ensureNumberArray(condition.value); return (
onChange({ ...condition, value: [Number(e.target.value), max] })} placeholder="min" /> and onChange({ ...condition, value: [min, Number(e.target.value)] })} placeholder="max" />
); } if (fieldType === "number") { return ( onChange({ ...condition, value: Number(e.target.value) })} placeholder={FIELD_OPTIONS.find(f => f.value === condition.field)?.placeholder || ""} /> ); } return ( onChange({ ...condition, value: e.target.value })} placeholder={FIELD_OPTIONS.find(f => f.value === condition.field)?.placeholder || ""} /> ); }; return (
{renderValueInput()}
{showRemove && ( )}
); } function ConditionBuilder({ ruleType, condition, onChange }) { const isComposite = ruleType === "composite"; if (!isComposite) { return ( ); } const composite = condition?.operator === "and" || condition?.operator === "or" ? condition : { operator: "and", conditions: [condition || { operator: "gt", field: "amount", value: "" }] }; const updateLogic = (operator) => onChange({ ...composite, operator }); const updateCondition = (idx, newCond) => { const conditions = [...composite.conditions]; conditions[idx] = newCond; onChange({ ...composite, conditions }); }; const addCondition = () => { onChange({ ...composite, conditions: [...composite.conditions, { operator: "gt", field: "amount", value: "" }] }); }; const removeCondition = (idx) => { const conditions = composite.conditions.filter((_, i) => i !== idx); onChange({ ...composite, conditions }); }; return (
Match of the following conditions
{composite.conditions.map((c, i) => ( updateCondition(i, newC)} onRemove={() => removeCondition(i)} showRemove={composite.conditions.length > 1} /> ))}
); } function ActionBuilder({ actions, onChange }) { const updateAction = (idx, updates) => { const next = actions.map((a, i) => i === idx ? { ...a, ...updates } : a); onChange(next); }; const addAction = () => onChange([...actions, { action: "create_alert", params: {} }]); const removeAction = (idx) => onChange(actions.filter((_, i) => i !== idx)); return (
{actions.map((action, i) => { const meta = ACTION_OPTIONS.find(a => a.value === action.action) || ACTION_OPTIONS[0]; return (
{meta.needsParam && ( updateAction(i, { params: { [meta.paramName]: meta.paramType === "number" ? Number(e.target.value) : e.target.value } })} placeholder={meta.paramLabel} style={{ minWidth: 120 }} /> )} {actions.length > 1 && ( )}
); })}
); } function RuleEditor({ rule, onSave, onCancel }) { const isNew = !rule; const [form, setForm] = useRuleState(() => { const base = rule ? { ...rule } : RULE_TEMPLATES.threshold; return { ...base, tags: Array.isArray(base.tags) ? base.tags.join(", ") : "", }; }); const [error, setError] = useRuleState(null); const [saving, setSaving] = useRuleState(false); const update = (key, value) => setForm(f => ({ ...f, [key]: value })); const applyTemplate = (type) => { const tpl = RULE_TEMPLATES[type]; setForm(f => ({ ...f, ...tpl, name: f.name || tpl.name, description: f.description || tpl.description, tags: f.tags || "", })); }; const validate = () => { if (!form.name.trim()) return "Rule name is required"; if (form.rule_type === "composite") { if (!form.condition || !Array.isArray(form.condition.conditions) || form.condition.conditions.length === 0) { return "Composite rules need at least one condition"; } } else { if (!form.condition || !form.condition.field || !form.condition.operator) { return "Condition is incomplete"; } } if (!Array.isArray(form.actions) || form.actions.length === 0) return "At least one action is required"; return null; }; const submit = async (e) => { e.preventDefault(); setError(null); const validationError = validate(); if (validationError) { setError(validationError); return; } const payload = { name: form.name, description: form.description || null, rule_type: form.rule_type, severity: form.severity, priority: Number(form.priority), condition: form.condition, actions: form.actions, lookback_seconds: Number(form.lookback_seconds), aggregation_key: form.aggregation_key || null, effective_from: form.effective_from || null, tags: form.tags.split(",").map(t => t.trim()).filter(Boolean), }; setSaving(true); try { await onSave(payload); } catch (err) { setError(err.message); } finally { setSaving(false); } }; return (

{isNew ? "New detection rule" : "Edit rule"}

{error &&
{error}
} {isNew && (
{Object.keys(RULE_TEMPLATES).map(t => ( ))}
)}
update("name", e.target.value)} required placeholder="e.g. Large wire transfer" />
update("description", e.target.value)} placeholder="What does this rule detect?" />
update("priority", e.target.value)} required />
update("lookback_seconds", e.target.value === "" ? "" : Number(e.target.value) * 86400)} />
update("effective_from", e.target.value ? new Date(e.target.value).toISOString() : null)} /> Leave blank to activate immediately
{form.rule_type === "velocity" && (
)}
update("condition", c)} />
Preview: Alert when {conditionToSentence(form.condition)}
update("actions", a)} />
update("tags", e.target.value)} placeholder="aml, structuring, high-risk" />
); } function SimulationPanel({ rules }) { const [selectedRuleId, setSelectedRuleId] = useRuleState(""); const [amountThreshold, setAmountThreshold] = useRuleState(""); const [countThreshold, setCountThreshold] = useRuleState(""); const [startDate, setStartDate] = useRuleState(""); const [endDate, setEndDate] = useRuleState(""); const [entityId, setEntityId] = useRuleState(""); const [loading, setLoading] = useRuleState(false); const [result, setResult] = useRuleState(null); const [error, setError] = useRuleState(null); useRuleEffect(() => { const end = new Date(); const start = new Date(); start.setDate(start.getDate() - 90); setEndDate(end.toISOString().slice(0, 10)); setStartDate(start.toISOString().slice(0, 10)); }, []); useRuleEffect(() => { if (rules && rules.length && !selectedRuleId) { const first = rules[0]; setSelectedRuleId(first.id); setAmountThreshold(String(first.condition?.value || "")); } }, [rules]); const run = async () => { if (!selectedRuleId || !startDate || !endDate) return; setLoading(true); setError(null); setResult(null); try { const overrides = {}; const overrideCondition = {}; if (amountThreshold !== "") overrideCondition.value = Number(amountThreshold); if (countThreshold !== "") overrideCondition.count = Number(countThreshold); if (Object.keys(overrideCondition).length) { overrides[selectedRuleId] = overrideCondition; } const body = { rule_ids: [selectedRuleId], condition_overrides: overrides, start_date: new Date(startDate).toISOString(), end_date: new Date(endDate + "T23:59:59Z").toISOString(), entity_scope: entityId.trim() ? "specific_entity" : "all", entity_id: entityId.trim() || null, }; const res = await window.mossadApi.simulateRules(body); setResult(res); } catch (err) { setError(err.message); } finally { setLoading(false); } }; const selectedRule = rules.find(r => r.id === selectedRuleId); return (

Rule backtesting

Simulate thresholds without changing production rules
Rule
Amount threshold
setAmountThreshold(e.target.value)} placeholder={selectedRule?.condition?.value ?? "e.g. 10000"} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg-2)", color: "var(--fg)" }} />
Count threshold
setCountThreshold(e.target.value)} placeholder="velocity only" style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg-2)", color: "var(--fg)" }} />
Specific entity ID (optional)
setEntityId(e.target.value)} placeholder="e.g. C123456 or customer hash" style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg-2)", color: "var(--fg)" }} />
Start date
setStartDate(e.target.value)} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg-2)", color: "var(--fg)" }} />
End date
setEndDate(e.target.value)} style={{ width: "100%", padding: 8, borderRadius: 6, border: "1px solid var(--border)", background: "var(--bg-2)", color: "var(--fg)" }} />
{error && (
{error}
)} {result && (
{result.total_alerts_generated.toLocaleString()}
Alerts generated
{result.unique_entities_flagged.toLocaleString()}
Entities flagged
{result.existing_cases_covered.toLocaleString()}
Existing cases covered
{result.existing_sars_covered.toLocaleString()}
Existing SARs covered

Rule breakdown

{result.rule_breakdown.map(rb => ( ))}
Rule Type Evaluated Alerts Entities
{rb.rule_name} {rb.rule_type} {rb.transactions_evaluated.toLocaleString()} {rb.alerts_generated.toLocaleString()} {rb.unique_entities.toLocaleString()}
{result.sample_alerts.length > 0 && ( <>

Sample alerts

{result.sample_alerts.slice(0, 10).map((sa, i) => ( ))}
Entity Rule Txn ID Amount Severity
{sa.primary_entity_id} {sa.rule_name} {sa.external_id || sa.transaction_id.slice(0, 8)} ${Number(sa.amount).toLocaleString(undefined, { minimumFractionDigits: 2, maximumFractionDigits: 2 })} {sa.currency} {sa.severity}
)}
)}
); } function RulesView({ data }) { const rules = data?.rules || window.RULES_API || []; const [editing, setEditing] = useRuleState(null); const [filter, setFilter] = useRuleState("all"); const [busyId, setBusyId] = useRuleState(null); const [tab, setTab] = useRuleState("rules"); const filtered = useRuleMemo(() => { if (filter === "all") return rules; return rules.filter(r => r.status === filter); }, [rules, filter]); const handleSave = async (payload) => { const api = window.mossadApi; if (editing && editing.id) { await api.updateRule(editing.id, payload); } else { await api.createRule(payload); } await data.refresh(); setEditing(null); }; const setStatus = async (rule, status) => { setBusyId(rule.id); try { await window.mossadApi.updateRule(rule.id, { status }); await data.refresh(); } catch (err) { alert("Failed to update rule: " + err.message); } finally { setBusyId(null); } }; const removeRule = async (rule) => { if (!confirm(`Delete rule "${rule.name}"? This cannot be undone.`)) return; setBusyId(rule.id); try { await window.mossadApi.deleteRule(rule.id); await data.refresh(); } catch (err) { alert("Failed to delete rule: " + err.message); } finally { setBusyId(null); } }; if (editing) { return (
setEditing(null)} />
); } return (

Rule Settings

{tab === "rules" && ( <> )}
{["rules", "simulation"].map(t => ( ))}
{tab === "simulation" && } {tab === "rules" && filtered.length === 0 && (
No rules match the selected filter.
)} {tab === "rules" && (
{filtered.map(r => (
{r.name}
{r.rule_type} · priority {r.priority}
{r.status} {r.severity}
{r.description && (
{r.description}
)}
Triggers when {conditionToSentence(r.condition)}
Lookback: {(Number(r.lookback_seconds) / 86400).toLocaleString(undefined, { maximumFractionDigits: 2 })}d {r.aggregation_key ? ` · Aggregate by ${r.aggregation_key}` : ""} {r.effective_from ? ` · Effective: ${new Date(r.effective_from).toLocaleString()}` : ""} {Array.isArray(r.tags) && r.tags.length > 0 ? ` · ${r.tags.join(", ")}` : ""}
{r.status !== "active" && ( )} {r.status === "active" && ( )} {r.status !== "archived" && ( )} {r.status === "archived" && ( )}
))}
)}
); } Object.assign(window, { RulesView });