8.1 KiB
Changelog — 2026-07-17 — P2 backup daemon (Track G) + P2-5 bulk marks audit
Branch: ops/sqlite-backup
Worktree: .worktrees/ops-sqlite-backup/
Owner: developer (devops)
Date: 2026-07-17
This change adds the production-host backup / restore / verify tooling for the Africa Alert SQLite database (P2-1), and documents an audit of the existing bulk marks CSV import path (P2-5). No application code is touched and no schema changes are introduced.
Files added
| Path | Purpose |
|---|---|
scripts/backup/africa-alert-backup.sh |
Hot online backup via sqlite3 .backup. Configurable via DB_PATH, BACKUP_DIR, LOG_DIR, RETAIN_DAILY env vars. Copies -wal/-shm sidecars if present, writes a sha256sum next to the snapshot, and prunes the local retention window. |
scripts/backup/install-backup-cron.sh |
One-shot installer. Appends a 0 */12 * * * cron entry (configurable via CRON_SCHEDULE), idempotent via a crontab -l | grep -q africa-alert guard. Logs the next-run estimate to logs/backup-install.log. |
scripts/backup/restore.sh |
Destructive restore. Prompts for typed "yes" at a TTY (or RESTORE_CONFIRM=yes from automation), stops africa-alert.service via systemctl, takes a pre-restore-YYYYmmdd-HHMMSS safety snapshot, copies the backup (and its sidecars) over the live DB, removes any stale live sidecars that do not have a matching backup sidecar, then restarts the service. |
scripts/backup/verify-restore.sh |
Smoke-test. Copies the backup to a temp dir, spawns the server with DB_PATH and PORT overrides, polls /api/auth/login until the route is ready, asserts a 200 with the admin creds (admin@school.com / admin123), and cleans up. Exits 0 on success, 1 on failure. |
docs/BACKUP.md |
Operator runbook. Installation one-liner, manual trigger, restore drill, retention policy (30 daily + 12 weekly with a copy-pasteable find command), failure-alerting contract, and a full disaster-recovery scenario for a bare-metal rebuild. |
No .sh script in this change was executed on Windows. They are written
for the production Ubuntu 22.04 host. Verification on this dev machine is
limited to bash -n syntax checks (results in §Verification below).
Bulk marks audit
Audit target: server/src/controllers/marks.controller.js — the
POST /api/marks/bulk endpoint that the teacher CSV importer calls.
Expected CSV header
assignment_id,student_id,score,feedback
The controller uses csv-parse/sync with columns: true, so the first
non-empty row is the header. Columns are lowercased by trim: true; the
parser trims whitespace on each value and tolerates a UTF-8 BOM (bom: true).
Per-row behaviour (summary)
- Validate the four required fields:
assignment_idandstudent_idmust be present and resolve to a realassignments.id/users.id;scoremust be numeric, non-negative, and<= assignments.max_scorewhenmax_scoreis non-null. The student must haverole='student'. - Look up an existing
submissionsrow by(assignment_id, student_id). If found, UPDATE the row'sgrade/feedback/graded_by/graded_at, forcestatus='graded', setsync_status='pending', and bumpupdated_at. If not found, INSERT a new row with the same fields, a freshly generateduid, andsync_status='pending'. - The whole loop is wrapped in a single
better-sqlite3transaction, so the per-row counters are consistent. Per-row errors are collected into anerrorsarray and the loop continues; a single bad row never aborts the rest of the batch. - After the transaction, the controller writes a best-effort audit log
(
AuditService.log, actionBULK_UPLOAD_MARKS, entity typesubmission) with{ total_rows, inserted, updated, error_count, filename }. Audit failures do not fail the response. - Response:
{ inserted, updated, errors: [{ row, reason, data }], total_rows }.
Gap analysis
- No header validation. A CSV with the wrong header (e.g.
student_name, scorebecause the teacher used the wrong export) will produce 0 inserts and N errors all of the formmissing assignment_id, with no upfront "wrong header" message. The recommended fix is a small header check at the top of the handler that returns 400 with a clear message naming the expected columns. - No teacher ownership / class-scope guard. The
adminOrTeachermiddleware is permissive: any teacher can grade any student for any assignment. A teacher could overwrite another teacher's grades by re-uploading the same CSV. Ateacher.assigned_classes/assignments.created_bycheck would close this gap; not done here because the existing RBAC refactor (Track C P1-10) is the right place to add it consistently. - No
dry_runmode. The handler always commits. A query flag (?dry_run=true) that runs the parse + validation + assignment/student pre-cache but skips the transaction would let operators catch malformed CSVs cheaply. - Score type is loosely validated.
Number(scoreRaw)accepts floats (75.5) even when the assignment expects an integer. The upper-bound check (score > a.max_score) catches overshoots but a fractional score that happens to be ≤ max_score will be stored as a float. Either pre-round or require integer explicitly. - MIME allowlist is permissive on purpose (
application/octet-streamand empty MIME are accepted so that browsers and curl both work). This is documented in the controller comment, but is worth a re-review when the file-attachment work in Track H lands a unified multer configuration.
Recommendation
Keep as-is for this PR — these gaps are real but small, and the fixes are best done in the same PR as the broader RBAC refactor (Track C) and the dry-run work (Track B tests). Track G's scope is backup tooling, not the marks controller. The audit findings above are captured here so the next agent who picks up Track C / Track B has them.
Verification
The repo's dev machine is Windows 11. The .sh scripts target the
production Ubuntu 22.04 host. Verification on this box is limited to
bash -n (syntax-only) using Git Bash. The actual end-to-end smoke
(bash africa-alert-backup.sh && bash verify-restore.sh) is run on
the production host after the branch is deployed.
| Check | Result |
|---|---|
bash -n scripts/backup/africa-alert-backup.sh |
SYNTAX_OK |
bash -n scripts/backup/install-backup-cron.sh |
SYNTAX_OK |
bash -n scripts/backup/restore.sh |
SYNTAX_OK |
bash -n scripts/backup/verify-restore.sh |
SYNTAX_OK |
wc -l docs/BACKUP.md |
283 docs/BACKUP.md (target: ≥80) |
| Marks bulk-audit findings | documented above; one-sentence recommendation: keep as-is, fix in Track B / Track C |
Deviations from the plan
.gitignorecovered already:logs/is created at the project root forbackup.log/verify-restore.log/backup-install.log. The existing*.logentry in.gitignore(line 9) catches the per-run log files, so no.gitignorechange is needed.- G.8 (Playwright
bulk-marks.spec.ts) deferred to Track B. The plan notes "Skip this if it would risk breaking the existing 24/24 Playwright baseline." The Playwright test infrastructure is on a different worktree (feature/playwright-e2e); adding specs from this branch would either duplicate fixtures (fragile) or skip them (defeats the purpose). Track B owns the test infra; the three specs listed in the plan should be added there.
Follow-ups
- On the production host, after deploy: run
bash -nonce more (paranoia), thensudo /usr/local/bin/install-backup-cron.shandcrontab -lto confirm the entry. Run one manual backup, thenverify-restore.shagainst the new snapshot to confirm the round trip. - Wire
logs/backup.logto the operator's monitoring (Promtail, mailx, Telegram webhook, etc.). The contract is indocs/BACKUP.md§6. - Promote the 12-weekly retention in
docs/BACKUP.md§5 to a real cron entry. The runbook gives the script; the cron line is left for the operator to paste. - Add the four
server/src/controllers/marks.controller.jsaudit items to the Track C / Track B backlogs.