Visual Spesimen Selector + Declutter Compare Modal
Date: 2026-07-14 · Branch: refactor/tidyup-foundation · Deploy: web ahu-ai-ocr:5015e26e (@192.168.83.20:3520) + ahu-signature-verify (new POST /crop, model v4)
Efran's 3-item wave (with a screenshot of the busy compare modal):
- Spesimen selector for Verifikator shall surface the actual spesimen, not only the name — the actual specimen shown side-by-side with the document's signature.
- Spesimen selektor shall have a list of matches, like in the Pemohon's view.
- The "Perbandingan tanda tangan (AI)" UI is too busy — make it simpler.
Why this matters: the Darsani contamination, now visible
The AHU registry's newer spesimen-file/YYYYMM/ format systematically attaches the wrong official's signature: a FORMULIR-02 "Keterangan Keabsahan" filed under a retired officer's name often carries the successor's signature. "Drs. H. Darsani" has 4 registry entries — only one (2655, pre-migration) is genuinely his.
Until now the verifikator only saw names in the selector — four near-identical "Drs. Darsani" rows, indistinguishable. Now they see the actual signature of each, so the contamination is obvious at a glance:
| Spesimen | Whose signature it actually is | Ink |
|---|---|---|
| 2655 | Drs. H. Darsani (genuine) | black |
| 290994327 | Sari Nurlistiana (successor) | blue |
| 291063455 | Sari Nurlistiana (successor) | blue |
| 291092585 | Galih Yudha Praja (third officer) | blue |
Live demo submission b01bb469 was auto-matched to the contaminated 291063455. In the redesigned selector the current "Terpilih" row shows the blue successor signature, while the genuine black Darsani (2655) sits right below as a one-click "Pilih" — the verifikator can now see the mismatch and correct it.
Ask 1 — the actual specimen signature, per candidate
The registry specimens are full PDF form sheets (FORMULIR-02), not pre-cropped signatures — so a raw thumbnail would show a form, not a signature. Instead we detect + crop the signature on demand:
(verifikator)"] -->|"GET /spesimen/:id/signature-crop"| BE[Backend] BE -->|disk cache hit| PNG[["spescrop-<id>.png"]] BE -->|miss → ensureSpecimenFile| MinIO[(MinIO / cache)] BE -->|"POST /crop (sheet bytes)"| SVC["ahu-signature-verify
/crop — best crop"] SVC -->|crop_png_b64| BE BE -->|cache to disk| PNG PNG --> UI SVC -. "found=false / outage" .-> FB["404 → UI falls back
to full-sheet link"]
- Service
POST /crop— best signature crop from a single file (reuses the specimen-side detection already used inside/verify; returns the raw padded crop, stamp/text intact = more human-legible than the isolated strokes used for scoring). - Backend
GET /api/apostille/spesimen/:id/signature-crop— disk-cached (spescrop-<id>.png), dormant-safe (404 whenSIGNATURE_VERIFY_URLunset), fail-soft (404 on outage/no-detection). Cache invalidated on manual specimen re-upload. - Cost: one GPU detect per distinct specimen, cached permanently. For Darsani, 4 detections on first open, then instant.
The document's own signature crop is shown once at the top of the modal ("Tanda tangan pada dokumen") as the fixed reference each candidate is compared against.
Ask 2 — the ranked list of matches
The selector now renders the same signer.candidates list the Pemohon block shows — ranked, grouped by officer (same-officer re-registrations collapse behind "+N registrasi lain"), each with Kecocokan N% and Spesimen ✓ chips. It shows for both roles; only the crops are verifikator-gated. When there are no candidates (UNMATCHED), the modal falls straight back to the registry search + manual edit (previous behaviour).
Ask 3 — decluttered compare modal
Before: title + 2 badges + a large yellow caveat paragraph + 2 crops + a titled "Peta kemiripan" box with a 4-line legend + a metadata row + a manual-verdict box + a 3-button footer — three stacked bordered boxes of prose.
After:
- Title "Perbandingan tanda tangan" (dropped "(AI)"); score folded into one compact chip ("Kemiripan AI: N% —
- The long honesty caveat and the heatmap legend collapse into a single "Apa arti skor ini?" disclosure (one click away, always in the DOM).
- Manual verdict kept prominent (the decision surface); timestamp / full-sheet link / "Bandingkan dengan spesimen lain" consolidated into one muted row.
Security / design posture
- Crops are verifikator-only (
showSpecimenSignatures, wired toisVerifikator). A genuine signature is forgery-sensitive — same policy that already hides the full-sheet link and the AI score from the Pemohon. The Pemohon still sees candidate names + scores. - The crop endpoint matches the existing
/spesimen/:id/fileposture: open API, FE-gated display. Gating the crop alone while/filestays open would be security theatre (the crop is derived from the sheet). If the whole specimen surface should be API-gated, that's a separate hardening task for both endpoints. - Registry access stays read-only; no
TERPAKAIwrites;.envnever committed.
Verification
- Suites green: frontend 985, backend 2561 / 0 fail. New
signer-edit-modaltest suite covers candidate grouping, per-role crop gating, expand, no-candidate→search fallback, and the pick call; compare-modal score-chip assertions updated. File-size ceiling test green. - Live on
b01bb469(admin persona, staging shadow-enforce):/crophealthy (v4, GPUcuda:0); backend route returns PNG for 2655/290994327/291092585; the three screenshots (compare declutter, selector collapsed, selector expanded) confirm the layout and the visible contamination. - Known limit:
291092585's crop is a wide region (captures the printed name "Galih Yudha Praja" + stamp) — diagnostically useful but detection is imperfect on that sheet layout; sheets with no upright detection fall back to the full-sheet link.
Files
signature-verify-service/app/main.py—POST /cropbackend/src/services/apostille-signature-verify.ts—ensureSpecimenSignatureCrop+invalidateSpecimenSignatureCropbackend/src/routes/apostille-spesimen.ts—GET /spesimen/:id/signature-crop+ cache invalidationfrontend/src/components/apostille/signer-edit-modal.tsx— visual selector redesignfrontend/src/components/apostille/signature-compare-modal.tsx— declutterfrontend/src/pages/apostille/ApostilleReviewPage.tsx— wireshowSpecimenSignatures- tests:
signer-edit-modal.test.tsx(new),signature-compare-modal.test.tsx