Every campus matches NA, tailored: Restart page, Companion-Mac programs, NA's auto-fix hours, nightly backups, restart from NA

- Auto-setup: programs on the Companion Mac itself (ProPresenter at Woonsocket) get a local process
  check and local start/restart fixes; every campus gets a Restart page (keys arrive as computers
  join); NA's auto-fix hours (Tue, Wed, Sun); a program found after its Companion connection joins
  that key instead of making a second one. settings.campus rebuild remakes a campus from what is
  found now, keeping its name, PIN and pages; upgrade-companion brings Companion to 5.0.7 from NA's
  copy (/campus/companion.zip), as Woonsocket was; setup stops with that advice on an older one.
- NA's CAMPUSES page has a RESTART key per campus; the CAMPUS page shows that campus's Restart page
  and a hold there restarts it at that campus.
- Backups: backup/backup-campuses.sh, nightly 04:30 on NA (installed everywhere, acts on the hub),
  into Drive > Waters Creative > Apps > Status Panel Backups/<campus>, 30 kept, alert email via the
  checklist's alert.js. Campuses report the account to back up with. sites/ws/config.json in the repo.
- Woonsocket rebuilt: 9 keys (PROP with ProPresenter on its Mac), Restart page 20 (SUPERRACK, VISTA,
  ABLETON, PROP), auto-fix on NA's hours; all OK. Both campuses backed up to Drive by the job.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-20 17:17:04 -04:00
co-authored by Claude Opus 5.5
parent b793490d90
commit 6d1cfea2f7
10 changed files with 985 additions and 29 deletions
+80
View File
@@ -0,0 +1,80 @@
#!/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 > <campus>), 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 ──"