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
+41 -5
View File
@@ -86,8 +86,11 @@ SHARED_FIXES = {
}
STATUS_ROWS, COLS = 4, 8
# North Attleboro's auto-fix hours, for every campus: Tuesday and Wednesday rehearsals, Sunday services.
DEFAULT_AUTO_FIX = {"after_seconds": 20, "max_tries": 2, "retry_seconds": 90, "pause_after_network_seconds": 60,
"restart_connections": True, "windows": [{"days": ["sun"], "start": "05:00", "end": "14:00"}]}
"restart_connections": True, "windows": [{"days": ["tue"], "start": "14:00", "end": "22:00"},
{"days": ["wed"], "start": "09:00", "end": "21:00"},
{"days": ["sun"], "start": "05:00", "end": "14:00"}]}
# --- small helpers ---------------------------------------------------------------------
@@ -218,6 +221,27 @@ def items(inv, nets):
apps_at[(a["app"], c.get("ip"))] = item
for ip in c.get("ips") or []:
apps_at.setdefault((a["app"], ip), item)
# Programs on this Mac itself (the Companion Mac): a local process check and local start/restart fixes.
ing = inv.get("ingest") or {}
for a in MAC_APPS:
exe = (ing.get("exes") or {}).get(a["process"])
app_path = (ing.get("paths") or {}).get(a["process"])
if not exe or not app_path:
continue
fid = slug("%s_ingest" % a["app"])
pretty = a["label"].title() if a["app"] not in ("propresenter", "ableton") else {"propresenter": "ProPresenter", "ableton": "Ableton"}[a["app"]]
start = {"template": "mac_start", "params": {"host": "ingest", "process": a["process"], "app": app_path},
"label": "start %s on this Mac if it is not running" % pretty, "short": a["label"]}
again = {"template": "mac_restart", "params": {"host": "ingest", "process": a["process"], "app": app_path, "wait_seconds": 10, "force": True},
"label": "restart %s on this Mac (closes it first)" % pretty, "short": a["label"]}
item = {"ident": "app:ingest|%s" % a["process"], "group": a.get("group") or ("VIDEO" if a["app"] == "arena" else "STREAM" if a["app"] == "obs" else "SHOW"),
"label": a["label"], "checks": [{"type": "process", "match": exe, "name": a["label"], "fix": fid}],
"fixes": {fid: start, "restart_" + fid: again}, "restart": [{"label": a["label"], "fix": "restart_" + fid}] if a.get("restart") else [],
"also": [], "fold": a.get("fold")}
out.append(item)
for ip in [(ing.get("wired") or {}).get("ip")] + list(ing.get("ips") or []) + ["127.0.0.1"]:
if ip:
apps_at.setdefault((a["app"], ip), item)
# Companion connections.
counts = {}
for conn in inv.get("connections") or []:
@@ -343,7 +367,7 @@ def layout(cfg, new_tiles, restart_items):
"""Put the keys on the Status page: everything fits, so no group keys; otherwise group keys along
the top row and the members after them (a member with no room left shows inside its group only)."""
fixed = {tuple(t["pos"]) for t in cfg["tiles"] if t.get("pos")}
if restart_items:
if cfg.get("restart_page"):
fixed.add(tuple(cfg["restart_page"]["jump_pos"]))
cells = free_cells(fixed)
groups = [g for g in GROUPS if any(t["_group"] == g for t in new_tiles)]
@@ -386,10 +410,10 @@ def place_new(cfg, tile):
def restart_layout(cfg, restart_items):
if not restart_items:
cfg.pop("restart_page", None)
"""The Restart page always exists (a campus set up before its computers joined gets their keys later)."""
rp = cfg.get("restart_page")
if not rp:
return
rp = cfg["restart_page"]
taken = {tuple(rp["back_pos"])} | {tuple(k["pos"]) for k in rp.get("keys", [])}
cells = free_cells(taken)
ids = {k["id"] for k in rp.get("keys", [])}
@@ -474,6 +498,18 @@ def extend(cfg, inv, seen):
continue
if any(c.get("params", {}).get("host") not in cfg["hosts"] for c in it["checks"] if c.get("template")):
continue
# A program whose Companion connection already has a key (Woonsocket's PROP got a second key):
# its own checks join that key.
home = next((t for t in cfg["tiles"] if any(("conn:" + c.get("conn", "")) in it["also"] for c in t.get("checks", []) if c.get("conn"))), None)
if home is not None:
for fid, f in it["fixes"].items():
cfg["fixes"].setdefault(fid, f)
home["checks"] += [c for c in it["checks"] if not c.get("conn")]
if it["restart"] and cfg.get("restart_page"):
restart_layout(cfg, it["restart"])
added.append("%s checks on the %s key" % (key_text(it["label"]), home["label"]))
now_seen += [it["ident"]] + it["also"]
continue
tid = slug(it["label"], taken)
taken.add(tid)
for fid, f in it["fixes"].items():