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
+54 -3
View File
@@ -142,6 +142,34 @@ class TestBuild(unittest.TestCase):
chk = {"type": "midi_peer", "host": "tracks", "session": "WS Tracks", "peer": "Lighting"}
self.assertEqual(sp.check_midi_peer(chk, snap, {"now": time.time()})[0][1].level, sp.OK) # no state: older macOS, configured
def test_a_program_on_the_companion_mac_gets_a_local_check_and_fixes(self):
"""Woonsocket: ProPresenter runs on the Mac that runs Companion."""
inv = small()
inv["computers"] = {}
exe = "/Applications/ProPresenter.app/Contents/MacOS/ProPresenter"
inv["ingest"].update(procs=["ProPresenter"], paths={"ProPresenter": "/Applications/ProPresenter.app"}, exes={"ProPresenter": exe})
inv["connections"] = [dict(c, host="10.4.1.1") if c["label"] == "ProP" else c for c in inv["connections"]]
cfg, _ = ac.build(BASE, inv, {"id": "ws", "name": "Woonsocket"}, "1234")
self.assertEqual(schema.validate(cfg)[0], [])
prop = next(t for t in cfg["tiles"] if t["label"] == "PROP")
self.assertEqual([(c["type"], c.get("match") or c.get("conn")) for c in prop["checks"]], [("conn", "ProP"), ("process", exe)])
fix = cfg["fixes"][prop["checks"][1]["fix"]]
self.assertEqual((fix["kind"], fix["run"]), ("local", "pgrep -xq ProPresenter || open /Applications/ProPresenter.app"))
self.assertIn(prop["checks"][1]["fix"], cfg["auto_fix"]["fixes"])
self.assertEqual([k["label"] for k in cfg["restart_page"]["keys"]], ["PROP"])
self.assertEqual(cfg["fixes"][cfg["restart_page"]["keys"][0]["fix"]]["kind"], "local")
def test_every_campus_gets_a_restart_page_and_north_attleboros_auto_fix_hours(self):
inv = small()
inv["computers"] = {}
cfg, _ = ac.build(BASE, inv, {"id": "sc", "name": "Southcoast"}, "1234")
self.assertEqual(cfg["restart_page"]["keys"], []) # computers join later: their keys come then
na = json.load(open(os.path.join(ROOT, "config.json")))
self.assertEqual(cfg["auto_fix"]["windows"], na["auto_fix"]["windows"])
self.assertEqual(cfg["service_windows"], na["service_windows"])
self.assertEqual(cfg["nav_keys"], na["nav_keys"])
make_page.build_restart(cfg)
def test_an_m32_is_called_m32(self):
inv = small()
inv["connections"] = [c for c in inv["connections"] if c["label"] != "x32"] + [
@@ -188,6 +216,21 @@ class TestExtend(unittest.TestCase):
again, added2, _ = ac.extend(new, inv, set(self.seen) | set(now_seen))
self.assertEqual(added2, [])
def test_a_program_found_later_joins_its_connections_key(self):
"""Woonsocket got a second PROP key when ProPresenter on the Companion Mac was found after the connection."""
inv = small()
inv["computers"] = {}
cfg, seen = ac.build(BASE, inv, {"id": "ws", "name": "Woonsocket"}, "1234")
exe = "/Applications/ProPresenter.app/Contents/MacOS/ProPresenter"
inv["ingest"].update(procs=["ProPresenter"], paths={"ProPresenter": "/Applications/ProPresenter.app"}, exes={"ProPresenter": exe})
inv["connections"] = [dict(c, host="127.0.0.1") if c["label"] == "ProP" else c for c in inv["connections"]]
new, added, _ = ac.extend(cfg, inv, seen)
self.assertEqual([t["label"] for t in new["tiles"]].count("PROP"), 1)
prop = next(t for t in new["tiles"] if t["label"] == "PROP")
self.assertEqual([c["type"] for c in prop["checks"]], ["conn", "process"])
self.assertIn("PROP checks on the PROP key", added)
self.assertEqual(schema.validate(new)[0], [])
def test_a_key_someone_took_off_is_not_added_back(self):
cfg = json.loads(json.dumps(self.cfg))
cfg["tiles"] = [t for t in cfg["tiles"] if t["label"] != "X32"]
@@ -338,12 +381,17 @@ class TestCampusesView(unittest.TestCase):
export = cv.build_overview(na, roster, 98, 99)
keys = [(r, c, ctl) for r, row in export["page"]["controls"].items() for c, ctl in row.items() if "'campus " in (ctl.get("options") or {}).get("notes", "")]
self.assertEqual([(r, c) for r, c, _ in keys], [("0", "1"), ("0", "2"), ("0", "3")])
rs = export["page"]["controls"]["1"]["3"] # WS's RESTART key, under its ALL key
acts = rs["steps"]["0"]["action_sets"]["up"]
self.assertEqual([a["options"].get("value", {}).get("value") for a in acts[:2]] + [acts[2]["options"]["page"]["value"]], ["WS", "RESTART", 99])
self.assertEqual(cv.campus_command([{"id": "rs_prop", "pos": [0, 1]}], "tap 0 1 1000;hold 0 1 1000", 1001, restart=True),
["force rs_prop 1000"]) # on its Restart page a hold forces, a tap does nothing
self.assertIn("'campus na'", keys[0][2]["options"]["notes"])
self.assertIn("page 19", keys[0][2]["options"]["notes"])
ws_key = next(k for _, _, k in keys if "'campus ws'" in k["options"]["notes"])
acts = ws_key["steps"]["0"]["action_sets"]["up"]
self.assertEqual([a["definitionId"] for a in acts], ["custom_variable_set_value", "set_page"])
self.assertEqual((acts[0]["options"]["value"]["value"], acts[1]["options"]["page"]["value"]), ("WS", 99))
self.assertEqual([a["definitionId"] for a in acts], ["custom_variable_set_value", "custom_variable_set_value", "set_page"])
self.assertEqual((acts[0]["options"]["value"]["value"], acts[1]["options"]["value"]["value"], acts[2]["options"]["page"]["value"]), ("WS", "", 99))
def test_a_campus_tells_the_hub_and_learns_nothing_back(self):
from settings import campuses as cv
@@ -365,7 +413,10 @@ class TestCampusesView(unittest.TestCase):
m = cv.Mirror(FakeSite())
data = m.sync_registry()
m.poll()
self.assertEqual(m.refresh_pages()["list"], {"ws": {"id": "ws", "name": "Woonsocket", "url": "http://10.5.1.1:8020"}})
listed = m.refresh_pages()["list"]
self.assertEqual({k: {f: v for f, v in e.items() if f != "user"} for k, e in listed.items()},
{"ws": {"id": "ws", "name": "Woonsocket", "url": "http://10.5.1.1:8020"}})
self.assertTrue(listed["ws"]["user"]) # the account the hub backs it up with
finally:
cv.http_json, cv.my_url = real, real_url
self.assertEqual(list(data["list"]), ["ws"]) # the others are forgotten