think
16px
820px

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):

  1. 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.
  2. Spesimen selektor shall have a list of matches, like in the Pemohon's view.
  3. 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:

flowchart LR UI["Selector modal
(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-cropdisk-cached (spescrop-<id>.png), dormant-safe (404 when SIGNATURE_VERIFY_URL unset), 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% — ") with the map toggle inline above the crops.
- 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 to isVerifikator). 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/file posture: open API, FE-gated display. Gating the crop alone while /file stays 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 TERPAKAI writes; .env never committed.

Verification

  • Suites green: frontend 985, backend 2561 / 0 fail. New signer-edit-modal test 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): /crop healthy (v4, GPU cuda: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.pyPOST /crop
  • backend/src/services/apostille-signature-verify.tsensureSpecimenSignatureCrop + invalidateSpecimenSignatureCrop
  • backend/src/routes/apostille-spesimen.tsGET /spesimen/:id/signature-crop + cache invalidation
  • frontend/src/components/apostille/signer-edit-modal.tsx — visual selector redesign
  • frontend/src/components/apostille/signature-compare-modal.tsx — declutter
  • frontend/src/pages/apostille/ApostilleReviewPage.tsx — wire showSpecimenSignatures
  • tests: signer-edit-modal.test.tsx (new), signature-compare-modal.test.tsx