# Runbook: Backup & Restore Database SIPORA

## Ringkasan

- `php tools/backup.php` — membuat backup penuh database (skema + data) sebagai file
  `.sql.gz` di `storage/backups/`. Ditulis murni PHP (tidak memanggil `mysqldump`), jadi
  tetap jalan di shared hosting yang biasanya tidak mengizinkan eksekusi proses eksternal.
- `php tools/restore.php <file> --yes` — memulihkan database dari sebuah file backup.
  **Menghapus dan mengganti seluruh isi database saat ini.** Hanya bisa dijalankan lewat
  CLI — sengaja tidak ada tombol/endpoint web untuk memicu restore.
- Halaman `/superadmin/health` (dan endpoint `/health`) menampilkan umur backup terbaru
  sebagai pemeriksaan `backup_recency` — status `warning` (bukan `error`) bila backup lebih
  tua dari 48 jam atau belum pernah dibuat sama sekali, supaya pemantau uptime tidak
  mengira aplikasi sedang mati hanya karena backup terlambat.

## Jadwal yang disarankan

- **Harian**, di luar jam sibuk sekolah (mis. 02:00-04:00 WIB), lewat cron:
  ```
  0 2 * * * cd /path/ke/sipora && php tools/backup.php >> storage/logs/backup-cron.log 2>&1
  ```
- Di shared hosting tanpa akses cron langsung, pakai fitur "Cron Jobs" panel hosting untuk
  menjalankan perintah yang sama.
- Setelah backup harian berjalan otomatis, cek berkala bahwa `/superadmin/health` menunjukkan
  `backup_recency: ok` — bila `warning`, cron belum jalan atau gagal dan perlu diperiksa.

## Retensi

Skrip ini TIDAK menghapus backup lama secara otomatis (sengaja — supaya tidak pernah diam-diam
kehilangan backup lama tanpa keputusan eksplisit). Kelola retensi lewat cron terpisah, mis.:

```
# Hapus backup lebih tua dari 30 hari, jalankan setelah backup.php di cron yang sama
find /path/ke/sipora/storage/backups -name "sipora-backup-*.sql.gz" -mtime +30 -delete
```

Simpan salinan backup di luar server aplikasi (mis. diunduh berkala ke penyimpanan terpisah)
supaya kegagalan pada server yang sama (disk penuh/rusak) tidak menghapus backup sekaligus
data aslinya. `storage/backups/` berada di luar `public/` sehingga tidak bisa diakses lewat
HTTP — jangan ubah struktur direktori ini tanpa memastikan tetap begitu.

## Keamanan

- File backup berisi data pribadi siswa (nama, NIS/NISN) dan hash/ciphertext kredensial —
  perlakukan dengan tingkat kerahasiaan yang sama dengan database produksi.
- `storage/backups/` sudah di-`.gitignore` — jangan pernah commit file backup ke git.
- Batasi permission direktori (`chmod 700 storage/backups`) supaya hanya user aplikasi yang
  bisa membaca isinya di server dengan banyak pengguna.
- Setiap backup/restore tercatat di audit log (`backup.created`, `backup.restored`) — periksa
  halaman audit log Super Admin bila ada backup/restore yang tidak dikenali.

## Prosedur restore (langkah demi langkah)

1. **Hentikan trafik ke aplikasi** bila memungkinkan (mode maintenance/nonaktifkan sementara) —
   restore mengganti seluruh isi database, transaksi yang sedang berjalan saat itu bisa hilang.
2. Pastikan Anda memilih file backup yang benar: `ls -la storage/backups/` untuk melihat
   tanggal setiap file.
3. Jalankan tanpa `--yes` dulu untuk melihat peringatan:
   ```
   php tools/restore.php storage/backups/sipora-backup-20260115-020000.sql.gz
   ```
4. Setelah yakin, jalankan ulang dengan `--yes`:
   ```
   php tools/restore.php storage/backups/sipora-backup-20260115-020000.sql.gz --yes
   ```
5. Jalankan `php tools/migrate.php status` untuk memastikan skema hasil restore konsisten
   dengan kode aplikasi yang sedang berjalan (backup lama bisa jadi belum punya migrasi
   terbaru — jalankan `php tools/migrate.php up` bila ada migrasi yang masih pending).
6. Buka `/superadmin/health` untuk memastikan semua pemeriksaan kembali `ok`.
7. Aktifkan kembali trafik ke aplikasi.
8. Catat insiden (kapan, kenapa restore diperlukan, file backup mana yang dipakai) di luar
   sistem (mis. dokumen internal tim) — audit log mencatat BAHWA restore terjadi, bukan
   alasan bisnisnya.

## Uji restore berkala (WAJIB, bukan opsional)

Backup yang belum pernah diuji restore-nya **tidak boleh dianggap sebagai backup yang valid**.
Minimal setiap kuartal, uji siklus penuh di lingkungan terpisah (bukan database produksi):

1. Siapkan database MySQL/MariaDB kosong terpisah untuk pengujian.
2. Arahkan `.env` sementara (di direktori kerja terpisah, atau lewat environment variable)
   ke database uji tersebut.
3. Jalankan `php tools/restore.php <backup-terbaru> --yes` ke database uji itu.
4. Verifikasi: `php tools/migrate.php status` bersih, jumlah baris tabel-tabel kunci
   (`schools`, `students`, `users`) masuk akal, dan aplikasi bisa login dengan salah satu akun
   dari data yang di-restore.
5. Catat hasil uji (tanggal, backup yang diuji, berhasil/gagal) — bila gagal, backup harian
   yang sedang berjalan mungkin punya masalah dan perlu diselidiki SEBELUM benar-benar
   dibutuhkan saat insiden sungguhan.

## Catatan teknis

- Format file: SQL polos (DROP TABLE + CREATE TABLE + INSERT batch per tabel), dikompres
  gzip. Bisa dibuka manual dengan `gunzip -c file.sql.gz | less` bila perlu diperiksa isinya.
- Restore memakai `PDO::MYSQL_ATTR_MULTI_STATEMENTS` pada koneksi khusus (bukan koneksi
  `db()` biasa yang dipakai aplikasi) supaya seluruh file (banyak statement SQL) bisa
  dieksekusi dalam satu panggilan dengan benar — MySQL sendiri yang memisah antar statement,
  bukan pemisahan `;` naif di PHP yang bisa salah bila ada `;` di dalam nilai teks.
- DDL (`DROP TABLE`, `CREATE TABLE`) di MySQL selalu auto-commit dan TIDAK BISA di-rollback —
  bila restore gagal di tengah jalan, database bisa berada dalam kondisi sebagian ter-restore.
  Selalu punya rencana pemulihan kedua (mis. backup sebelumnya) sebelum memulai restore.
