# Backup otomatis dan pemulihan

## Status awal

Kode backup, penyimpanan terpisah, penjadwal, dan pemulihan disertakan. Layanan produksi belum terhubung. Dashboard menampilkan **Belum aktif**. Uji lokal membuktikan logika snapshot, enkripsi, pengiriman arsip, idempotensi harian, serta pemulihan database dan file; ini tidak membuktikan cron produksi telah berjalan.

## Pasang layanan cadangan terpisah

1. Buat bucket privat khusus cadangan pada akun/cloud storage organisasi. Jangan gunakan bucket media aplikasi. Batasi akses operator yang dapat membaca cadangan dan rahasia.
2. Salin `backup-service/wrangler.example.toml` ke konfigurasi deployment layanan tersebut. Ganti nama bucket dan `APP_URL` dengan origin situs. Deploy `backup-service/worker.ts` melalui prosedur Cloudflare organisasi.
3. Pasang `BACKUP_SERVICE_TOKEN` dan `APP_OPERATIONS_TOKEN` sebagai secret pada layanan cadangan. Nilai kedua harus sama dengan `OPERATIONS_TOKEN` pada aplikasi.
4. Pasang `BACKUP_SERVICE_URL`, `BACKUP_SERVICE_TOKEN`, dan `BACKUP_KEY` pada runtime aplikasi. `BACKUP_KEY` harus berupa 256 bit acak yang berbeda dari kunci authenticator.
5. Bila situs masih privat, layanan scheduler juga membutuhkan secret `SITES_ACCESS_TOKEN` yang sah untuk header `OAI-Sites-Authorization`. Dapatkan service credential dari pengelolaan Sites; jangan memasukkannya ke source atau jadwal berbasis teks. Situs publik tetap memerlukan `OPERATIONS_TOKEN` aplikasi.
6. Cron layanan cadangan memeriksa setiap menit. Aplikasi memutuskan kapan backup harian jatuh tempo berdasarkan WIB dan pengaturan pemilik. Default pukul 02:00 WIB, masa simpan 30 hari.
7. Uji satu backup manual dan satu pemanggilan cron. Periksa status, waktu selesai, jumlah file, dan manifest terenkripsi. Setelah cron terverifikasi, set `BACKUP_SCHEDULE_ACTIVE=true` dan deploy environment revision. Flag tersebut hanya indikator konfigurasi, bukan pengganti cron.
8. Aktifkan pemberitahuan kegagalan melalui layanan observabilitas/alert organisasi. Dashboard sudah menampilkan kegagalan. Notifikasi eksternal belum dikonfigurasi pada penyerahan awal.

Pemilik mengubah waktu dan retensi dari dashboard. Timestamp kedaluwarsa dikirim dengan setiap objek arsip. Layanan cadangan menghapus objek yang lewat masa simpan. Waktu dan zona memakai Asia/Jakarta. Jadwal tidak menghasilkan cadangan baru bila hari tersebut sudah memiliki backup berhasil.

## Isi dan keamanan cadangan

Satu transaksi read D1 (`batch`) mengambil tabel aplikasi dalam snapshot konsisten. Sesi login dan pembatas percobaan tidak dipulihkan. Metadata foto, dokumen publik/privat, bukti donor, ledger, donasi, mutasi, pencocokan, versi, akun, kode pemulihan yang sudah dipakai, dan audit termasuk dalam cadangan.

Berkas media bersifat tetap; mengganti gambar membuat key baru dan file lama tidak dihapus selama masih dipakai versi. Snapshot yang diambil sebelum unggahan baru tetap merujuk kumpulan file yang konsisten. Setiap file dan manifest dienkripsi AES-256-GCM sebelum dikirim. Manifest mencatat checksum untuk memverifikasi file setelah dekripsi. Kunci aplikasi disimpan hanya di dalam manifest terenkripsi agar secret authenticator dapat dipulihkan; tidak ditampilkan pada API dashboard atau log.

Simpan `BACKUP_KEY` pada pengelola rahasia terpisah. Kehilangan kunci ini membatasi kemampuan pemulihan. Cadangan bukan dokumen publik dan tidak boleh diunggah ke Galeri publik atau dibagikan kepada donatur.

Batas backup aplikasi: snapshot JSON kurang dari 15 MB, berkas maksimal sesuai batas unggahan. Pemulihan dashboard sampai 5.000 operasi baris. Untuk skala lebih besar, gunakan ekspor/restore D1 resmi pada layanan backup operator. Kegagalan batas ditampilkan, bukan dianggap cadangan lengkap.

