Files
OCR-SPRIN-SERVICE/docs/FRONTEND-INTEGRATION.md

11 KiB

Frontend Integration Guide

Dokumen ini menjelaskan kontrak API yang perlu dipakai frontend untuk upload dokumen sprint, menampilkan hasil OCR, menjalankan review manual, dan approve hasil final.

Base URL

Default local API:

http://localhost:8000/api/v1

Untuk frontend, simpan URL di environment variable:

VITE_OCR_API_BASE_URL=http://localhost:8000/api/v1

Jika API_KEYS di backend diisi, semua endpoint protected membutuhkan header:

X-API-Key: <api-key>

Catatan: jangan expose API key production di frontend publik. Untuk deployment internal, gunakan reverse proxy atau session backend-for-frontend jika aksesnya tidak sepenuhnya trusted.

Health Check

GET /health
GET /health/ready

Contoh response /health:

{
  "status": "ok",
  "version": "0.1.0"
}

Contoh response /health/ready:

{
  "status": "ready",
  "version": "0.1.0",
  "models": {
    "paddleocr": "ready",
    "pp_structure": "disabled"
  }
}

Gunakan /health/ready untuk disable upload button sampai model OCR siap.

Upload Dokumen

Endpoint:

POST /documents
POST /documents?sync=true

Body harus multipart/form-data dengan field file.

Backend menerima PDF dan format image umum. Default max upload mengikuti backend config BLOB_MAX_UPLOAD_MB, saat ini 25 MB.

Untuk frontend production, gunakan async flow:

  1. POST /documents
  2. Jika status HTTP 202, ambil job_id
  3. Poll GET /documents/{job_id} setiap 1-3 detik
  4. Stop polling saat status completed, needs_review, atau failed

Untuk local dev sederhana, POST /documents?sync=true boleh dipakai, tetapi request bisa lama karena OCR berjalan inline.

Upload Example

const API_BASE = import.meta.env.VITE_OCR_API_BASE_URL;
const API_KEY = import.meta.env.VITE_OCR_API_KEY;

async function uploadDocument(file: File) {
  const form = new FormData();
  form.append("file", file);

  const res = await fetch(`${API_BASE}/documents`, {
    method: "POST",
    headers: API_KEY ? { "X-API-Key": API_KEY } : undefined,
    body: form,
  });

  if (!res.ok) {
    throw await readApiError(res);
  }

  return (await res.json()) as DocumentResponse;
}

Polling Job

Endpoint:

GET /documents/{job_id}
const TERMINAL_STATUSES = new Set(["completed", "needs_review", "failed"]);

async function getDocument(jobId: string) {
  const res = await fetch(`${API_BASE}/documents/${jobId}`, {
    headers: API_KEY ? { "X-API-Key": API_KEY } : undefined,
  });

  if (!res.ok) {
    throw await readApiError(res);
  }

  return (await res.json()) as DocumentResponse;
}

async function pollDocument(jobId: string, onUpdate: (doc: DocumentResponse) => void) {
  while (true) {
    const doc = await getDocument(jobId);
    onUpdate(doc);

    if (TERMINAL_STATUSES.has(doc.status)) {
      return doc;
    }

    await new Promise((resolve) => setTimeout(resolve, 2000));
  }
}

Response Schema

DocumentResponse

type DocumentStatus =
  | "pending"
  | "processing"
  | "completed"
  | "needs_review"
  | "failed";

type DocumentResponse = {
  job_id: string;
  status: DocumentStatus;
  confidence: number | null;
  data: ExtractionResult | null;
  review_flags: ReviewFlag[];
  error: string | null;
  approved: boolean;
  reviewed_by: string | null;
  reviewed_at: string | null;
};

ExtractionResult

type ExtractionResult = {
  header: HeaderFields;
  personel: PersonnelEntry[];
  untuk: string[];
  ttd: Signatory;
  raw_text: string;
  confidence: number;
  review_flags: ReviewFlag[];
};

