# Log Perubahan — Penambahbaikan SPMS (berdasarkan Laporan Semakan Sistem)

Fail ini meringkaskan SEMUA perubahan kod yang dibuat berdasarkan
`SPMS_Semakan_Sistem_Cadangan_Penambahbaikan.docx`. Ia memfokus item
**Fasa 1** (kritikal & mudah) daripada pelan tindakan dalam laporan
tersebut — risiko integriti data dan keselamatan yang paling ketara.

## 1. Padam pertindanan logik fallback kewangan (Penemuan 2.1 — Keutamaan Tinggi)

**Punca masalah:** peraturan "kalau tiada data YTD terus, guna angka bulan
terakhir suku tahun" ditulis semula secara manual di 5 fail berasingan,
dengan komen amaran ⚠️ yang mengesahkan pembangun sendiri sedar akan
risiko ini.

**Perubahan:**
- `includes/helpers.php` — tambah 2 fungsi kongsi baharu:
  - `quarter_end_month(string $quarter): int` — SATU sumber kebenaran
    untuk peraturan Mac=Q1/Jun=Q2/Sept=Q3/Dis=Q4, digunakan semula oleh
    `get_financial_with_monthly_fallback()` dan semua fail di bawah.
  - `get_financial_line_items_with_monthly_fallback()` — versi "baris
    penuh" (label/category/display_order) untuk keperluan eksport.
- `public/dashboard.php` — cawangan suku-tahun kini panggil
  `get_financial_with_monthly_fallback()` terus (34 baris kod
  disalin-tampal dibuang).
- `public/export-excel.php`, `public/export-print.php` — kini panggil
  `get_financial_line_items_with_monthly_fallback()` (kod pertindanan
  dibuang di kedua-dua fail).
- `public/group-targets.php`, `public/subsidiary.php` — bentuk query di
  fail ini BERBEZA (line_item_id spesifik / guna semula data yang
  sudah ditarik untuk elak query tambahan), jadi tak diganti terus
  dengan fungsi kongsi supaya prestasi/tingkah laku sedia ada tak
  terjejas — sebaliknya map `['Q1'=>3,...]` yang dihardcode digantikan
  dengan panggilan `quarter_end_month()`, menghapuskan bahagian paling
  berisiko (peraturan tercicir semasa kemas kini akan datang).

**Kesan:** perubahan pada peraturan fallback pada masa hadapan kini
hanya perlu dibuat di SATU tempat (`includes/helpers.php`) untuk
dashboard, export Excel, dan export Print — mengurangkan risiko angka
tidak konsisten antara paparan.

## 2. Kunci alat `hash-tool.php` (Penemuan 2.2 — Keutamaan Tinggi)

**Punca masalah:** alat penjana hash kata laluan tidak memerlukan log
masuk langsung — sesiapa yang menemui URL boleh menggunakannya.

**Perubahan (`public/admin/hash-tool.php`):**
- Berkunci automatik 24 jam selepas kunjungan pertama (tempoh
  "tenang" untuk migrasi/pemasangan awal sahaja).
- Selepas berkunci, hanya `super_admin` yang log masuk boleh akses.
- Laluan kecemasan (`?recovery_key=<APP_SECRET_KEY>`) disediakan untuk
  situasi semua akaun super_admin terkunci — guna nilai
  `APP_SECRET_KEY` daripada `config/database.php`.
- **Disyorkan masih:** padam fail ini sepenuhnya apabila sistem sudah
  stabil di production — ini kekal cara paling selamat.

## 3. Audit Trail: penapis, paging, eksport & paparan diff (Penemuan 2.6 — Keutamaan Tinggi)

**Punca masalah:** halaman hanya papar 200 rekod terkini tanpa penapis,
tanpa paging, tanpa eksport, dan data `old_value`/`new_value` yang
disimpan tidak pernah dipaparkan.

**Perubahan (`public/audit-trail.php` — ditulis semula sepenuhnya):**
- Penapis: julat tarikh, modul/jadual, jenis tindakan (INSERT/UPDATE/
  DELETE), pengguna.
- Paging sebenar (50 rekod/halaman + jumlah keseluruhan rekod).
- Butang **Eksport CSV** (guna penapis semasa, had 5,000 baris).
- Setiap rekod dengan perubahan boleh diklik **"Lihat butiran"** untuk
  papar jadual diff Medan / Nilai Lama / Nilai Baharu.
