mirror of
https://codeberg.org/PostERG/xamxam.git
synced 2026-06-25 16:19:19 +02:00
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
This commit is contained in:
198
backup-plan.md
Normal file
198
backup-plan.md
Normal file
@@ -0,0 +1,198 @@
|
||||
# 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'` |
|
||||
Reference in New Issue
Block a user