type HeaderFields = {
  nomor_sprint: string | null;
  tanggal: string | null; // YYYY-MM-DD
  satuan_penerbit: string | null;
  perihal: string | null;
  dasar: string[];
};

type PersonnelEntry = {
  no: number | null;
  pangkat: string | null;
  nrp: string | null;
  nama: string | null;
  jabatan_dinas: string | null;
  jabatan_sprint: string | null;
  keterangan: string | null;
  confidence: number;
};

type Signatory = {
  nama: string | null;
  pangkat: string | null;
  nrp: string | null;
  jabatan: string | null;
};

Review Flags

type ReviewFlag =
  | "low_ocr_confidence"
  | "missing_field"
  | "invalid_nrp"
  | "unknown_pangkat"
  | "personnel_count_mismatch"
  | "date_parse_failed"
  | "llm_fallback"
  | "llm_unavailable"
  | "personnel_text_fallback"
  | "personnel_text_fallback_no_nrp"
  | "incomplete_personnel_row";

Recommended UI labels:

Flag Label
low_ocr_confidence Confidence OCR rendah
missing_field Field wajib belum lengkap
invalid_nrp NRP tidak valid
unknown_pangkat Pangkat tidak dikenali
personnel_count_mismatch Jumlah personel perlu dicek
date_parse_failed Tanggal gagal dibaca
llm_fallback Sebagian field diisi fallback LLM
llm_unavailable LLM tidak tersedia
personnel_text_fallback Personel dibaca dari fallback teks
personnel_text_fallback_no_nrp Personel dibaca tanpa NRP
incomplete_personnel_row Baris personel belum lengkap

Example Final Response

{
  "job_id": "e21e83ed-a42c-4672-baec-914e5c60cc5a",
  "status": "needs_review",
  "confidence": 0.82,
  "data": {
    "header": {
      "nomor_sprint": "Sprin/123/IV/2026",
      "tanggal": "2026-04-21",
      "satuan_penerbit": "POLRES BANJAR",
      "perihal": "Instruktur Ops Pekat I Lodaya 2026",
      "dasar": []
    },
    "personel": [
      {
        "no": 1,
        "pangkat": "IPDA",
        "nrp": "12345678",
        "nama": "BUDI SANTOSO",
        "jabatan_dinas": "KANIT",
        "jabatan_sprint": "INSTRUKTUR",
        "keterangan": null,
        "confidence": 0.91
      }
    ],
    "untuk": ["Melaksanakan kegiatan sesuai surat perintah."],
    "ttd": {
      "nama": "AGUS",
      "pangkat": "AKBP",
      "nrp": "87654321",
      "jabatan": "KAPOLRES"
    },
    "raw_text": "full OCR text...",
    "confidence": 0.82,
    "review_flags": ["low_ocr_confidence"]
  },
  "review_flags": ["low_ocr_confidence"],
  "error": null,
  "approved": false,
  "reviewed_by": null,
  "reviewed_at": null
}

raw_text bisa panjang. Tampilkan di collapsible/debug panel, bukan di layar utama.

Review dan Koreksi HITL

Frontend review screen sebaiknya mengizinkan editor untuk:

  • Header: nomor sprint, tanggal, satuan penerbit, perihal, dasar
  • Personel: pangkat, NRP, nama, jabatan dinas, jabatan sprint, keterangan
  • Untuk: daftar tugas
  • TTD: nama, pangkat, NRP, jabatan

Patch Corrections

Endpoint:

PATCH /documents/{job_id}

Body:

{
  "corrections": [
    {
      "path": "header.perihal",
      "value": "Pelaksanaan Operasi Pekat I Lodaya 2026",
      "reason": "OCR membaca perihal tidak lengkap"
    },
    {
      "path": "personel[0].nama",
      "value": "BUDI SANTOSO",
      "reason": "Perbaikan nama"
    }
  ]
}

Header opsional untuk audit trail:

X-User-Id: reviewer-a

Path yang umum dipakai:

