Files
xamxam/backup-plan.md
Pontoporeia cf9bd5cd5d feat: require 3 mots-clés in partage, language asterisk toggle, admin auto-save checkboxes
- tag-search: add minTags/required params, counter shows red if < 3, accent if ≥ 3
- form.php: pass minTags=3 for partage mode keywords
- checkbox-list: support labelHtml for raw HTML label with targetable asterisk span
- language-autre-fragment: OOB swap updates #languages-required-asterisk when autre pills change
- language-search: client-side update #languages-required-asterisk on pill add/remove
- contenus.php: replace 3 form+submit-button fieldsets with HTMX auto-save checkboxes
- settings.php: detect HX-Request header, return OOB CSRF token updates, skip redirect
2026-05-19 00:08:06 +02:00

199 lines
6.7 KiB
Markdown

# SQLite Backup & Data Integrity Plan
## Status Legend
- `[ ]` To do
- `[x]` Done
- `[~]` Partial / needs review
---
## Phase 1 — WAL Mode
**Goal:** Ensure SQLite uses Write-Ahead Logging for safe concurrent reads and hot backups.
- [ ] Connect to the DB and verify WAL is active:
```bash
sqlite3 /path/to/your.db "PRAGMA journal_mode;"
# Expected output: wal
```
- [ ] If not `wal`, enable it (run once, persists):
```bash
sqlite3 /path/to/your.db "PRAGMA journal_mode=WAL;"
```
- [ ] Confirm the `-wal` and `-shm` sidecar files exist next to the `.db` file after a write
- [ ] Make sure nginx/PHP has write access to those sidecar files (same owner as the `.db`)
---
## Phase 2 — Audit Log
**Goal:** Record every INSERT, UPDATE, and DELETE with the actor, timestamp, and a before/after snapshot.
### 2.1 — Create the table
- [ ] Add the `audit_log` table to the DB:
```sql
CREATE TABLE IF NOT EXISTS audit_log (
id INTEGER PRIMARY KEY AUTOINCREMENT,
timestamp TEXT NOT NULL DEFAULT (datetime('now')),
actor TEXT NOT NULL,
action TEXT NOT NULL CHECK(action IN ('INSERT','UPDATE','DELETE')),
table_name TEXT NOT NULL,
record_id INTEGER,
old_data TEXT,
new_data TEXT
);
```
### 2.2 — Instrument PHP mutations
- [ ] Create a reusable `audit()` helper in PHP that accepts `$db, $actor, $action, $table, $id, $old, $new`
- [ ] Wrap every **DELETE** in the admin dashboard with `audit()`, capturing the row before deletion
- [ ] Wrap every **UPDATE** (form submissions + admin edits) with `audit()`, capturing before/after
- [ ] Wrap **INSERTs** for completeness (new_data only)
- [ ] Verify by triggering a test delete and querying `SELECT * FROM audit_log ORDER BY id DESC LIMIT 5;`
### 2.3 — Protect the audit log
- [ ] No UI should expose a "clear audit log" button
- [ ] The PHP DB user should not have `DELETE` permission on `audit_log` (use a restricted PDO connection for app queries if possible)
---
## Phase 3 — Soft Deletes
**Goal:** Prevent hard DELETEs on critical tables so data is always recoverable instantly. htmx elements that query languages/keywords must continue to work transparently.
### 3.1 — Schema changes
- [ ] Identify all tables that htmx elements query (e.g. `languages`, `keywords`, any lookup/reference tables)
- [ ] Add `deleted_at` to each:
```sql
ALTER TABLE languages ADD COLUMN deleted_at TEXT DEFAULT NULL;
ALTER TABLE keywords ADD COLUMN deleted_at TEXT DEFAULT NULL;
-- repeat for other affected tables
```
### 3.2 — Replace DELETE queries
- [ ] Search the codebase for `DELETE FROM languages`, `DELETE FROM keywords`, etc.
- [ ] Replace each hard DELETE with a soft delete:
```php
// Before
$db->prepare("DELETE FROM languages WHERE id = ?")->execute([$id]);
// After
$db->prepare("UPDATE languages SET deleted_at = datetime('now') WHERE id = ?")
->execute([$id]);
```
- [ ] Do the same in any admin dashboard bulk-delete operations
### 3.3 — Filter deleted rows everywhere
- [ ] Add `WHERE deleted_at IS NULL` to **every** SELECT that feeds an htmx endpoint:
```sql
-- Example
SELECT * FROM languages WHERE deleted_at IS NULL ORDER BY name;
SELECT * FROM keywords WHERE deleted_at IS NULL ORDER BY name;
```
- [ ] Search for raw `SELECT * FROM languages` and `SELECT * FROM keywords` across all PHP files and patch each one
- [ ] Test each htmx-driven element (dropdowns, tag lists, autocompletes) to confirm deleted entries no longer appear
### 3.4 — Admin: show soft-deleted entries
- [ ] Add an admin view that lists soft-deleted rows (`WHERE deleted_at IS NOT NULL`) with a **Restore** button
- [ ] The restore action sets `deleted_at = NULL`
---
## Phase 4 — Hourly Snapshots via Cronjob
**Goal:** Automatically save compressed, timestamped copies of the DB locally, retained for 30 days.
### 4.1 — Create the backup script
- [ ] Create `/usr/local/bin/backup-sqlite.sh`:
```bash
#!/bin/bash
DB_PATH="/var/www/myapp/database.db"
BACKUP_DIR="/var/backups/myapp"
RETENTION_DAYS="${RETENTION_DAYS:-30}"
TIMESTAMP=$(date +"%Y-%m-%dT%H-%M-%S")
BACKUP_FILE="$BACKUP_DIR/db-$TIMESTAMP.db.gz"
mkdir -p "$BACKUP_DIR"
# Safe hot backup using SQLite's online backup API
sqlite3 "$DB_PATH" ".backup /tmp/myapp-snapshot.db"
gzip -c /tmp/myapp-snapshot.db > "$BACKUP_FILE"
rm /tmp/myapp-snapshot.db
# Prune old backups
find "$BACKUP_DIR" -name "*.db.gz" -mtime +$RETENTION_DAYS -delete
echo "[$(date)] Backup written: $BACKUP_FILE"
```
- [ ] Make it executable:
```bash
chmod +x /usr/local/bin/backup-sqlite.sh
```
- [ ] Run it manually once and verify a `.db.gz` file appears in `/var/backups/myapp/`
- [ ] Test restore by decompressing and opening the snapshot:
```bash
gunzip -c /var/backups/myapp/db-<timestamp>.db.gz > /tmp/test-restore.db
sqlite3 /tmp/test-restore.db ".tables"
```
### 4.2 — Schedule with cron
- [ ] Open the crontab:
```bash
crontab -e
```
- [ ] Add hourly and daily jobs:
```cron
# Hourly snapshot — kept 30 days
0 * * * * /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
# Daily snapshot at 2am — kept 90 days
0 2 * * * RETENTION_DAYS=90 /usr/local/bin/backup-sqlite.sh >> /var/log/sqlite-backup.log 2>&1
```
- [ ] Verify the log after the next hour: `tail -f /var/log/sqlite-backup.log`
---
## Phase 5 — Remote Sync *(for later)*
**Goal:** Push backups off the VM to a remote destination so a disk failure or VM loss doesn't take your history with it.
- [ ] Choose a remote destination (Backblaze B2, S3, SFTP, etc.)
- [ ] Install and configure rclone:
```bash
apt install rclone
rclone config # set up a remote, name it "mybackups"
```
- [ ] Add remote sync to the backup script after the `gzip` step:
```bash
rclone copy "$BACKUP_FILE" mybackups:myapp-backups/
```
- [ ] Enable versioning on the remote bucket (B2/S3) so even remote overwrites are recoverable
- [ ] Test a full restore from remote:
```bash
rclone copy mybackups:myapp-backups/db-<timestamp>.db.gz /tmp/
gunzip /tmp/db-<timestamp>.db.gz
sqlite3 /tmp/db-<timestamp>.db ".tables"
```
- [ ] (Optional) Set up a separate cron to prune remote copies older than 6 months
---
## Quick Reference — Recovery Scenarios
| Scenario | Solution |
|---|---|
| Admin accidentally deleted a row | Set `deleted_at = NULL` in the relevant table |
| User submitted bad data via a form | Query `audit_log` for the `old_data` JSON, restore manually |
| Bulk accidental delete | Restore from the last hourly snapshot (< 1h data loss max) |
| VM or disk failure | Pull latest snapshot from remote (Phase 5) |
| "Who deleted this and when?" | `SELECT * FROM audit_log WHERE table_name='x' AND action='DELETE'` |