import { MetricGrid } from "@govoplan/core-webui"; import { CheckCircle2, Database, Network, Pencil, Play, Plus, RefreshCw, Scale, Upload } from "lucide-react"; import { useCallback, useEffect, useMemo, useState, type FormEvent } from "react"; import { useSearchParams } from "react-router"; import { FormGrid, ActionToolbar, ToolbarSpacer, ActionBlockerHint, Button, ConfirmDialog, Dialog, DismissibleAlert, DocumentationHelpLink, FormField, IconButton, LoadingIndicator, MetricCard, SegmentedControl, SelectionList, SelectionListItem, StatePanel, StatusBadge, ToggleSwitch, hasScope, i18nMessage, useUnsavedDraftGuard, type PlatformRouteContext } from "@govoplan/core-webui"; import { createDisposition, getAssuranceGraph, getAssuranceSummary, getCandidate, importListSnapshot, listConnectorSnapshots, listAssuranceNodes, listListSnapshots, listReviewQueue, runScreening, saveAssuranceEdge, saveAssuranceNode, type AssuranceEdge, type AssuranceEdgeWrite, type AssuranceNode, type AssuranceNodeKind, type AssuranceNodeWrite, type AssuranceSummary, type CandidateDetail, type ConnectorSnapshot, type ListSnapshot, type ReviewQueueItem, type ScreeningRun } from "../../api/riskCompliance"; import { RISK_COMPLIANCE_ADMIN_DOCUMENTATION, RISK_COMPLIANCE_BLOCKER_LABELS, RISK_COMPLIANCE_DOCUMENTATION, RISK_COMPLIANCE_I18N } from "./interfacePatterns"; type ViewMode = "sources" | "screen" | "review" | "assurance"; export default function RiskCompliancePage({ settings, auth }: PlatformRouteContext) { const [searchParams] = useSearchParams(); const requestedView = searchParams.get("view"); const requestedAssuranceId = searchParams.get("node")?.trim() ?? ""; const [view, setView] = useState(() => requestedView === "assurance" ? "assurance" : "review" ); const [sourceSnapshots, setSourceSnapshots] = useState< ConnectorSnapshot[] >([]); const [sourcesAvailable, setSourcesAvailable] = useState(false); const [listSnapshots, setListSnapshots] = useState([]); const [queue, setQueue] = useState([]); const [selectedCandidateId, setSelectedCandidateId] = useState(""); const [candidate, setCandidate] = useState(null); const [run, setRun] = useState(null); const [assuranceNodes, setAssuranceNodes] = useState([]); const [assuranceSummary, setAssuranceSummary] = useState(null); const [selectedAssuranceId, setSelectedAssuranceId] = useState( requestedAssuranceId ); const [assuranceGraph, setAssuranceGraph] = useState<{ nodes: AssuranceNode[]; edges: AssuranceEdge[]; truncated: boolean; } | null>(null); const [loading, setLoading] = useState(true); const [busy, setBusy] = useState(false); const [error, setError] = useState(""); const [notice, setNotice] = useState(""); const [pendingImport, setPendingImport] = useState(null); const canAdmin = hasScope( auth, "risk_compliance:sanctions:admin" ); const canScreen = hasScope( auth, "risk_compliance:sanctions:screen" ); const canReview = hasScope( auth, "risk_compliance:sanctions:review" ); const canReadAssurance = [ "risk_compliance:workspace:read", "risk_compliance:workspace:write", "risk_compliance:workspace:admin" ].some((scope) => hasScope(auth, scope)); const canWriteAssurance = [ "risk_compliance:workspace:write", "risk_compliance:workspace:admin" ].some((scope) => hasScope(auth, scope)); useEffect(() => { if (requestedView === "assurance" && canReadAssurance) { setView("assurance"); if (requestedAssuranceId) { setSelectedAssuranceId(requestedAssuranceId); } } }, [canReadAssurance, requestedAssuranceId, requestedView]); useEffect(() => { if (view === "review" && !canReview) { setView(canReadAssurance ? "assurance" : "sources"); } else if (view === "assurance" && !canReadAssurance) { setView(canReview ? "review" : "sources"); } }, [canReadAssurance, canReview, view]); const refresh = useCallback(async () => { setLoading(true); setError(""); try { const [sources, lists, reviewQueue, assurance] = await Promise.all([ listConnectorSnapshots(settings), listListSnapshots(settings), canReview ? listReviewQueue(settings) : Promise.resolve({ candidates: [] }), canReadAssurance ? Promise.all([ listAssuranceNodes(settings), getAssuranceSummary(settings) ]).then(([nodes, summary]) => ({ nodes: nodes.nodes, summary })) : Promise.resolve({ nodes: [], summary: null }) ]); setSourcesAvailable(sources.available); setSourceSnapshots(sources.snapshots); setListSnapshots(lists.snapshots); setQueue(reviewQueue.candidates); setAssuranceNodes(assurance.nodes); setAssuranceSummary(assurance.summary); setSelectedCandidateId((current) => current && reviewQueue.candidates.some((item) => item.id === current) ? current : reviewQueue.candidates[0]?.id ?? "" ); setSelectedAssuranceId((current) => current || assurance.nodes[0]?.stable_id || "" ); } catch (reason) { setError(errorMessage(reason)); } finally { setLoading(false); } }, [canReadAssurance, canReview, settings]); useEffect(() => { void refresh(); }, [refresh]); useEffect(() => { if (!selectedCandidateId) { setCandidate(null); return; } let cancelled = false; void getCandidate(settings, selectedCandidateId) .then((item) => { if (!cancelled) setCandidate(item); }) .catch((reason) => { if (!cancelled) setError(errorMessage(reason)); }); return () => { cancelled = true; }; }, [selectedCandidateId, settings]); useEffect(() => { if (!selectedAssuranceId || !canReadAssurance) { setAssuranceGraph(null); return; } let cancelled = false; void getAssuranceGraph(settings, selectedAssuranceId) .then((graph) => { if (!cancelled) setAssuranceGraph(graph); }) .catch((reason) => { if (!cancelled) setError(errorMessage(reason)); }); return () => { cancelled = true; }; }, [canReadAssurance, selectedAssuranceId, settings]); async function importSnapshot(item: ConnectorSnapshot) { setBusy(true); setError(""); try { const result = await importListSnapshot(settings, item.ref); setNotice( result.created ? `Imported ${result.snapshot.entry_count} list entries.` : "This immutable snapshot was already imported." ); setPendingImport(null); await refresh(); } catch (reason) { setError(errorMessage(reason)); } finally { setBusy(false); } } return (
Sources ) }, { id: "screen", label: ( <> Screen ), disabled: !canScreen, title: !canScreen ? RISK_COMPLIANCE_I18N.screenRequired : undefined }, { id: "review", label: ( <> Review {queue.length > 0 && ( {queue.length} )} ), disabled: !canReview, title: !canReview ? RISK_COMPLIANCE_I18N.reviewRequired : undefined }, { id: "assurance", label: ( <> Assurance ), disabled: !canReadAssurance, title: !canReadAssurance ? RISK_COMPLIANCE_I18N.assuranceReadRequired : undefined } ]} /> {loading && } } onClick={() => void refresh()} disabled={loading || busy} disabledReason={loading ? RISK_COMPLIANCE_I18N.loading : busy ? RISK_COMPLIANCE_I18N.busy : undefined} /> {(error || notice) && (
{error && ( setError("")} > {error} )} {notice && !error && ( setNotice("")} > {notice} )}
)}
{view === "sources" && ( )} {view === "screen" && ( )} {view === "review" && ( )} {view === "assurance" && ( )}
setPendingImport(null)} onConfirm={() => pendingImport && void importSnapshot(pendingImport)} />
); } function SourcesPane({ available, sources, imported, canImport, busy, onImport }: { available: boolean; sources: ConnectorSnapshot[]; imported: ListSnapshot[]; canImport: boolean; busy: boolean; onImport: (item: ConnectorSnapshot) => void; }) { const importedRefs = useMemo( () => new Set(imported.map((item) => item.sha256)), [imported] ); return (
Connector evidence Immutable acquired source snapshots
{!available && ( )}
{sources.map((item) => { const isImported = importedRefs.has(item.sha256); return (
{item.publisher} {item.source_version} · {formatDate(item.acquired_at)} {item.sha256.slice(0, 16)}…
{isImported ? ( ) : ( } disabled={!canImport || busy} disabledReason={!canImport ? RISK_COMPLIANCE_I18N.adminRequired : busy ? RISK_COMPLIANCE_I18N.busy : undefined} onClick={() => void onImport(item)} /> )}
); })}
Screening catalogues Normalized, immutable list versions
{imported.map((item) => (
{item.publisher} {item.entry_count.toLocaleString()} entries ·{" "} {item.normalization_version} {item.source_version}
))} {!imported.length && ( )}
); } function ScreenPane({ settings, snapshots, run, onRun, onError }: { settings: PlatformRouteContext["settings"]; snapshots: ListSnapshot[]; run: ScreeningRun | null; onRun: (run: ScreeningRun | null) => void; onError: (message: string) => void; }) { const [listId, setListId] = useState(snapshots[0]?.id ?? ""); const [subjectType, setSubjectType] = useState<"person" | "entity">( "person" ); const [name, setName] = useState(""); const [identifier, setIdentifier] = useState(""); const [submitting, setSubmitting] = useState(false); useEffect(() => { if (!snapshots.some((item) => item.id === listId)) { setListId(snapshots[0]?.id ?? ""); } }, [listId, snapshots]); const draftDirty = Boolean(name.trim() || identifier.trim()); const submitDisabledReason = submitting ? RISK_COMPLIANCE_I18N.busy : !listId ? RISK_COMPLIANCE_I18N.snapshotRequired : !name.trim() && !identifier.trim() ? RISK_COMPLIANCE_I18N.subjectRequired : undefined; useUnsavedDraftGuard({ dirty: draftDirty, onSave: executeScreening, onDiscard: () => { setName(""); setIdentifier(""); } }); async function submit(event: FormEvent) { event.preventDefault(); await executeScreening(); } async function executeScreening(): Promise { if (submitDisabledReason) return false; setSubmitting(true); onError(""); try { const response = await runScreening(settings, { list_snapshot_id: listId, idempotency_key: crypto.randomUUID(), subject: { subject_type: subjectType, primary_name: name, identifiers: identifier.trim() ? [{ type: "document", value: identifier.trim() }] : [] } }); onRun(response.run); setName(""); setIdentifier(""); return true; } catch (reason) { onError(errorMessage(reason)); return false; } finally { setSubmitting(false); } } return (
New screening Use only the data needed for comparison
{!snapshots.length && ( )} setName(event.target.value)} maxLength={1000} /> setIdentifier(event.target.value)} maxLength={1000} />
Result Version-pinned candidate evidence
{run && }
{!run && ( )} {run && (
{run.candidates.map((item) => (
{item.score}
{item.entry.primary_name} {item.match_kind} · {item.entry.source_entry_id}
))} {run.outcome === "clear" && (
No candidate met the configured threshold.
)}
)}
); } function ReviewPane({ queue, selectedId, detail, busy, onSelect, onBusy, onError, onNotice, onRefresh, settings }: { queue: ReviewQueueItem[]; selectedId: string; detail: CandidateDetail | null; busy: boolean; onSelect: (id: string) => void; onBusy: (value: boolean) => void; onError: (message: string) => void; onNotice: (message: string) => void; onRefresh: () => Promise; settings: PlatformRouteContext["settings"]; }) { const [dialogOpen, setDialogOpen] = useState(false); const [decision, setDecision] = useState("false_positive"); const [reason, setReason] = useState(""); const [reusable, setReusable] = useState(false); const [expiresAt, setExpiresAt] = useState(""); const dispositionDirty = dialogOpen && Boolean( reason.trim() || reusable || expiresAt || decision !== "false_positive" ); const dispositionDisabledReason = busy ? RISK_COMPLIANCE_I18N.busy : !detail ? RISK_COMPLIANCE_I18N.candidateRequired : reason.trim().length < 3 ? RISK_COMPLIANCE_I18N.dispositionReasonRequired : reusable && !expiresAt ? RISK_COMPLIANCE_I18N.exceptionExpiryRequired : undefined; function resetDisposition() { setDialogOpen(false); setDecision("false_positive"); setReason(""); setReusable(false); setExpiresAt(""); } useUnsavedDraftGuard({ dirty: dispositionDirty, onSave: recordDisposition, onDiscard: resetDisposition }); async function submitDisposition(event: FormEvent) { event.preventDefault(); await recordDisposition(); } async function recordDisposition(): Promise { if (dispositionDisabledReason || !detail) return false; onBusy(true); onError(""); try { await createDisposition(settings, detail.candidate.id, { decision, reason, exception_scope: reusable ? "subject_entry" : "candidate", expires_at: reusable && expiresAt ? new Date(`${expiresAt}T23:59:59`).toISOString() : null }); resetDisposition(); onNotice("The disposition was recorded as append-only evidence."); await onRefresh(); return true; } catch (reasonValue) { onError(errorMessage(reasonValue)); return false; } finally { onBusy(false); } } return (
Candidate evidence Subject and immutable list entry comparison
{!detail && ( )} {detail && (
item.value || "" )} dates={detail.subject.dates} /> item.name )} identifiers={detail.candidate.entry.identifiers.map( (item) => `${item.identifier_type}: ${item.value}` )} dates={detail.candidate.entry.dates.map( (item) => item.value )} />
{detail.candidate.score}% ·{" "} {detail.candidate.match_kind} List {detail.list_snapshot.source_version} ·{" "} {detail.run.matcher_version} ·{" "} {detail.run.normalization_version} {detail.candidate.entry.raw_evidence_locator}
)}
} >