# 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: ```text http://localhost:8000/api/v1 ``` Untuk frontend, simpan URL di environment variable: ```env VITE_OCR_API_BASE_URL=http://localhost:8000/api/v1 ``` Jika `API_KEYS` di backend diisi, semua endpoint protected membutuhkan header: ```http X-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 ```http GET /health GET /health/ready ``` Contoh response `/health`: ```json { "status": "ok", "version": "0.1.0" } ``` Contoh response `/health/ready`: ```json { "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: ```http 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: 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 ```ts 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: ```http GET /documents/{job_id} ``` ```ts 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 ```ts 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 ```ts 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 ```ts 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 ```json { "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: ```http PATCH /documents/{job_id} ``` Body: ```json { "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: ```http X-User-Id: reviewer-a ``` Path yang umum dipakai: ```text 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 ```ts async function patchDocument(jobId: string, corrections: FieldCorrection[], userId?: string) { const headers: Record = { "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: ```http GET /documents/{job_id}/history ``` Response: ```ts 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: ```http POST /documents/{job_id}/approve ``` Header opsional: ```http X-User-Id: reviewer-a ``` Response: ```json { "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: ```json { "error": "UnsupportedDocumentError", "message": "Uploaded file is empty." } ``` FastAPI validation errors memakai shape standar: ```json { "detail": [ { "type": "missing", "loc": ["body", "file"], "msg": "Field required" } ] } ``` Helper error: ```ts 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. ```http 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 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.