#!/bin/bash # backup-campuses.sh - every campus's status panel settings, off-site in Google Drive, nightly. # # The same pattern as the checklist's backup-to-drive.sh on this Mac: a LaunchAgent runs it at 04:30, # it copies into the shared Drive (Waters Creative > Apps > Status Panel Backups > ), keeps # the newest 30 per campus, and emails an alert (through the checklist's alert.js) when something # fails, because a backup nobody hears about failing is no backup. # # Only the hub (North Attleboro: no "home" in campuses.json) does anything; elsewhere it exits. # Each campus is reached over SSH with the panel's own key, as the account it reported. What is # copied: config.json (the settings), history/ (every Apply), the page-key record and the campus # lists. Never outlets.json (the smart-plug keys stay on their Mac). set -uo pipefail DEST="$HOME/Library/Application Support/status-panel" LOG="$HOME/Library/Logs/status-panel-backup.log" KEEP=30 KEY="$HOME/.ssh/id_status_panel" FILES="config.json history nav_locations.json campuses.json computers.json discover-seen.json" log() { printf '%s %s\n' "$(date '+%Y-%m-%d %H:%M:%S')" "$*" >> "$LOG"; echo "$*"; } alert() { log "ALERT: $1" local js="$HOME/checklist-app/scripts/alert.js" [ -f "$js" ] && node "$js" "Status panel backup: $1" "${2:-}$(printf '\n\nLog: %s' "$LOG")" >> "$LOG" 2>&1 || true } /usr/bin/python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); sys.exit(0 if d.get("list") and not d.get("home") else 1)' \ "$DEST/campuses.json" 2>/dev/null || { log "not the hub: nothing to back up from here"; exit 0; } DRIVE="" for acct in "$HOME/Library/CloudStorage"/GoogleDrive-*; do [ -d "$acct/Shared drives/Waters Creative/Apps" ] && { DRIVE="$acct/Shared drives/Waters Creative/Apps/Status Panel Backups"; break; } done if [ -z "$DRIVE" ]; then alert "tonight's backup did not run - the Google Drive folder is missing" \ "Shared drives > Waters Creative > Apps could not be found on this Mac. Usually Google Drive is not running or signed out." exit 1 fi mkdir -p "$DRIVE" || { alert "could not create $DRIVE"; exit 1; } log "── run start ── into $DRIVE" STAMP="$(date '+%Y-%m-%d')" FAILED="" while IFS=$'\t' read -r id user host; do [ -z "$id" ] && continue mkdir -p "$DRIVE/$id" OUT="$DRIVE/$id/$id-$STAMP.tgz" TMP="$(mktemp -t status-backup)" if [ "$host" = "self" ]; then (cd "$DEST" && tar -czf "$TMP" $(ls -d $FILES 2>/dev/null)) 2>>"$LOG" else ssh -n -i "$KEY" -o BatchMode=yes -o ConnectTimeout=15 -o StrictHostKeyChecking=accept-new "$user@$host" \ "cd 'Library/Application Support/status-panel' && tar -czf - \$(ls -d $FILES 2>/dev/null)" > "$TMP" 2>>"$LOG" fi # Listing it proves it whole (not gzip -t: macOS tar pads a piped archive, which gzip calls "trailing garbage"). if [ -s "$TMP" ] && tar -tzf "$TMP" 2>/dev/null | grep -qx 'config.json'; then cp "$TMP" "$OUT.part" && mv "$OUT.part" "$OUT" && log "✓ $id -> $(basename "$OUT") ($(du -k "$OUT" | cut -f1) KB)" ls -1t "$DRIVE/$id"/"$id"-*.tgz 2>/dev/null | tail -n +$((KEEP + 1)) | while read -r old; do rm -f "$old"; done else FAILED="$FAILED $id" log "✗ $id: nothing usable came back from $user@$host" fi rm -f "$TMP" done < <(/usr/bin/python3 - "$DEST/campuses.json" "$DEST/config.json" <<'PY' import json, sys d = json.load(open(sys.argv[1])); me = (json.load(open(sys.argv[2])).get("site") or {}).get("id") for cid, e in sorted((d.get("list") or {}).items()): if cid == me: print("%s\t-\tself" % cid) elif e.get("user") and e.get("url"): print("%s\t%s\t%s" % (cid, e["user"], e["url"].split("//")[1].split(":")[0])) else: print("%s\t?\t?" % cid) PY ) if [ -n "$FAILED" ]; then alert "no backup tonight for:$FAILED" "The other campuses were backed up. A campus missing here is usually off, off the network, or no longer letting this Mac log in." exit 1 fi log "── done ──"