diff --git a/CHANGELOG.md b/CHANGELOG.md
index bc0effd..5b4aaba 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -47,6 +47,7 @@ _None yet._
- **Per-student detail page (ENH-021, #66):** new `GET /mahasiswa/detail/(:any)` → `Mahasiswa::detail()` renders a single student's full biodata record (all columns from `GetBiodataMahasiswa`) read-only, with a "Detail" button added to each row in the Mahasiswa list. Reuses the `fetchBiodata($id)` single-record fetch shared with `edit()`.
- **Pagination & UI/UX refinements on menu pages (ENH-022, #68):** the three NeoFeeder menu pages (Mahasiswa, Aktivitas Kuliah, Mahasiswa Lulus/DO) gained a page-size selector (10/20/50/100), a "Halaman X dari Y" indicator with total count (via `GetCount*` endpoints), First/Prev/Next/Last links, and accessible `
diff --git a/app/Views/verifikasi_ipk/index.php b/app/Views/verifikasi_ipk/index.php
new file mode 100644
index 0000000..cc2e0ba
--- /dev/null
+++ b/app/Views/verifikasi_ipk/index.php
@@ -0,0 +1,47 @@
+
+
+
+
+
Verifikasi IPK
+
+
+
+
+
+
+
+
+
= esc($error) ?>
+
+
+
+
+ Terdapat sesi verifikasi IPK yang belum selesai.
+ Lanjutkan
+
+
+
+
+
+
Unggah Excel
+
+
+
+
+
+
diff --git a/app/Views/verifikasi_ipk/results.php b/app/Views/verifikasi_ipk/results.php
new file mode 100644
index 0000000..6dc60a3
--- /dev/null
+++ b/app/Views/verifikasi_ipk/results.php
@@ -0,0 +1,44 @@
+
+
+
+
+
Verifikasi IPK — Hasil
+
+
+
+
+
+
+
+
+ Verifikasi selesai. Berikut ringkasan pengiriman ke Neo Feeder.
+
diff --git a/app/Views/verifikasi_ipk/verification.php b/app/Views/verifikasi_ipk/verification.php
new file mode 100644
index 0000000..29a8757
--- /dev/null
+++ b/app/Views/verifikasi_ipk/verification.php
@@ -0,0 +1,100 @@
+
+
+
+
+
Verifikasi IPK
+
+
+
+
+
+
+
+
+
+
Hasil batch sebelumnya
+
+
+
NIM
Status
Keterangan
+
+
+
+
= esc($r['nim']) ?>
+
= $r['success'] ? 'Berhasil' : 'Gagal' ?>
+
= esc($r['msg']) ?>
+
+
+
+
+
+
+
+
+
+
+
diff --git a/docs/IMPROVEMENTS.md b/docs/IMPROVEMENTS.md
index b16be1e..23f938b 100644
--- a/docs/IMPROVEMENTS.md
+++ b/docs/IMPROVEMENTS.md
@@ -207,3 +207,14 @@ The label is encoded directly in the ID prefix. When a valid item becomes a GitH
- **Actual Implemented:** Added a `nilai_skripsi` column (letter grade) to the Excel template — `EXCEL_HEADERS`, `downloadTemplate()` (header + example `A`), and `parseExcel()` (stored uppercased on each student record). Added `NeoFeeder::updateNilaiPerkuliahanKelas($token, $idRegistrasi, $idKelas, $record)` using `sendMutation('UpdateNilaiPerkuliahanKelas', ...)` with composite key `id_registrasi_mahasiswa` + `id_kelas_kuliah` (key schema verified live via GetDictionary: key `id_registrasi_mahasiswa` + `id_kelas_kuliah`, record `nilai_huruf`/`nilai_angka`/`nilai_indeks`). In `Graduation::finish()`, for each saved student with a non-empty `nilai_skripsi`, the thesis/skripsi course is re-resolved via `getCekTranskripMahasiswa`; a new private helper `findMissingGradeThesis()` finds the thesis row whose grade is currently missing (`hasGrade == false` — `nilai_huruf`/`nilai_angka` empty) and returns its `id_reg_pd`/`id_kls` from the cloud row (both verified live on real data), then `updateNilaiPerkuliahanKelas` is called with only `record.nilai_huruf` = the Excel value. The push happens only when the grade is still missing; noise results are reported in the finish guidance. In `wizard.php` card 3, a thesis row without a grade now shows the Excel value with a "(akan diisi)" tag instead of a bare "Belum ada nilai" when a value was provided.
- **Changes:** The Excel template now carries an optional `nilai_skripsi` letter-grade column. A registered-but-ungraded thesis/skripsi course is filled in with the Excel letter grade at submission (`finish()`), only when it currently has no grade; students without a value in that column are left untouched. Card 3 indicates which thesis rows will be filled in.
- **Changes:** The Excel template now carries an optional `nilai_skripsi` letter-grade column. A registered-but-ungraded thesis/skripsi course is filled in with the Excel letter grade at submission (`finish()`), only when it currently has no grade; students without a value in that column are left untouched. Card 3 indicates which thesis rows will be filled in.
+
+### ENH-011 — Verify and fix last-semester IPK in bulk via Excel
+- **Status:** `implemented`
+- **Issue:** #115
+- **Recorded:** 2026-08-31
+- **Implemented:** 2026-08-31
+- **Problem:** Several (possibly all) students have a last-semester `ipk` that is not up to date in Neo Feeder. The graduation wizard only surfaces the last-semester IPK for the students being processed one at a time; there is no dedicated screen to bulk-verify and correct the last-semester IPK across many students against an external source.
+- **Possible Fix:** Add a dedicated read-only "Verifikasi IPK" menu that takes an Excel file (Graduation template: `nim` + `ipk` columns), reads each student's last-semester `ipk` from Neo Feeder (`getAktivitasKuliahMahasiswa`, largest `id_semester`), shows a comparison (Neo Feeder IPK vs Excel IPK, cocok/beda) batch-by-batch, lets the admin tick the rows to fix, and pushes `updatePerkuliahanMahasiswa(['ipk' => ...])` only to the **active** last semester (`a_periode_aktif=1`, ENH-007 rule). State stored in a cache + resume cookie separate from the graduation `WizardProgress` so the two do not collide. Processing is batch-by-batch (one `getAktivitasKuliahMahasiswa` call per student) to avoid the 60 s PHP time limit.
+- **Actual Fix:** Added `VerifikasiIpk` controller (`/verifikasi-ipk`, batch-by-batch comparison) + `VerifikasiIpkStore` library (cache prefix `ipk_verif_`, resume cookie `ipk_verif_resume`), routes, sidebar menu, and views (`index`/`verification`/`results`). The push uses a full `UpdatePerkuliahanMahasiswa` record — the whole active last-semester row from `getAktivitasKuliahMahasiswa` (minus the key) plus `id_pembiayaan` resolved via `GetListRiwayatPendidikanMahasiswa` and the `ipk` overridden with the Excel value — because Neo Feeder rejects partial records (`id_status_mahasiswa`, `biaya_kuliah_smt`, `id_pembiayaan` all demanded even though the GetDictionary schema lists only `id_status_mahasiswa` as NOT NULL). Per-row results (success/failure with the Neo Feeder error message) are shown on the `results` page. `apply()` uses `set_time_limit(0)` for large batches.
+- **Actual Implemented:** 2026-08-31
+- **Changes:** New `app/Controllers/VerifikasiIpk.php`, `app/Libraries/VerifikasiIpkStore.php`, `app/Views/verifikasi_ipk/*`; `NeoFeeder.php` gained `getListRiwayatPendidikanMahasiswa()`; routes + sidebar menu added.