header.nomor_sprint
header.tanggal
header.satuan_penerbit
header.perihal
header.dasar
ttd.nama
ttd.pangkat
ttd.nrp
ttd.jabatan
personel[0].pangkat
personel[0].nrp
personel[0].nama
personel[0].jabatan_dinas
personel[0].jabatan_sprint
personel[0].keterangan
untuk

Semua correction dalam satu request bersifat atomic. Jika satu path invalid, seluruh batch ditolak dan tidak ada perubahan disimpan.

Patch Example

async function patchDocument(jobId: string, corrections: FieldCorrection[], userId?: string) {
  const headers: Record<string, string> = { "Content-Type": "application/json" };
  if (API_KEY) headers["X-API-Key"] = API_KEY;
  if (userId) headers["X-User-Id"] = userId;

  const res = await fetch(`${API_BASE}/documents/${jobId}`, {
    method: "PATCH",
    headers,
    body: JSON.stringify({ corrections }),
  });

  if (!res.ok) {
    throw await readApiError(res);
  }

  return (await res.json()) as DocumentResponse;
}

type FieldCorrection = {
  path: string;
  value: unknown;
  reason?: string | null;
};

Correction History

Endpoint:

GET /documents/{job_id}/history

Response:

type CorrectionEventResponse = {
  id: number;
  job_id: string;
  field_path: string;
  old_value: unknown | null;
  new_value: unknown | null;
  corrected_by: string | null;
  reason: string | null;
  corrected_at: string;
};

Gunakan endpoint ini untuk audit panel di halaman review.

Approve Final Result

Endpoint:

POST /documents/{job_id}/approve

Header opsional:

X-User-Id: reviewer-a

Response:

{
  "job_id": "e21e83ed-a42c-4672-baec-914e5c60cc5a",
  "approved": true,
  "reviewed_by": "reviewer-a",
  "reviewed_at": "2026-04-26T16:30:00"
}

Setelah approved, PATCH /documents/{job_id} akan ditolak dengan 409.

Error Handling

Application errors:

{
  "error": "UnsupportedDocumentError",
  "message": "Uploaded file is empty."
}

FastAPI validation errors memakai shape standar:

{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "file"],
      "msg": "Field required"
    }
  ]
}

Helper error:

async function readApiError(res: Response) {
  let payload: unknown = null;
  try {
    payload = await res.json();
  } catch {
    payload = await res.text();
  }

  return {
    status: res.status,
    payload,
  };
}

Recommended UI handling:

HTTP Status UI Handling
400 Tampilkan pesan validasi/upload
401 Session/API key tidak valid
404 Job tidak ditemukan
409 Job belum selesai atau sudah approved
422 Form correction tidak valid
500 Tampilkan error umum dan minta operator cek log backend

Ground Truth Admin

Endpoint ini opsional untuk dashboard admin/training data.

GET /ground-truth/stats?top_n=10
GET /ground-truth/export?approved_only=true&has_corrections=true&limit=1000

/ground-truth/export mengembalikan application/x-ndjson, satu JSON per baris. Frontend biasanya cukup menyediakan tombol download, bukan parse seluruh stream di browser.

  1. Upload screen

    • Dropzone file PDF/image
    • Health readiness badge
    • Upload progress
    • Processing state setelah job_id diterima
  2. Result screen

    • Status badge
    • Confidence score
    • Review flags
    • Header summary
    • Personnel table
    • Untuk list
    • TTD section
    • Raw OCR collapsible
  3. Review screen

    • Editable fields
    • Dirty-state tracking
    • Correction reason input
    • Save corrections via PATCH
    • History panel
    • Approve button
  4. Admin screen

    • Health/ready status
    • Ground-truth stats
    • Export approved samples

UX Rules

  • Jangan tunggu POST /documents?sync=true untuk production UI; gunakan async + polling.
  • Disable approve kalau status masih pending atau processing.
  • Tampilkan needs_review sebagai hasil yang berhasil diproses tetapi perlu validasi manusia.
  • Jangan render raw_text sebagai konten utama.
  • Pada failed, tampilkan error dari response jika ada.
  • Pada confidence rendah, arahkan user ke review fields yang punya flag terkait.