- `lang/ms.php` & `lang/en.php` — tambah ~19 kunci terjemahan baharu
  (`audit.filter_*`, `audit.btn_*`, `audit.field`, dll.) untuk UI baharu
  ini, dwibahasa penuh seperti seluruh sistem.
- `sql/migration_add_audit_trail_indexes.sql` (fail baharu) — indeks
  pada `changed_at`, `table_name`, `action`, `changed_by` supaya
  penapis baharu ini cekap walaupun `audit_logs` membesar (jadual ini
  diisi automatik oleh trigger pada SETIAP perubahan data).

## Tindakan Susulan Diperlukan Selepas Muat Naik

1. **Jalankan migrasi SQL baharu** di phpMyAdmin:
   `sql/migration_add_audit_trail_indexes.sql`
   (selamat dijalankan — hanya tambah indeks, tak ubah data).
2. Kalau `sql/migration_add_performance_indexes.sql` (daripada laporan
   asal) belum pernah dijalankan pada database production anda,
   jalankan juga — ia belum dimasukkan semula di sini kerana ia sedia
   ada dalam pakej asal, cuma sahkan ia SUDAH dijalankan.
3. Uji semula halaman Dashboard, Subsidiary, Export Excel/Print, dan
   Group Targets pada beberapa kombinasi tahun/suku tahun untuk
   sahkan angka kekal SAMA seperti sebelum kemas kini (refactor ini
   tidak menukar peraturan perniagaan, hanya memusatkannya).
4. Semak `public/admin/hash-tool.php` — kalau anda sudah tidak
   memerlukannya, PADAM terus fail ini daripada production.

## Item Fasa 2–4 (Belum Dilaksanakan Dalam Kemas Kini Ini)

Baki cadangan dalam laporan (caching agregat Group, pemisahan
logik/paparan dashboard.php, ujian automatik, command-bar navigasi,
mod gelap, dll.) memerlukan perbincangan reka bentuk/infrastruktur
lanjut (cth. pilih Redis vs APCu, keutamaan UX) dan dicadangkan
dilaksanakan mengikut fasa dalam pelan tindakan asal.

---

# FASA 2 — Prestasi & Kebolehselenggaraan (kemas kini susulan)

## 4. Caching untuk agregat Dashboard (Penemuan 3.1/3.2 — Keutamaan Tinggi)

**Perubahan:**
- `includes/helpers.php` — 3 fungsi kongsi baharu:
  - `cache_get()` / `cache_set()` — guna APCu automatik jika tersedia,
    jatuh balik ke cache berasaskan fail (`config/cache/*.cache`) untuk
    hosting cPanel perkongsian biasa yang TIADA extension APCu.
  - `cache_flush_prefix()` — kosongkan semua cache (dipanggil selepas
    tulis data).
- `public/dashboard.php` (kini dalam `includes/dashboard_data.php` —
  lihat item 6): data kewangan mentah (`$raw`), keputusan Group
  Adjustment, dan trend FY 5-tahun kini dicache:
  - Data kewangan mentah & Group Adjustment: TTL **3 minit**.
  - Trend FY (tahun yang sudah ditutup): TTL **1 jam** — jarang berubah.
- Cache **automatik dikosongkan** selepas tulis data berjaya di:
  `public/upload-monthly-actuals.php` (import & rollback),
  `public/upload.php`, `public/group-targets-upload.php`,
  `public/group-targets.php` (edit sasaran individu).
- `public/admin/system-diagnostics.php` — panel baharu "Cache
  Dashboard" dengan bilangan fail cache aktif + butang kosongkan
  manual (untuk situasi kecemasan, cth. suntingan terus di phpMyAdmin
  yang tak melalui laluan upload biasa).

**Nota reka bentuk:** cache dikunci kepada kombinasi tahun/tempoh/
penapis yang SEBENAR diminta (bukan cache "sejagat" merentasi semua
penapis) — jadi ia sentiasa BETUL, dan manfaat prestasinya datang
daripada permintaan SAMA yang berulang (cth. ramai pengguna buka
Dashboard tanpa penapis pada masa yang hampir sama). Ini pendekatan
selamat & ringkas berbanding invalidasi cache berkeping (per-key) yang
lebih kompleks dan lebih berisiko tersasar.

## 5. Chart.js dihoskan tempatan (Penemuan 2.5/3.3 — Keutamaan Rendah)