## Pemulihan melalui dashboard

Gunakan ini untuk data atau file yang hilang tanpa menimpa catatan aktif.

1. Pemilik melakukan autentikasi ulang.
2. Pilih backup berhasil lalu **Periksa pemulihan**. Periksa waktu, cakupan, jumlah file, jumlah baris, dan transaksi setelah snapshot.
3. Isi alasan dan konfirmasi **PULIHKAN DATA**.
4. Aplikasi membuat backup kondisi saat ini. Bila file sudah hilang, checkpoint ditandai **sebagian**, dengan penjelasan; bukan backup lengkap.
5. File yang hilang dipulihkan dan checksum diperiksa. Baris yang hilang dimasukkan dalam transaksi database. Baris aktif tidak ditulis ulang. Keuangan tidak digandakan, sesi lama tidak dihidupkan, dan kode pemulihan yang sudah dipakai tetap terpakai.
6. Rekening aktif tidak ditimpa. Jika row rekening hilang, identitas dari cadangan dimasukkan sebagai belum dikonfirmasi. Pemilik mengonfirmasi melalui proses rekening yang terpisah.
7. Tinjau riwayat pemulihan dan rekonsiliasi sesudah waktu backup. Periksa publikasi dan hak akses sebelum membuka layanan.

Ini adalah pemulihan data yang hilang, bukan rollback semua baris ke waktu lampau. Pemulihan konten ke versi lama dilakukan dari Riwayat versi. Rollback penuh membutuhkan lingkungan terpisah dan penanganan transaksi sesudah snapshot.

## Pemulihan penuh di lingkungan terpisah

Operator menjalankan `scripts/recover-backup.mjs` dengan `BACKUP_KEY`, `BACKUP_SERVICE_URL`, serta `BACKUP_SERVICE_TOKEN` dari secret manager. Berikan ID backup berhasil dan direktori output yang privat. Rahasia tidak diberikan sebagai argumen perintah.

```bash
node --experimental-strip-types scripts/recover-backup.mjs BACKUP_ID work-recovery
```

Script memverifikasi enkripsi/checksum, menulis file dalam `files/public/` dan `files/private/`, menyimpan `snapshot.json`, `restore.sql`, serta `.env.recovery` dengan mode file 0600. Folder dibuat mode 0700. File dekripsi hanya berada pada mesin pemulihan yang dipercaya; jangan commit atau bagikan folder tersebut.

Buat D1 dan R2 baru yang terpisah. Terapkan kedua migrasi skema pada database kosong, kemudian `restore.sql`. SQL menunda pemeriksaan FK hingga commit, mengembalikan donasi/pencocokan/ledger secara konsisten, dan mengunci transaksi kembali. Rekening sengaja kembali belum dikonfirmasi sampai pemeriksaan pemilik. Unggah file menggunakan key yang sama ke bucket pemulihan. Set `DATA_KEY` dari `.env.recovery` pada lingkungan tersebut, serta konfigurasi identitas dan origin yang tepat. Jangan sambungkan rekening atau merchant produksi saat uji.

Periksa jumlah transaksi, receipt unik, saldo per program, prioritas, pengeluaran/biaya/refund, pencocokan bank, foto, PDF, versi konten, peran, dan kode pemulihan. Sesi login tidak dicadangkan. Login baru dan faktor kedua diperlukan. Periksa `PRAGMA foreign_key_check` dan `PRAGMA integrity_check`.

Untuk produksi, pemilik mengonfirmasi cakupan dan melakukan autentikasi ulang. Catat alasan, pelaku, waktu, checksum sumber, serta hasil uji pada riwayat pemulihan. Cadangkan keadaan saat ini terlebih dahulu. Jangan menghapus penerimaan setelah waktu backup: simpan ekspor transaksi tersebut, cocokkan dengan bank, lalu lakukan pemulihan/merge terkontrol. Alokasi dan receipt unik mencegah penerimaan yang sudah ada dimasukkan dua kali. Pengembalian status lama tidak boleh mengabaikan transaksi baru atau kode pemulihan yang sudah digunakan.

Pergantian resource produksi dilakukan oleh operator berwenang setelah pemeriksaan pemilik. Pemeriksaan operator tetap wajib walaupun uji lokal lulus. Batas pemulihan adalah data yang benar-benar ada pada backup terakhir ditambah transaksi yang dapat direkonsiliasi setelahnya.
