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
+24 -1
View File
@@ -692,6 +692,27 @@ class Handler(http.server.BaseHTTPRequestHandler):
return self.send(200, campus.install_script(self.base_url(), q.get("id", ["new"])[0], key), "text/plain; charset=utf-8")
if path == "/campus/program.tgz":
return self.send(200, campus.program_kit(site.data_dir), "application/gzip")
if path == "/campus/companion.zip":
# Companion 5.0.7 as this Mac has it, for `settings.campus upgrade-companion` on a campus
# still on an older one (packed once, kept beside the config, streamed).
app = "/Applications/Companion 5.0.7.app"
pack = os.path.join(site.data_dir, "companion-5.0.7.zip")
if not os.path.exists(app):
return self.send(404, {"error": "no Companion 5.0.7 on this Mac"})
if not os.path.exists(pack):
subprocess.run(["/usr/bin/ditto", "-c", "-k", "--keepParent", app, pack + ".tmp"], check=True, timeout=900)
os.replace(pack + ".tmp", pack)
self.send_response(200)
self.send_header("Content-Type", "application/zip")
self.send_header("Content-Length", str(os.path.getsize(pack)))
self.end_headers()
with open(pack, "rb") as f:
while True:
chunk = f.read(1 << 20)
if not chunk:
break
self.wfile.write(chunk)
return
if path == "/join":
return self.send(200, campus.join_mac(site.load(), self.base_url()), "text/plain; charset=utf-8")
if path == "/join.ps1":
@@ -893,7 +914,9 @@ class Handler(http.server.BaseHTTPRequestHandler):
lst = data.setdefault("list", {})
for other in [k for k, e in lst.items() if k != cid and (e or {}).get("url") == url]:
lst.pop(other) # the same Mac under its old name (before its first setup)
lst[cid] = {"id": cid, "name": str(body.get("name") or cid)[:40], "url": url}
user = str(body.get("user") or "")
lst[cid] = dict({"id": cid, "name": str(body.get("name") or cid)[:40], "url": url},
**({"user": user} if re.fullmatch(r"[A-Za-z0-9._-]{1,40}", user) else {}))
campuses.save(site.data_dir, data)
return self.send(200, {"ok": True})
if path == "/api/fleet/hello":