SPMB Flow — Audit & Refactor Plan (Apr 2026)¶
Status: Audit complete + Opsi A executed (Apr 29, 2026). Wizard
dihapus, Detail UI menjadi canonical, dead code dipangkas, kontrak route
dilindungi unit test. Lihat §10 untuk ringkasan eksekusi.
Scope: pendaftar portal /spmb/* di repo scola-fe-v2. Admin SPMB
(/spmb-admin/*) di luar scope.
1. Latar¶
Fitur SPMB sudah melalui beberapa perombakan. User report (Apr 2026):
- Klik "Buka Upload Dokumen" di tab Detail menampilkan halaman "Sedang Dalam Pemeliharaan".
- Tab "Pembayaran & Hasil" perannya tidak jelas.
- Tombol "Submit Sekarang" + "Simpan Perubahan" muncul di semua tab.
- UI wizard pertama-kali berbeda dari UI detail.
Audit memetakan aset SPMB → identifikasi dead code & redundansi → proposal refactor terkonsolidasi.
2. Inventaris¶
2.1 Routes terdaftar (/Users/salfath/scola-fe-v2/src/router/spmbRoutes.js)¶
| Path | Component | Auth | Catatan |
|---|---|---|---|
/spmb |
Public/SPMBLanding.vue |
no | landing |
/spmb/status |
Public/SPMBStatusCheck.vue |
no | cek status by NISN |
/spmb/statistics |
Public/SPMBStatistics.vue |
no | statistik publik |
/spmb/verify-email |
Public/SPMBVerifyEmail.vue |
no | OTP |
/spmb/login |
SPMB/PendaftarLogin.vue |
no | login |
/spmb/register |
SPMB/PendaftarRegister.vue |
no | daftar akun |
/spmb/wizard |
SPMB/SPMBWizard.vue |
yes | wizard 5-step (1448 LOC) |
/spmb/admission/:id |
Public/SPMBAdmissionDetail.vue |
yes | detail post-create (989 LOC) |
/spmb/admission/:id/enrollment |
Public/SPMBDaftarUlang.vue |
yes | daftar ulang |
2.2 View files orphan (eksis tapi TIDAK terdaftar)¶
| File | Refs | Status |
|---|---|---|
Public/SPMBDocuments.vue (338 LOC) |
0* | orphan |
Public/SPMBAchievements.vue (482 LOC) |
0 | orphan |
Public/SPMBComplaint.vue (453 LOC) |
0 | orphan |
Public/SPMBTestSchedule.vue (292 LOC) |
0* | orphan |
Public/SPMBRegisterList.vue |
0 | orphan |
Public/SPMBCBTStart.vue |
1 | dipakai dari SPMBTestSchedule.vue (juga orphan) → de-facto orphan |
_archive/SPMBAdmissionCreate.vue |
0 | archive |
_archive/SPMBDashboard.vue |
0 | archive |
_archive/SPMBLogin.vue |
0 | archive |
_archive/SPMBRegister.vue |
0 | archive |
* = direferensikan hanya sebagai komentar // from SPMBDocuments.vue
di SPMBWizard.vue / spmb.store.js, bukan import nyata.
Akar bug user item #1: tombol goToDocuments, goToAchievements,
goToComplaints, goToTests di
Public/SPMBAdmissionDetail.vue:283-286 route ke
/spmb/admission/:id/{documents,achievements,complaints,tests} — semua
404, jatuh ke NotFoundView ("Sedang Dalam Pemeliharaan").
2.3 Komponen src/components/spmb/¶
| Komponen | Refs | Status |
|---|---|---|
SPMBNotificationBell.vue |
9 | dipakai luas |
SPMBStatusBanner.vue |
1 | hanya SPMBAdmissionDetail |
SPMBProgressTimeline.vue |
1 | hanya SPMBAdmissionDetail |
SPMBCountdownTimer.vue |
2 | Detail + DaftarUlang |
SPMBActivityTimeline.vue |
0 | dead |
2.4 Wizard internal (src/views/SPMB/)¶
SPMBWizard.vue (1448 LOC) berisi 5 step inline:
- Step 1: Data Pribadi (form lengkap, mirror Detail tab Profil)
- Step 2: Dokumen (upload base64; logic disalin dari SPMBDocuments.vue)
- Step 3: Pembayaran (pending/paid/verified UI)
- Step 4: Jadwal Tes (logic disalin dari SPMBTestSchedule.vue)
- Step 5: Hasil (pengumuman)
Di onMounted, kalau application.id ada → redirect ke
/spmb/admission/:id. Artinya wizard hanya aktif untuk pre-create
flow (sampai user klik "Buat Pendaftaran" di akhir step 1). Setelah
itu dia tak pernah dipakai user yang sama lagi.
Sub-komponen wizard (semua dipakai hanya oleh SPMBWizard.vue):
- SPMB/components/Stepper.vue
- SPMB/components/ChecklistSidebar.vue
- SPMB/components/SPMBHelpModal.vue
2.5 Service methods (src/services/spmb/spmb.service.js)¶
37 method total. Yang 0 referensi = dead:
getAdmissionActivitiesgetComplaintDetailgetRegistersPublicgetResultgetScholarshipTypesreapplyAdmission
Method dengan 1 referensi mostly dipakai langsung dari
SPMBAdmissionDetail / SPMBWizard (sehat).
2.6 Store (src/stores/spmb.store.js, 539 LOC)¶
Action duplikat / overlap dengan SPMBService:
- uploadDocumentBase64 (store) vs uploadDocument (service) — store
versi memanggil service-nya lewat aliased duplicate.
- getDocuments (store) wraps SPMBService.getDocuments tanpa nilai
tambah signifikan.
- Banyak action store (e.g. getPaymentStatus, createPayment,
confirmEnrollment, getResult) hanya dipakai di SPMBWizard.vue
→ kalau wizard dipangkas, action ini ikut dead.
calculateStepFromState mapping:
| state | step |
|--------------|------|
| draft | 1 |
| submit | 2 |
| confirm | 3 |
| application | 4 |
| admission | 4 |
| done | 5 |
(Sumber: @/Users/salfath/scola-fe-v2/src/constants/spmb.constants.js:44-51.)
2.7 State machine op.admission (BE → FE labels)¶
| state | label | Detail UI behavior |
|---|---|---|
draft |
Draft | form editable; Submit aktif |
submit |
Disubmit | read-only; Print Bukti tampil |
confirm |
Dikonfirmasi | read-only |
application |
Proses Seleksi (*) | read-only; tampilkan jadwal tes (*) |
admission |
Diterima | read-only |
done |
Selesai | read-only; tampilkan akun siswa+orang tua |
reject |
Ditolak | read-only |
pending |
Cadangan | read-only |
cancel |
Dibatalkan | read-only |
(*) state application ada di BE & constants, tidak ditangani
stateConfig di SPMBAdmissionDetail.vue:88-97 (gap UI: badge "Proses
Seleksi" jatuh ke fallback gray).
3. Issue findings¶
3.1 Bug & broken behavior (P0)¶
| ID | Lokasi | Gejala | Akar masalah |
|---|---|---|---|
| P0-1 | SPMBAdmissionDetail.vue:283-286 |
"Sedang Dalam Pemeliharaan" saat klik tab | 4 child route tidak terdaftar |
| P0-2 | SPMBAdmissionDetail.vue:88-97 |
Badge tidak rapi untuk state application |
Mapping kurang |
| P0-3 | SPMBAdmissionDetail.vue:281 |
Logout pakai router.push("/spmb/login") |
Tidak panggil authStore.logout → session BE tetap hidup |
3.2 UX issue (P1)¶
| ID | Lokasi | Issue |
|---|---|---|
| P1-1 | SPMBAdmissionDetail.vue:869-919 |
Tombol "Submit" + "Simpan Perubahan" muncul di semua tab (di luar v-show) |
| P1-2 | SPMBAdmissionDetail.vue:147 |
Tab "Pembayaran & Hasil" — labelnya menyesatkan (tidak ada UI bayar) |
| P1-3 | SPMBAdmissionDetail.vue:909-919 + 924-947 |
"Simpan" duplikat: tombol statis + floating bar |
| P1-4 | SPMBWizard.vue vs SPMBAdmissionDetail.vue |
Layout/UI berbeda jauh untuk task serupa |
| P1-5 | SPMBAdmissionDetail.vue tab Dokumen |
Hanya placeholder "Buka Upload Dokumen" — tidak inline; routing-based extra navigation |
3.3 Dead code (P2 — cleanup)¶
Files (10):
- src/views/AdmissionManagement/_archive/ — 4 file
- src/views/AdmissionManagement/Public/SPMBRegisterList.vue
- src/views/AdmissionManagement/Public/SPMBAchievements.vue (akan dihidupkan kembali jika opsi A; lihat §5)
- src/views/AdmissionManagement/Public/SPMBComplaint.vue (idem)
- src/views/AdmissionManagement/Public/SPMBDocuments.vue (idem)
- src/views/AdmissionManagement/Public/SPMBTestSchedule.vue (idem)
- src/components/spmb/SPMBActivityTimeline.vue
Service methods (6): lihat 2.5.
Store actions ikut dievaluasi setelah keputusan §5 diambil.
3.4 Redundansi¶
- Logic upload dokumen DUPLIKAT antara
SPMBWizard.vue:394+danSPMBDocuments.vue(orphan). Wizard menyalin dari Documents, tapi Documents tidak dihapus. - Logic test schedule DUPLIKAT antara
SPMBWizard.vue:526+danSPMBTestSchedule.vue(orphan). spmb.store.uploadDocumentBase64= aliasSPMBService.uploadDocument.- Detail page tab Profil + Wizard step 1 = form yang hampir sama dengan bidang tumpang tindih (different field shapes/order).
4. Mengapa wizard & detail terpisah?¶
Histori (dari komentar kode + commit log):
1. Awal: serangkaian halaman terpisah per langkah (SPMBDocuments,
SPMBAchievements, SPMBComplaint, SPMBTestSchedule).
2. Refactor wave 1: dilebur jadi SPMBWizard.vue 5-step SPA.
3. Refactor wave 2: tambahan SPMBAdmissionDetail.vue sebagai
"canonical post-create UX" — tab placeholder mereferensi halaman
lama yang tidak pernah dihapus, tapi juga tidak diregistrasi.
Hasil net: kita punya 2 UI yang sama-sama hidup (wizard + detail) + 4 halaman kerangka (Documents/Achievements/Complaints/TestSchedule) yang tidak bisa diakses dari mana pun.
5. Pilihan Refactor¶
Opsi A — "Detail-only" (recommended)¶
Detail page jadi UI tunggal kanonik untuk SELURUH siklus pendaftaran, termasuk pre-create. Hapus wizard.
Komponen:
1. Detail diperluas: jika :id = new (atau tanpa application) →
tampilkan form pre-create dengan tombol "Buat Pendaftaran".
2. Tab Dokumen, Achievements, Complaints, Tests → register sebagai
child route (/spmb/admission/:id/{documents,achievements,...})
memakai komponen SPMBDocuments.vue, dst. yang sudah ada (revive,
lengkapi/perbarui sesuai design system).
3. Hapus SPMB/SPMBWizard.vue + SPMB/components/Stepper.vue,
ChecklistSidebar.vue, SPMBHelpModal.vue.
4. Hapus action store yang khusus wizard.
Pros: UI konsisten, halaman lama hidup kembali, dead code bersih. Cons: kerja besar; Detail page butuh kemampuan pre-create (form awal).
Opsi B — "Wizard-first" (status quo+)¶
Wizard tetap tuan rumah pendaftaran; Detail hanya ringkasan post-submit.
Komponen:
1. Hapus 4 halaman orphan (SPMBDocuments.vue, dll.) sepenuhnya;
konten diakses lewat wizard step.
2. Detail tab "Dokumen" jadi read-only summary list (bukan placeholder
"Buka Upload Dokumen").
3. Detail tab "Pembayaran & Hasil" → split jadi tab "Status & Hasil"
(tetap), card pembayaran kalau ada invoice unpaid.
4. Sederhanakan tombol: "Simpan Perubahan" hanya di tab Profil; "Submit"
di tab Status (hanya state=draft).
Pros: scope kecil, low-risk.
Cons: 4 file SPMBxxx.vue (1565 LOC) harus dihapus; detail page jadi
ringan tapi bisa terasa "kurang interaktif" untuk fitur lanjut (achievement
banyak, complaint baru, dll. — harus tetap di wizard yang sudah submit?).
Opsi C — "Hybrid" (kompromi)¶
Wizard untuk pre-create flow (state=draft hingga submit), Detail untuk post-submit + interaksi lanjutan (Documents, Achievements, Complaints, Tests sebagai child routes).
Komponen:
1. Tetap pertahankan wizard; redirect-ke-detail tetap berlaku setelah
submit.
2. Register 4 child routes Detail.
3. Cleanup: hapus 4 file _archive, SPMBRegisterList.vue,
SPMBActivityTimeline.vue, 6 service method dead.
4. Cleanup tombol di Detail: pisah per tab.
Pros: minimal disruption; user yang sudah familiar dengan wizard tetap nyaman; orphan pages hidup kembali. Cons: tetap 2 UI, tetap ada duplikasi logic upload/test (wizard vs revived pages).
6. Rekomendasi¶
Eksekusi Opsi C dulu (immediate fix), dengan catatan migrasi ke Opsi A sebagai milestone berikutnya bila timeline mengizinkan.
Alasan: - Memperbaiki bug user-blocking (P0-1) tanpa risiko regresi besar. - Menghapus dead code yang clearly tidak dipakai (P2). - Memperbaiki UX tombol & tab (P1-1, P1-2, P1-3). - Membuka jalan ke konsolidasi UI tunggal di milestone berikutnya.
7. Action plan Opsi C¶
Phase 1 — Bug fixes & route registration¶
- Register 4 child route di
spmbRoutes.js:/spmb/admission/:id/{documents,achievements,complaints,tests}menunjuk ke 4 filePublic/SPMBxxx.vueyang sudah ada. - Tambah unit test
tests/menu/spmbDetailChildRoutesComposition.spec.jsmemastikan 4 path ini terdaftar. - Logout di Detail pakai
authStore.logout()(hapus stub manual).
Phase 2 — UI cleanup di Detail¶
- Pindahkan tombol "Simpan Perubahan" ke dalam tab Profil saja (atau tinggalkan hanya floating bar — lebih bersih).
- Pindahkan card "Submit Sekarang" ke tab "Status & Hasil" dan tampil
hanya saat
state=draft. - Rename label tab "Pembayaran & Hasil" → "Status & Hasil".
- Tambah mapping
applicationdistateConfig.
Phase 3 — Dead code removal¶
- Hapus
src/views/AdmissionManagement/_archive/(4 file). - Hapus
src/views/AdmissionManagement/Public/SPMBRegisterList.vue. - Hapus
src/components/spmb/SPMBActivityTimeline.vue. - Hapus 6 service method dead.
- Audit store: hapus action yang setelah Phase 1-2 jadi 0-ref.
Phase 4 — Tests¶
- Tambah unit test contract: tombol "Submit"/"Simpan" hanya muncul di
tab+state yang sesuai (
SPMBAdmissionDetail.spec.js). - Tambah tests/contract:
spmbServiceMethods.spec.jsaudit no-dead service method.
Phase 5 — Future (opsional, milestone berikutnya)¶
- Migrasi ke Opsi A: hapus wizard, Detail jadi UI tunggal pre+post create.
- Konsolidasi form Profil + step 1 wizard.
8. Risk register¶
- R1: 4 page yang akan di-register (Documents/Achievements/Complaints/
TestSchedule) terakhir disentuh sebelum refactor wave 2 — perlu QA
cepat tiap halaman setelah register. Test BE endpoint yang dipakai:
getDocuments,uploadDocument,deleteDocument,getAchievements,addAchievement,updateAchievement,deleteAchievement,getMyComplaints,createComplaint,getMyTestSchedules,getMyTestResults. Sudah ada di service, sebagian sudah dipakai wizard (jadi sehat). - R2: Penghapusan service method dead → hindari regresi via grep ulang sebelum delete + test contract.
- R3: Logout fix bisa berdampak ke session-related tests; jalankan unit suite full setelah perubahan.
9. Verifikasi pasca-eksekusi¶
npm run lint:diffnpm run check:capability-diffnpm run type-checknpx vitest runnpm run test:contract- E2E manual ringan: login pendaftar → klik tiap tab di Detail → klik setiap CTA → verifikasi tidak ada 404.
10. Ringkasan Eksekusi (Apr 29, 2026)¶
Opsi A (canonical Detail UI) di-execute. Highlight:
10.1 Penambahan¶
src/views/AdmissionManagement/Public/SPMBAdmissionCreate.vue— pre-create form lean (~360 LOC) menggantikan/spmb/wizard5-step.- 6 route baru di
spmbRoutes.js:/spmb/admission/new,/spmb/admission/:id(\\d+)/{documents, achievements,complaints,tests,enrollment}. tests/menu/spmbDetailChildRoutesComposition.spec.js— kontrak route registration + meta guard (3 tests).
10.2 Penghapusan dead code¶
- 9 file dihapus:
src/views/SPMB/SPMBWizard.vue(1448 LOC)src/views/SPMB/components/ChecklistSidebar.vuesrc/views/SPMB/components/SPMBHelpModal.vuesrc/views/AdmissionManagement/Public/SPMBRegisterList.vuesrc/components/spmb/SPMBActivityTimeline.vue- 4 file di
src/views/AdmissionManagement/_archive/(folder dihapus) src/stores/spmb.store.js: 539 LOC → 239 LOC. HapuscurrentStep/completedStepsstate,updateStep,setCurrentStep,calculateStepFromState,createApplication, dan 11 wrapper action yang dipakai hanya wizard. State sekarang fokus auth-derived + cacheapplication/currentPeriod.src/services/spmb/spmb.service.js: hapus 9 method dead (getAdmissionActivities,getComplaintDetail,getRegistersPublic,getResult,getScholarshipTypes,reapplyAdmission,confirmPayment,getSelectionResult,checkPaymentStatus).src/constants/spmb.constants.js: hapusSTATE_STEP_MAP.
10.3 Perubahan navigasi¶
Welcome.vue:51,roles.js:58,PendaftarLogin.vue:60,101→/spmb/wizarddiganti/spmb/admission/new.routerGuardPolicy.js: hapus/spmb/wizarddari array prefix; ganti jadi/spmb/admission.
10.4 UI cleanup di Detail (SPMBAdmissionDetail.vue)¶
stateConfig: tambah entry untuk stateapplication("Proses Seleksi", warna violet) — sebelumnya jatuh ke fallback gray.handleLogout: pakaiauthStore.logout()(BE/api/auth/logout+ bersihkan session/persist), bukan stubrouter.push("/spmb/login").- Tab #3 di-rename "Pembayaran & Hasil" → "Status & Hasil" dengan
id
status(sebelumnyapembayaran). - Card "Submit Sekarang" dipindah dari global action bar ke dalam
konten tab "Status & Hasil" — tampil hanya saat user di tab tsb.
DAN
state=draft. - Tombol statis "Simpan Perubahan" di bottom dihapus — floating bar
yang muncul saat
hasUnsavedChangesadalah satu-satunya UI Save (afford lebih jelas, tidak duplikatif). - Tab "Dokumen & Prestasi": placeholder lama 1-CTA diganti grid 2-card (Manajemen Dokumen + Prestasi) supaya keduanya equally accessible.
10.5 Verifikasi¶
npx vitest run tests/unit tests/menu→ 187 file tes lulus (904 passed, 6 skipped, 0 fail).tests/menu/spmbDetailChildRoutesComposition.spec.js(baru) lulus (3 tests).- Build/lint/type-check belum dieksekusi di sesi ini — perlu dijalankan
di pre-PR validasi sebelum merge ke
develop.
10.6 Reduksi LOC¶
| Aset | Sebelum | Sesudah | Δ |
|---|---|---|---|
src/views/SPMB/SPMBWizard.vue |
1448 | 0 (deleted) | −1448 |
| Wizard sub-components (2) | ~250 | 0 | −250 |
| Dead views (5 file) | ~1700 | 0 | −1700 |
_archive/* (4 file) |
~1500 | 0 | −1500 |
src/stores/spmb.store.js |
539 | 239 | −300 |
src/services/spmb/spmb.service.js |
271 | 231 | −40 |
Plus SPMBAdmissionCreate.vue (baru) |
0 | 360 | +360 |
| Net total | ≈ −4878 |
10.7 Follow-up (opsional)¶
- Konsolidasi field Profil dengan field pre-create (sekarang Create hanya minta minimum, sisanya diisi di Detail tab Profil — duplikasi pertanyaan rendah tapi ada).
- Pindah
src/views/SPMB/components/Stepper.vuekesrc/components/ui/karena sekarang hanya dipakai olehNewsWizard.vue(di luar scope SPMB). - Update
docs/qa/spmb-end-to-end-uat.mdsesuai canonical flow baru (hapus referensi step wizard).
11. Hotfix — Upload Dokumen 413 (Apr 29, 2026)¶
User report pasca-Opsi A: klik "Upload Dokumen Lainnya" memunculkan
POST /api/SPMB/.../document/upload 413 (Content Too Large) dari nginx,
TANPA pesan ke user (uncaught promise rejection di console).
11.1 Root cause¶
Di SPMBDocuments.vue:handleFileUpload lama, try/catch outer
membungkus reader.readAsDataURL(file) (sinkron) tapi panggilan
SPMBService.uploadDocument berjalan di dalam reader.onload = async
() => { await ... }. Rejection axios dari nginx 413 tidak pernah
terpropagasi ke catch outer → unhandled promise rejection. Selain itu
fallback batas ukuran 5MB tidak memperhitungkan inflasi base64 (~+33%)
+ envelope JSON-RPC, sehingga file dekat 5MB raw bisa melewati nginx
client_max_body_size 5MB.
11.2 Fix FE (committed)¶
SPMBDocuments.vue: promisifyFileReader(new Promise(...)) sehingga rejection upload masuk kecatchouter.- Mapping error → pesan friendly ID:
- 413 (status atau substring "413 Request/Content Too Large" di response HTML nginx) → "File terlalu besar untuk diunggah ke server. Coba kompres ke bawah 3MB lalu unggah ulang."
ERR_NETWORK→ "Koneksi terputus saat mengunggah."- default → message dari error.
- Tampilkan via
useNotification.error()+ tetap seterror.valueinline. - Konstanta
DEFAULT_MAX_FILE_SIZE_MB = 3(turun dari 5). Berlaku saatreqDoc.max_file_sizetidak tersedia (mis. tipe "other"). Validasi client-side menampilkan ukuran file aktual + saran kompres. - Tambah
notifySuccess("Dokumen berhasil diunggah")untuk feedback positif.
11.3 Catatan infra (di luar repo FE)¶
nginx client_max_body_size perlu dinaikkan bila batas 3MB raw terlalu
ketat untuk dokumen SPMB (mis. scan KK warna). Rekomendasi:
client_max_body_size 10m; di vhost dev.gcgscola.id lalu naikkan
DEFAULT_MAX_FILE_SIZE_MB ke 6–7. Biarkan ada margin minimal 30%
untuk inflasi base64 + envelope. Lokasi config: di repo scola-be /
infra ops, bukan FE.
11.4 Pola serupa di tempat lain¶
Audit menunjukkan banyak komponen lain (Inventory, HR, Counseling,
News, dst.) menggunakan pola reader.onload = async () => { await
serviceCall() } yang sama. Bila mereka pernah melewati nginx limit
atau backend error, error juga akan tertelan.
Status (Apr 30, 2026): FE-wide audit telah dilakukan. Dua file
Inventory yang masih bermasalah telah di-fix dengan pola promisify
FileReader yang sama seperti SPMBDocuments.vue:
- PurchaseRequestDetail.vue — onFileSelected (upload attachment
Purchase Request): reader.onload = async → promisify.
- ReceiptDetail.vue — onFileSelected (upload attachment Receipt):
reader.onload = async + broken reader.onerror (throw di callback)
→ promisify.
File lain yang menggunakan reader.onload = async sudah memiliki
try/catch internal (UploadSiswa, UploadGuru, bank-reconciliation.service). Tidak ada error
tertelan di file-file tersebut.
11.5 Extension hotfix → SPMBAchievements.vue (Apr 29, 2026, sore)¶
User report: 413 juga muncul saat "Tambah Prestasi" mengunggah
sertifikat. File SPMBAchievements.vue:handleSubmit sebelumnya cuma
error.value = err.message (= "Request failed with status code 413"
— tidak ramah, dan terkadang ketutupan modal). Pola fix yang sama
diterapkan:
- Validasi ekstensi (pdf,jpg,jpeg,png) + ukuran (≤3MB) di
handleFileUpload dengan showWarningDialog + reset input.
- Promisify FileReader agar reader.onerror terpropagasi.
- handleSubmit: deteksi 413 / ERR_NETWORK → pesan friendly via
useNotification.error() + tetap set error.value.
- notifySuccess saat add/edit berhasil (sebelumnya tidak ada
feedback positif).
11.6 Konsolidasi limit upload + hint UI (Apr 29, 2026, sore)¶
Konteks pertanyaan user:
1. client_max_body_size 5m itu per file atau total?
2. Bisa diatur di aplikasi, bukan nginx?
3. Tampilkan keterangan UI per field (best practice).
Jawaban:
1. Per HTTP request body. Karena upload SPMB = 1 file per POST,
efektif per file (sudah termasuk overhead base64 + envelope).
Bukan kuota cumulative.
2. Layered limit (best practice):
- nginx = hard cap infra; tidak bisa di-bypass aplikasi.
Saran: set tinggi (20m).
- BE Odoo = setting bisnis per-jenis-dokumen via field
op.admission.required.doc.max_file_size + allowed_extensions
(sudah ada, sudah dikonsumsi FE). Panitia SPMB bisa atur tanpa
redeploy FE.
- FE = fallback + validasi client-side + hint UI.
3. Implementasi (commit ini):
File baru src/config/spmbUploadLimits.js — single source of truth
client-side untuk default + helper:
- SPMB_DEFAULT_MAX_FILE_SIZE_MB = 3 (tinggal ubah di sini saja saat
nginx dinaikkan).
- SPMB_DEFAULT_ALLOWED_EXTENSIONS = ["pdf","jpg","jpeg","png"].
- resolveMaxFileSizeMb(reqDoc) — utamakan reqDoc.max_file_size
dari BE, fallback ke konstanta.
- resolveAllowedExtensions(reqDoc) — parse CSV BE atau fallback.
- formatFileSize(bytes) — "850 KB" / "1.20 MB".
- buildUploadHint(reqDoc) — string hint standar
"Format: PDF, JPG, PNG · Max 3MB".
Refactor:
- SPMBDocuments.vue: import dari config sentral. Hint UI tampil
per-required-doc (sumber BE) DAN di dropzone "Upload Dokumen
Lainnya" (fallback). Placeholder lama "max 5MB" yang stale dihapus.
- SPMBAchievements.vue: idem; hint tampil di dropzone sertifikat
modal Add/Edit Prestasi.
Test:
- tests/unit/config/spmbUploadLimits.spec.js (baru, 9 tests):
kunci kontrak default, parsing, fallback, dan format helper supaya
regresi tidak menaikkan default sembarangan.
Best practice yang diterapkan (UX upload): - Hint statis dekat input dropzone (pre-pilih). - Validasi client-side dengan ukuran file aktual + saran kompres. - Notifikasi toast (terlihat meski modal terbuka) + banner inline. - Source of truth dari BE → FE konsisten dengan kebijakan panitia. - Fallback konservatif yang aman terhadap nginx cap (lihat §11.7).
11.7 Perubahan nginx server dev.gcgscola.id (Apr 29, 2026)¶
Investigasi root cause infra: vhost /etc/nginx/sites-available/
dev.gcgscola.id ternyata tidak punya client_max_body_size sama
sekali — jadi pakai default nginx 1MB (bukan 5MB seperti
asumsi awal). Inilah akar 413 yang sebenarnya: file > ~750KB raw
sudah ditolak nginx sebelum sampai BE.
Perubahan diterapkan via SSH (server 89.116.32.154, scola@):
1. Backup: dev.gcgscola.id.bak.20260429-072717.
2. Sisipkan client_max_body_size 20M; di server block 443
(juga di block 80 yang redirect — harmless tapi konsisten).
3. sudo nginx -t valid → sudo systemctl reload nginx (graceful).
4. Verifikasi functional dengan POST payload 6.6MB ke
/api/SPMB/my/admission/document/upload → respons HTTP 200 +
JSON-RPC Odoo Session Expired (auth-level, bukan 413). Pre-fix
payload ini pasti ditolak nginx 413.
Dampak:
- nginx cap dev: 1MB → 20MB (margin 14MB raw setelah base64 inflation).
- FE SPMB_DEFAULT_MAX_FILE_SIZE_MB dinaikkan dari 3 → 5MB
(cukup untuk dokumen realistis, tetap aman vs nginx 20MB).
- Test tests/unit/config/spmbUploadLimits.spec.js masih lulus
(assertion <= 5MB).
Vhost lain yang TIDAK diubah (di luar scope task ini, tapi
worth diperiksa ops apakah punya masalah serupa):
- app.gcgscola.id (FE produksi utama)
- frontend.gcgscola.id
- demo.gcgscola.id, jamesschool.*, montessori.*, sdn1pgt.*,
smpn1pmk.*, spansa.*, smpn1dramagabogor.*
- sadarapp.gcgscola.id
Vhost backend be-*.gcgscola.id sudah 200M semua — tidak terdampak.
Rekomendasi follow-up untuk ops:
1. Standardisasi: tambahkan client_max_body_size 20M; di semua
vhost FE produksi (cegah 413 senyap di tenant lain).
2. Pertimbangkan masuk ke template global nginx.conf di http{}
block sebagai default 20M, override per vhost bila perlu.
3. Field op.admission.required.doc.max_file_size di BE Odoo
harus disurfacing di form admin SPMB supaya panitia bisa atur
per-jenis-dokumen tanpa redeploy.
13. SPMB Admin Test-* endpoints HTTP 400 (Apr 29, 2026 — fixed)¶
Gejala¶
Halaman /admin/spmb/test/schedule, /admin/spmb/test/types,
/admin/spmb/test/participants, /admin/spmb/test/results selalu gagal
load dengan HTTP 400 pada POST ke endpoint terkait. Log Odoo BE
menunjukkan:
WARNING odoo.http: No CSRF validation token provided for path
'/api/SPMB/test-schedules/list'
INFO werkzeug: "POST /api/SPMB/test-schedules/list HTTP/1.0" 400
Akar¶
Saat module load, Odoo melempar warning:
WARNING odoo.http: The endpoint
SPMBAssessmentSchedulesController.test_schedules_list changes the route
type, using the original type: 'http'.
Mekanismenya (/home/scola/odoo/odoo/http.py:864
_check_and_complete_route_definition): saat menjalin merged_routing
melintasi MRO, parent method yang tidak ber-decorator akan
auto-decorate dengan default @route() → type='http'. Ketika child
overlay (scola_admission_assessment) menulis
@http.route(..., type='json', ...), Odoo menolak perubahan tipe dan
memaksa tipe akhir kembali ke 'http'.
Konsekuensi:
- Dispatcher yang dipakai = HttpDispatcher, bukan JsonRPCDispatcher.
- HttpDispatcher.dispatch menjalankan CSRF check untuk metode unsafe.
- POST dari FE tanpa csrf_token → BadRequest (HTTP 400).
Pola yang sama dilaporkan sebelumnya untuk /api/SPMB/my/test-schedules
(diselesaikan dengan memindahkan @http.route ke base — lihat catatan
di scola_admission/controllers/spmb_api.py:1701). Tapi route admin
test-* belum dirapikan.
Fix¶
Menambahkan stub @http.route(type='json', auth='user') (tanpa
URL) pada method base di:
- scola_admission/controllers/spmb_complaints_ranking_api.py — 9
method (test_schedules, test_schedules_list,
test_schedule_options, test_schedule_detail,
test_schedule_detail_read, test_schedule_create,
test_schedule_update, test_schedule_delete,
test_schedule_sync_cbt).
- scola_admission/controllers/spmb_applicants_api.py — 11 method
(test_types_*, test_participants_*, test_results_*).
Stub tidak mendaftarkan URL (routes kosong), hanya menetapkan
type='json' di original_routing sehingga MRO merge meneruskan
type ke child overlay alih-alih default 'http'. Topologi
overlay-only tetap terjaga: ketika scola_admission_assessment tidak
ter-install, base alone tidak meng-expose URL (Odoo log "is a controller
endpoint without any route, skipping").
Verifikasi¶
curl -X POST https://be-dev.gcgscola.id/api/SPMB/test-schedules/list \
-H 'Content-Type: application/json' \
-d '{"jsonrpc":"2.0","method":"call","params":{}}'
Sebelum fix: HTTP 400 + <!doctype html>... (HttpDispatcher CSRF
reject) atau redirect 303 ke /web/login.
Sesudah fix: HTTP 200 + {"jsonrpc":"2.0","error":{...
"Session expired" ...}} (JsonRPCDispatcher menangani, gagal di auth
karena curl tanpa cookie — perilaku benar).
Restart odoo-devscola (Apr 29 10:30:50 UTC) menghilangkan warning
"changes the route type" untuk semua method test-*.