**Perubahan:**
- `public/assets/js/chart.umd.min.js` (fail baharu, ~200KB, chart.js
  v4.4.4 — versi SAMA seperti sebelum ini, cuma sumber berbeza) —
  dibina & dikecilkan daripada pakej rasmi npm chart.js, BUKAN daripada
  CDN (untuk elak pergantungan luar tanpa kawalan versi/integrity).
- `public/dashboard.php`, `public/report-division.php`,
  `public/subsidiary-dashboard.php` — kini rujuk fail tempatan
  (`/assets/js/chart.umd.min.js`) dengan cache-busting `filemtime()`,
  bukan `cdn.jsdelivr.net`.

**Kesan:** carta tidak lagi bergantung pada ketersediaan/dasar rangkaian
CDN luar (relevan untuk rangkaian kerajaan yang mungkin menyekat CDN
awam); versi Chart.js kini dikawal sepenuhnya oleh anda (tiada
auto-update tanpa disedari daripada CDN).

## 6. Carian & Paging untuk admin/users.php (Penemuan 3.5 — Keutamaan Sederhana)

**Perubahan (`public/admin/users.php`):**
- Kotak carian (nama/username/e-mel) + penapis peranan.
- Paging sebenar (50 pengguna/halaman) dengan kawalan Sebelum/Seterusnya.
- **Nota keselamatan reka bentuk:** carian/penapis/paging beroperasi ke
  atas senarai yang diambil daripada DB (bukan query SQL berasingan),
  tetapi pautan **Edit**/**Set Password** sedia ada (cth. daripada
  bookmark lama) tetap dicari dalam senarai PENUH (bukan hasil carian/
  halaman semasa) — jadi tiada pengguna "hilang" akibat carian aktif.

## 7. Pemisahan Logik/Paparan `dashboard.php` (Penemuan 2.3 — Keutamaan Sederhana)

**Punca masalah:** `dashboard.php` asal (719–725 baris) mencampurkan
pengendalian request, query pangkalan data, pengiraan perniagaan, DAN
HTML dalam satu fail — sukar diuji/disemak, risiko regresi tinggi.

**Perubahan:**
- `includes/dashboard_data.php` (fail baharu, ~230 baris) — SEMUA query
  pangkalan data, caching, dan pengiraan perniagaan (status pemantauan,
  jumlah Group, trend FY, ranking PAT, konfigurasi widget) kini di sini.
- `public/dashboard.php` kini **505 baris** (turun daripada 725) —
  hanya kekal: bootstrap/auth, pengendali aksi AJAX "save_commentary",
  SATU baris `require includes/dashboard_data.php`, dan HTML/paparan.
- Fail baharu ini **direquire terus dalam skop pembolehubah**
  `dashboard.php` (bukan corak `return array()` + `extract()`) — supaya
  SEMUA pembolehubah sedia ada terus untuk HTML tanpa langkah tambahan,
  sambil kekal terpisah secara fizikal untuk kebolehbacaan. Tiada
  perubahan tingkah laku/peraturan perniagaan — semata-mata organisasi
  kod semula.

## Pengesahan (bukan sekadar `php -l`)

Kemas kini Fasa 2 ini diuji dengan **menjalankan sistem sebenar**
(MariaDB + PHP built-in server tempatan, data ujian 38 subsidiari +
rekod kewangan), bukan sekadar semakan sintaks:
- Log masuk penuh (termasuk CAPTCHA matematik) — berjaya.
- `dashboard.php` (paparan suku tahun, bulan, dengan penapis sektor,
  DAN dipanggil dua kali berturut-turut untuk sahkan laluan cache-hit
  tiada ralat) — semua **HTTP 200, sifar ralat PHP**.
- `audit-trail.php`, `admin/users.php` (dengan/tanpa carian),
  `group-targets.php`, `admin/system-diagnostics.php`,
  `subsidiary.php`, `export-excel.php`, `export-print.php` — semua
  **HTTP 200, sifar ralat PHP**.

## Penemuan Tambahan (bukan disebabkan kemas kini ini, untuk makluman)

Semasa membina persekitaran ujian, didapati `sql/import_32_subsidiaries_mbi.sql`
turut mengandungi `ALTER TABLE ADD COLUMN sector` — perubahan skema
yang tersembunyi dalam fail import DATA, bukan dalam fail
`migration_*.sql` yang khusus untuk perubahan skema. Kalau pemasangan
production sebenar hanya menjalankan fail bermula `migration_` (ikut
susunan abjad/tarikh), lajur `sector` mungkin tercicir melainkan fail
import ini turut dijalankan. Disyorkan asingkan `ALTER TABLE` daripada
fail import data pada masa hadapan, walaupun ini di luar skop kemas
kini semasa.

## Item Fasa 2 Belum Dilaksanakan

- ETL berjadual untuk `group_adjustments` — memerlukan keputusan
  reka bentuk (cth. adakah adjustment perlu dikira automatik daripada
  sumber lain, atau kekal input manual seperti sekarang) sebelum kod
  ditulis; dicadangkan dibincang berasingan.

## Tindakan Susulan (Fasa 2)

1. Muat naik fail `public/assets/js/chart.umd.min.js` bersama baki
   sistem — fail ini WAJIB ada untuk carta berfungsi (menggantikan CDN).
2. Pastikan folder `config/cache/` boleh ditulis oleh PHP (sama seperti
   `config/sessions/` sedia ada) — akan dicipta automatik pada
   kunjungan pertama jika hosting membenarkan.
3. Tiada migrasi SQL tambahan diperlukan untuk Fasa 2 ini.

---

# FASA 2 (SUSULAN) — Cache Warming Berjadual

## 8. `cron/warm-dashboard-cache.php` (melengkapkan Penemuan 3.2)

**Nota reka bentuk penting:** cadangan asal laporan menyebut "ETL
berjadual untuk `group_adjustments`" — selepas semakan lanjut,
`group_adjustments` didapati TIDAK mempunyai sumber data luaran/API
untuk di-ETL; ia sentiasa dimasukkan secara manual melalui Upload
Monthly Actuals (lihat `sql/migration_add_group_adjustments.sql`).
Jadi "ETL berjadual" seperti yang dicadangkan asal tidak berkenaan.

Apa yang dilaksanakan sebaliknya (memberi kesan sebenar kepada
matlamat asal — Dashboard yang pantas): skrip cron yang **memanaskan
cache Dashboard** (dibina di Fasa 2 — lihat item 4) SEBELUM waktu
pejabat bermula, supaya pengguna PERTAMA setiap pagi tidak terkena
cache-miss (query penuh).

**Fail baharu:**
- `includes/helpers.php` — 3 fungsi kongsi baharu
  (`dashboard_raw_cache_key()`, `dashboard_groupadj_cache_key()`,
  `dashboard_fytrend_cache_key()`) — SATU sumber kebenaran untuk
  format kunci cache, dikongsi antara `includes/dashboard_data.php`
  DAN skrip cron ini (elak isu "kunci cache tak sepadan" yang buat
  cache warming tak memberi kesan).
- `cron/warm-dashboard-cache.php` — panaskan suku tahun semasa +
  suku tahun lepas (paparan lalai tanpa penapis) + trend FY.
  Arahan pemasangan cPanel Cron Job disertakan dalam komen fail.

**Diuji langsung:** dijalankan via command line terhadap DB ujian —
5 entri cache berjaya dikira & disimpan; `dashboard.php` seterusnya
berjaya baca daripada cache yang dipanaskan tanpa ralat.

---

# FASA 3 — UX & Self-Service

## 9. Mod Gelap (Penemuan 4.4)

**Pendekatan teknikal:** `spms.min.css` sudah PRA-KOMPIL (pakej ini
sengaja tiada keperluan Composer/npm untuk dijalankan — lihat
`install.php`), dan tiada config/sumber Tailwind disertakan untuk bina
semula dengan varian `dark:`. Jadi mod gelap dilaksanakan dengan
teknik **"class remapping"**: `public/assets/css/dark-mode.css` (fail
baharu) menimpa (override) class utiliti SEDIA ADA yang paling kerap
digunakan merentasi sistem (`border-slate-300` 223×, `bg-white` 169×,
`text-slate-500` 127×, dll. — disemak kekerapan sebenar dalam kod)
apabila `<html>` ada class `.dark-mode`.

**Perubahan:**
- `public/assets/css/dark-mode.css` (fail baharu).
- `includes/layout_header.php` — pautan CSS baharu, skrip pra-render
  (elak "kelip" putih, corak SAMA seperti sidebar-collapsed sedia ada),
  butang togol di sidebar (bawah suis bahasa), simpan pilihan dalam
  `localStorage['spms_dark_mode']` — keutamaan PERIBADI peranti,
  bukan tetapan global/admin.
- `lang/ms.php`, `lang/en.php` — kunci `nav.dark_mode`.

## 10. Command Bar — Carian Global Ctrl+K/Cmd+K (Penemuan 4.2)

**Perubahan:**
- `includes/layout_header.php` — senarai 27 halaman (ditapis mengikut
  `ROUTE_ACCESS` sebenar dalam `includes/rbac.php`, SAMA peraturan yang
  sekat akses sebenar — command bar TIDAK mendedahkan pautan yang
  pengguna tiada akses) dibenamkan sebagai JSON dalam halaman.
- `public/api/nav-search.php` (fail baharu) — endpoint JSON
  disahkan-sesi (session-authenticated, BUKAN API key seperti
  `api/powerbi-*.php` yang untuk integrasi luaran) memulangkan senarai
  38 anak syarikat untuk carian "lompat terus".
- Modal carian dengan navigasi papan kekunci penuh (↑↓ Enter Esc),
  dibuka via butang di header ATAU pintasan `Ctrl+K`/`Cmd+K`.
- `lang/ms.php`, `lang/en.php` — 6 kunci terjemahan baharu
  (`nav.command_bar_*`).

## 11. Eksport Carta ke PNG (Penemuan 4.5, sebahagian)

**Perubahan (`includes/layout_footer.php`):**
- Skrip generik yang mengesan SEMUA elemen `<canvas>` dengan instance
  Chart.js aktif (`Chart.getChart()`) pada mana-mana halaman, dan
  menambah butang "⬇" kecil di penjuru setiap carta untuk muat turun
  PNG (`chart.toBase64Image()`).
- Dilaksanakan di `layout_footer.php` (bukan setiap fail carta
  berasingan) sebab fail ini di-require PALING AKHIR — selepas skrip
  inisialisasi carta setiap halaman siap jalan. Kesan: SEMUA carta
  sedia ada (Dashboard, Subsidiary Dashboard, Division Analysis) DAN
  carta baharu akan datang automatik dapat butang ini tanpa perubahan
  lanjut.

## Pengesahan Fasa 3 (bukan sekadar `php -l`)

- `dashboard.php`, `report-division.php`, `subsidiary-dashboard.php`
  diuji langsung (MariaDB + PHP built-in server) — semua **HTTP 200,
  sifar ralat PHP**.
- JSON command bar (27 halaman) diuji parse dengan Python `json.loads()`
  — sah, setiap entri ada `path`+`label` tidak kosong.
- `api/nav-search.php` diuji — pulangkan 38 subsidiari dalam format
  JSON yang betul.
- **SEMUA blok JavaScript inline** pada 3 halaman ini (5 blok setiap
  halaman: pra-render, togol nav/gelap, command bar, carta, footer)
  diekstrak dan disahkan dengan `node --check` — **sifar ralat sintaks
  JS**.

## Tindakan Susulan (Fasa 3)

1. Muat naik fail baharu bersama baki sistem:
   `public/assets/css/dark-mode.css`, `public/api/nav-search.php`,
   `public/export-center.php`.
2. Tiada migrasi SQL diperlukan untuk Fasa 3.

---

# FASA 3 (SUSULAN) — Baki Item Diselesaikan

## 12. Penapis Tersimpan Per-Pengguna (Penemuan 4.2)

**Perubahan (`public/dashboard.php`):** butang "📌 Simpan penapis ini
sebagai lalai saya" di bawah bar penapis — simpan kombinasi
tahun/tempoh/sektor/cluster/status SEMASA (query string penuh) ke
`localStorage['spms_dashboard_saved_filter']`. Apabila pengguna buka
`/dashboard.php` **TANPA** sebarang query string (cth. klik pautan
sidebar/command bar biasa), JS auto-alih ke gabungan tersimpan itu.
Kalau URL SUDAH ada query string (cth. pautan dikongsi rakan sekerja
atau bookmark khusus), tak diganggu — hormat apa yang diminta URL.
Peribadi setiap peranti (localStorage), bukan tetapan pelayan.

## 13. Personalisasi Widget untuk SEMUA Peranan (Penemuan 4.3)

**Nota reka bentuk:** jadual `dashboard_widgets` (susun atur GLOBAL,
kawalan super_admin) sudah pernah punca satu bug menyusahkan (lihat
`sql/migration_fix_dashboard_widgets.sql` sedia ada dalam pakej —
"Save Layout" senyap gagal kerana `widget_id` tak sepadan). Memandangkan
risiko itu, personalisasi untuk role LAIN dilaksanakan secara
**berasingan sepenuhnya** daripada jadual admin tersebut — TIDAK
sentuh/migrasi jadual `dashboard_widgets` langsung, mengelak sebarang
risiko mengulangi bug serupa.

**Perubahan (`public/dashboard.php`):** setiap widget kini ada butang
"✕" kecil (di penjuru, tersedia untuk **SEMUA peranan** — bukan
super_admin sahaja) untuk sembunyi widget tersebut PADA PERANTI
peribadi pengguna (`localStorage['spms_hidden_widgets']`). Bar
"N widget disembunyikan — Papar semula semua" terpapar apabila ada
widget disembunyikan. Ini BERBEZA dan tidak mengganggu susun atur
GLOBAL admin — dua lapisan personalisasi berasingan (admin: susun
atur untuk SEMUA orang; pengguna: sembunyi untuk DIRI sendiri sahaja).

## 14. Onboarding — Perkenalkan Ciri Baharu (Penemuan 4.x)

**Nota reka bentuk:** tooltip "anak panah" menuding ke setiap elemen
UI dielakkan sengaja — kedudukan sebegitu rapuh merentasi saiz skrin
berbeza (mobile/desktop) dan bertambah kerumitan penyelenggaraan setiap
kali UI berubah. Sebaliknya, panel ringkas SEKALI PAPARAN (bukan
per-halaman) yang menyenaraikan ciri baharu secara tulisan — lebih
selamat, dan tetap capai matlamat "pengguna sedar ciri wujud".

**Perubahan (`includes/layout_footer.php`):** panel diri-tutup
terpapar SEKALI sahaja (`localStorage['spms_onboarding_v1_seen']`)
memperkenalkan Command Bar, Mod Gelap, eksport PNG carta, dan sembunyi
widget peribadi — muncul di MANA-MANA halaman pertama pengguna
kunjungi selepas kemas kini (bukan dikunci ke satu halaman sahaja).

## 15. Pusat Laporan & Eksport (Penemuan 4.5)

**Perubahan:**
- `public/export-center.php` (fail baharu) — halaman TUNGGAL
  menyenaraikan SEMUA laporan/eksport tersedia:
  - Borang pilih Anak Syarikat + Tahun + Suku Tahun, dengan butang
    terus ke Excel (`export-excel.php`) / Cetak-PDF (`export-print.php`)
    — TIADA logik eksport baharu, hanya bina pautan ke fail sedia ada
    (konsisten dengan pembetulan Fasa 1: satu tempat sahaja untuk
    logik pengiraan).
  - Senarai Laporan Peringkat Group (Overall Performance, Division
    Analysis, Follow-Up Checklist, Risk Monitoring, Audit Trail) —
    **ditapis mengikut `can_access_route()`** (peraturan akses SEBENAR
    dalam `includes/rbac.php`), jadi pengguna hanya nampak laporan yang
    mereka benar-benar boleh akses.
- Pautan baharu ditambah di sidebar (bahagian "Utama", untuk SEMUA
  peranan) DAN didaftarkan dalam senarai Command Bar (Ctrl+K).
- `lang/ms.php`, `lang/en.php` — kunci `nav.export_center`.

## Pengesahan (Fasa 3 Susulan)

Diuji langsung (MariaDB + PHP built-in server): `dashboard.php` (dengan
widget-hide, save-filter, onboarding) dan `export-center.php` (borang
eksport + senarai laporan tertapis) — **HTTP 200, sifar ralat PHP**.
Semua blok JavaScript pada kedua-dua halaman diekstrak & disahkan
dengan `node --check` — **sifar ralat sintaks JS**. Lint PHP penuh
merentasi 131 fail — sifar ralat.

## Semua Item Fasa 3 Kini Selesai

Tiada baki daripada senarai Fasa 3 asal. Baki keseluruhan projek:
Fasa 4 (SSO/Azure AD, job queue/webhook, automated test suite PHPUnit,
CI pipeline) — lihat jadual "Pelan Tindakan Dicadangkan" dalam laporan
Word asal untuk skop penuh.


