2026-10-20 17:17:04 -04:00
#!/bin/bash
2026-10-21 13:42:22 -04:00
# backup-campuses.sh - every location's status panel settings, off-site in Google Drive, nightly.
2026-10-20 17:17:04 -04:00
#
# The same pattern as the checklist's backup-to-drive.sh on this Mac: a LaunchAgent runs it at 04:30,
2026-10-21 13:42:22 -04:00
# it copies into the shared Drive (Waters Creative > Apps > Status Panel Backups > <location>), keeps
# the newest 30 per location, and emails an alert (through the checklist's alert.js) when something
2026-10-20 17:17:04 -04:00
# fails, because a backup nobody hears about failing is no backup.
#
2026-10-21 13:12:30 -04:00
# Only the hub (North Attleboro: no "home" in campuses.json) does anything; elsewhere it exits, except
2026-10-21 13:42:22 -04:00
# on a standalone location (Apollo Beach), which backs up only itself into ~/Documents/Status Panel Backups.
# Each location 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 location
2026-10-20 17:17:04 -04:00
# 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
}
2026-10-21 13:42:22 -04:00
MODE = $( /usr/bin/python3 -c 'import json,sys; d=json.load(open(sys.argv[1])); print("standalone" if d.get("standalone") else "location" if d.get("home") else "hub" if d.get("list") else "")' \
2026-10-21 13:12:30 -04:00
" $DEST /campuses.json" 2>/dev/null)
[ " $MODE " = hub ] || [ " $MODE " = standalone ] || { log "not the hub: nothing to back up from here" ; exit 0; }
2026-10-20 17:17:04 -04:00
DRIVE = ""
2026-10-21 13:12:30 -04:00
if [ " $MODE " = standalone ] ; then
2026-10-21 13:42:22 -04:00
# A location on its own (Apollo Beach) keeps its own copies, in this Mac's Documents (or wherever
2026-10-21 13:12:30 -04:00
# STATUS_PANEL_BACKUPS points, e.g. a Google Drive folder).
DRIVE = " ${ STATUS_PANEL_BACKUPS :- $HOME /Documents/Status Panel Backups } "
else
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
fi
2026-10-20 17:17:04 -04:00
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
2026-10-21 13:42:22 -04:00
alert "no backup tonight for: $FAILED " "The other locations were backed up. A location missing here is usually off, off the network, or no longer letting this Mac log in."
2026-10-20 17:17:04 -04:00
exit 1
fi
log "── done ──"