- pages (NA 98 Locations / 99 Location, re-imported via PAGES_VERSION), keys, settings page, setup and install messages, alerts, docs, the Apollo Beach kit; python3 -m settings.location and /location/... addresses (the old names still answer for installed Macs) - Apply reads the before settings from the file: an in-place edit skipped the watchdog restart - SC: SUPERRACK (+SoundGrid) and X32 EDIT from SC FOH; FOH TRACKS and TRACKS LIGHTS MIDI links watched Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
89 lines
4.4 KiB
Bash
Executable File
89 lines
4.4 KiB
Bash
Executable File
#!/bin/bash
|
|
# backup-campuses.sh - every location'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 > <location>), keeps
|
|
# the newest 30 per location, 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, except
|
|
# 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
|
|
# 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
|
|
}
|
|
|
|
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 "")' \
|
|
"$DEST/campuses.json" 2>/dev/null)
|
|
[ "$MODE" = hub ] || [ "$MODE" = standalone ] || { log "not the hub: nothing to back up from here"; exit 0; }
|
|
|
|
DRIVE=""
|
|
if [ "$MODE" = standalone ]; then
|
|
# A location on its own (Apollo Beach) keeps its own copies, in this Mac's Documents (or wherever
|
|
# 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
|
|
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 locations were backed up. A location missing here is usually off, off the network, or no longer letting this Mac log in."
|
|
exit 1
|
|
fi
|
|
log "── done ──"
|