dow-prod-tracker/scripts/backup-db.sh
DJP 307619ffe6 Nightly pg_dump backups + admin "Export Full XLSX" button
Two complementary safety nets for the business-critical DB:

1. Host-side nightly backup
   - scripts/backup-db.sh drives pg_dump through docker compose exec,
     gzips to /srv/backups/dow-prod-tracker/, auto-prunes >30 days.
     Env-overridable (BACKUP_DIR / RETAIN_DAYS / COMPOSE_DIR / PGUSER
     / PGDATABASE) for anyone running a different layout.
   - Runs from host cron at midnight; crontab snippet + restore
     procedure + optional off-site (S3 / rsync) pattern documented
     in DEPLOY.md.

2. On-demand admin XLSX export
   - New GET /api/projects/export?format=xlsx — ADMIN-only; builds a
     two-sheet workbook via new buildFullExportWorkbook():
       - "Job Tracker": one row per project, header strings chosen to
         round-trip through the Dow bulk-import endpoint so a dump can
         be re-ingested in a worst case. Owner / Risk / OMG Number /
         Team / etc., mirroring the importer's fuzzy HEADER_MATCHERS.
       - "Deliverables": one row per deliverable with project OMG #,
         status, priority, dates, CMF/SKU, current stage, assignees,
         notes — enough to reconstitute pipeline state.
     Respects visibility scoping (ADMIN sees everything).
   - Dashboard shows an "Export Full XLSX" button in the header for
     admins; streams the workbook with a date-stamped filename using
     the standard blob-download pattern from ExportButton.

Both are additive — no schema, no migration, no deploy breakage.
2026-04-21 16:53:49 -04:00

56 lines
2.2 KiB
Bash
Executable file

#!/usr/bin/env bash
# scripts/backup-db.sh — nightly Postgres dump for dow-prod-tracker
#
# Runs on the HOST (not inside the app container), driving pg_dump
# through the db service's compose project. Writes a gzipped SQL
# file to $BACKUP_DIR and prunes anything older than $RETAIN_DAYS.
#
# Intended to run from cron at midnight — see DEPLOY.md for the
# crontab entry. Safe to run manually at any time.
#
# Env vars (all optional, sensible defaults for optical-dev):
# BACKUP_DIR where to write dumps (default /srv/backups/dow-prod-tracker)
# RETAIN_DAYS days to keep dumps (default 30)
# COMPOSE_PROJECT compose project name (default dow-prod-tracker)
# COMPOSE_DIR dir containing docker-compose.yml (default /opt/dow-prod-tracker)
# PGDATABASE database name (default dow_prod_tracker)
# PGUSER postgres user (default postgres)
#
# Exit codes: 0 ok, non-zero = failure (cron will mail root on non-zero).
set -euo pipefail
BACKUP_DIR="${BACKUP_DIR:-/srv/backups/dow-prod-tracker}"
RETAIN_DAYS="${RETAIN_DAYS:-30}"
COMPOSE_PROJECT="${COMPOSE_PROJECT:-dow-prod-tracker}"
COMPOSE_DIR="${COMPOSE_DIR:-/opt/dow-prod-tracker}"
PGDATABASE="${PGDATABASE:-dow_prod_tracker}"
PGUSER="${PGUSER:-postgres}"
TIMESTAMP=$(date +%Y-%m-%d_%H%M%S)
OUT_FILE="${BACKUP_DIR}/dow-prod-tracker_${TIMESTAMP}.sql.gz"
mkdir -p "$BACKUP_DIR"
echo "[backup] $(date -Iseconds) starting dump → $OUT_FILE"
# Stream pg_dump output straight through gzip to disk without buffering
# the whole dump in memory. `exec -T` keeps the TTY disabled so this
# works from cron. We `cd` into the compose dir because compose reads
# its project definition from the working dir.
(
cd "$COMPOSE_DIR"
docker compose -p "$COMPOSE_PROJECT" exec -T db \
pg_dump -U "$PGUSER" --format=plain --clean --if-exists "$PGDATABASE"
) | gzip > "$OUT_FILE"
SIZE=$(du -h "$OUT_FILE" | cut -f1)
echo "[backup] wrote $OUT_FILE ($SIZE)"
# Prune anything older than RETAIN_DAYS. `-mtime +N` is "modified more
# than N days ago" — older dumps get deleted. Fails gracefully if the
# dir is empty.
find "$BACKUP_DIR" -maxdepth 1 -type f -name 'dow-prod-tracker_*.sql.gz' \
-mtime +"$RETAIN_DAYS" -print -delete || true
echo "[backup] $(date -Iseconds) done"