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.
Recommended Flow
Untuk frontend production, gunakan async flow:
POST /documents- Jika status HTTP
202, ambiljob_id - Poll
GET /documents/{job_id}setiap 1-3 detik - Stop polling saat status
completed,needs_review, ataufailed
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.
Recommended Screens
-
Upload screen
- Dropzone file PDF/image
- Health readiness badge
- Upload progress
- Processing state setelah
job_idditerima
-
Result screen
- Status badge
- Confidence score
- Review flags
- Header summary
- Personnel table
- Untuk list
- TTD section
- Raw OCR collapsible
-
Review screen
- Editable fields
- Dirty-state tracking
- Correction reason input
- Save corrections via
PATCH - History panel
- Approve button
-
Admin screen
- Health/ready status
- Ground-truth stats
- Export approved samples
UX Rules
- Jangan tunggu
POST /documents?sync=trueuntuk production UI; gunakan async + polling. - Disable approve kalau status masih
pendingatauprocessing. - Tampilkan
needs_reviewsebagai hasil yang berhasil diproses tetapi perlu validasi manusia. - Jangan render
raw_textsebagai konten utama. - Pada
failed, tampilkanerrordari response jika ada. - Pada confidence rendah, arahkan user ke review fields yang punya flag terkait.