"Location", never "campus", in everything people read; SC FOH joined with its MIDI links

- 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>
This commit is contained in:
2026-10-21 13:42:22 -04:00
co-authored by Claude Opus 5.5
parent d0c755824b
commit 7068244ba4
31 changed files with 951 additions and 788 deletions
+7
View File
@@ -160,6 +160,13 @@ class TestPipeline(unittest.TestCase):
self.assertEqual(ap.history_config(self.data, entries[0]["id"])["debounce_seconds"], 3) # the version before
self.assertEqual(oct(os.stat(os.path.join(self.data, "history", entries[0]["id"] + ".json")).st_mode)[-3:], "600")
def test_settings_edited_in_place_still_restart_the_watchdog(self):
cfg = self.site.load() # the cached object, edited as a script would
cfg["debounce_seconds"] = 5
self.run_apply(cfg, "in place")
names = {s["name"]: s["state"] for s in self.applier.status["steps"]}
self.assertEqual(names["restart"], "ok")
def test_page_only_data_needs_no_restart(self):
new = json.loads(json.dumps(self.cfg))
new["gear"] = {"UHDJr": {"location": "lobby"}}
+16 -12
View File
@@ -1,5 +1,5 @@
"""Auto-setup for the other campuses: an inventory becomes keys that pass every rule, laid out like
North Attleboro's; a running campus only gains what is new (never loses anything, never gets back
"""Auto-setup for the other locations: an inventory becomes keys that pass every rule, laid out like
North Attleboro's; a running location only gains what is new (never loses anything, never gets back
what someone took off); a computer's join report is checked; the lines people paste are right."""
import io
import json
@@ -356,7 +356,11 @@ class TestRoutes(unittest.TestCase):
self.assertNotIn("outlets.json", names)
status, body = get(self.port, "/campus/install?id=ws")
self.assertIn("--site ws", body.decode())
self.assertIn("SUPPORT='ssh-ed25519 AAAAC3NzaTESTKEY", body.decode()) # this campus can help the new one
self.assertIn("SUPPORT='ssh-ed25519 AAAAC3NzaTESTKEY", body.decode()) # this location can help the new one
status, body = get(self.port, "/location/install?id=ws") # the address people see now
self.assertEqual(status, 200)
self.assertIn("/location/program.tgz", body.decode())
self.assertIn("settings.location setup", body.decode())
def test_look_now_needs_the_code(self):
self.assertEqual(get(self.port, "/api/discover/now", {})[0], 401)
@@ -367,7 +371,7 @@ if __name__ == "__main__":
class TestCampusesView(unittest.TestCase):
"""North Attleboro alone shows every campus: a copy of each status page (view only) and a CAMPUSES page."""
"""North Attleboro alone shows every location: a copy of each status page (view only) and a LOCATIONS page."""
def test_the_campus_page_is_view_only_cells_with_a_way_back(self):
from settings import campuses as cv
@@ -375,10 +379,10 @@ class TestCampusesView(unittest.TestCase):
na = json.load(open(os.path.join(ROOT, "config.json")))
export = cv.build_view(na, 99, 98)
ctrls = export["page"]["controls"]
self.assertEqual(export["page"]["name"], "99\\nCampus")
self.assertIn("CAMPUSES", json.dumps(ctrls["3"]["0"])) # where ALARMS sits on every status page
self.assertIn("$(custom:sp_view)", json.dumps(ctrls["3"]["0"]["style"])) # it says which campus is shown
cells = [ctl for row in ctrls.values() for ctl in row.values() if "campus view" in (ctl.get("options") or {}).get("notes", "")]
self.assertEqual(export["page"]["name"], "99\\nLocation")
self.assertIn("LOCATIONS", json.dumps(ctrls["3"]["0"])) # where ALARMS sits on every status page
self.assertIn("$(custom:sp_view)", json.dumps(ctrls["3"]["0"]["style"])) # it says which location is shown
cells = [ctl for row in ctrls.values() for ctl in row.values() if "location view" in (ctl.get("options") or {}).get("notes", "")]
self.assertEqual(len(cells), 28)
acts = cells[0]["steps"]["0"]["action_sets"]
self.assertEqual(sorted(acts), ["2000", "down", "up"]) # tap = acknowledge, hold = fix, there
@@ -404,16 +408,16 @@ class TestCampusesView(unittest.TestCase):
na = json.load(open(os.path.join(ROOT, "config.json")))
roster = {"na": {"name": "North Attleboro"}, "ws": {"name": "Woonsocket"}, "ab": {"name": "Apollo Beach"}}
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", "")]
keys = [(r, c, ctl) for r, row in export["page"]["controls"].items() for c, ctl in row.items() if "'location " 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("'location 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"])
ws_key = next(k for _, _, k in keys if "'location 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", "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))
@@ -462,7 +466,7 @@ class TestCampusHello(TestRoutes):
class TestStandalone(unittest.TestCase):
"""Apollo Beach runs its own panel (2026-10-21): it tells nobody, keeps no campus list, backs itself up."""
"""Apollo Beach runs its own panel (2026-10-21): it tells nobody, keeps no location list, backs itself up."""
def test_a_standalone_campus_tells_nobody_and_is_no_hub(self):
from settings import campuses as cv
+5 -5
View File
@@ -19,7 +19,7 @@ sp.log.addHandler(logging.NullHandler())
sp.log.propagate = False
CONFIG = sp.load_config(os.path.join(ROOT, "config.json"))
# The tests pin their own READY hours (the campus sets its own on the settings page).
# The tests pin their own READY hours (the location sets its own on the settings page).
CONFIG["ready"]["due"] = [{"days": ["sat"], "start": "12:00", "end": "23:59"}, {"days": ["sun"], "start": "00:00", "end": "13:30"}]
RC = CONFIG["ready"]
WEDNESDAY = datetime(2026, 10, 14, 15, 0).timestamp() # the week before the plan: gaps are amber
@@ -556,7 +556,7 @@ if __name__ == "__main__":
class TestOtherCampuses(unittest.TestCase):
"""The other campuses play the same songs: their plans get North Attleboro's timecode, in its order."""
"""The other locations play the same songs: their plans get North Attleboro's timecode, in its order."""
CAMPUS = {"name": "Apollo Beach", "short": "AB", "service_type": "1113969"}
def campus_plan(self, titles, notes=None):
@@ -569,7 +569,7 @@ class TestOtherCampuses(unittest.TestCase):
na = rd.song_ltc(RC, plan(), ableton())
titles = [s["title"] for s in na]
self.assertGreaterEqual(len(titles), 3)
# the campus plays song 2 first, then 1, then 3: song 1 breaks the order and is skipped; a song NA does not play is skipped too
# the location plays song 2 first, then 1, then 3: song 1 breaks the order and is skipped; a song NA does not play is skipped too
cp = self.campus_plan([titles[1], titles[0], titles[2], "Something Else"])
res = rd.campus_ltc(RC, plan(), na, self.CAMPUS, cp)
self.assertEqual([s["title"] for s in res["songs"]], [titles[1], titles[2]])
@@ -582,7 +582,7 @@ class TestOtherCampuses(unittest.TestCase):
self.assertEqual(rd.eval_campus_ltc(RC, rd.campus_ltc(RC, plan(), na, self.CAMPUS, right), rd.Line()).faults, [])
def test_a_song_without_home_timecode_is_matched_but_not_faulted_or_written(self):
"""The set is not on Tracks yet: the campus's songs are still in order, nothing is wrong at the campus, nothing is written."""
"""The set is not on Tracks yet: the location's songs are still in order, nothing is wrong at the location, nothing is written."""
p = plan()
cp = self.campus_plan([it["title"] for it in rd.plan_songs(RC, p)])
res = rd.campus_ltc(RC, p, [], self.CAMPUS, cp)
@@ -607,4 +607,4 @@ class TestOtherCampuses(unittest.TestCase):
self.assertTrue(rd.run_action(cfg, "ltc_to_pco", sleep=lambda s: None))
finally:
rd.http_json, rd.pco_get, rd.pco_login = real_http, real_get, real_login
self.assertEqual(len([c for c in calls if "/1113969/" in c[1] and c[0] == "POST"]), len(res["songs"])) # one note per campus song
self.assertEqual(len([c for c in calls if "/1113969/" in c[1] and c[0] == "POST"]), len(res["songs"])) # one note per location song
+1 -1
View File
@@ -295,7 +295,7 @@ class TestSchemaMatchesCode(unittest.TestCase):
def test_phrases(self):
cfg = sp.load_config(os.path.join(ROOT, "config.json"))
p = schema.phrases(cfg)
self.assertEqual(p["auto_fix"], "may run by itself " + sp.windows_phrase(cfg["auto_fix"]["windows"])) # the campus's own hours
self.assertEqual(p["auto_fix"], "may run by itself " + sp.windows_phrase(cfg["auto_fix"]["windows"])) # the location's own hours
self.assertEqual(schema.phrases(dict(cfg, auto_fix={"windows": [{"days": ["sun"], "start": "06:30", "end": "13:30"}]}))["auto_fix"],
"may run by itself on Sundays 06:30-13:30")
self.assertEqual(p["live"], "changes wait while a HyperDeck is recording")
+1 -1
View File
@@ -227,7 +227,7 @@ class TestServer(unittest.TestCase):
self.assertIn("config.json", z.namelist())
bundle_cfg = json.loads(z.read("config.json"))
self.assertEqual(bundle_cfg["site"]["editing_code"], self.CODE) # the bundle is the real file
# a new campus from that bundle: this campus's own site block stays, computers are flagged
# a new location from that bundle: this location's own site block stays, computers are flagged
other = json.loads(json.dumps(bundle_cfg)); other["site"]["name"] = "Woonsocket"; other["site"]["editing_code"] = "theirs"
buf = io.BytesIO()
with zipfile.ZipFile(buf, "w") as z2:
+2 -2
View File
@@ -1,5 +1,5 @@
"""The generated set-up scripts and the campus template: every value comes from the record, the
templates hold no site-shaped value, and the empty campus validates and builds its pages."""
"""The generated set-up scripts and the location template: every value comes from the record, the
templates hold no site-shaped value, and the empty location validates and builds its pages."""
import json
import os
import sys
+1 -1
View File
@@ -30,7 +30,7 @@ CONFIG["tiles"] = [dict(t, label="SERVICE") if t["id"] == "service" else t for t
# A Sunday 08:00 (inside the service window) and a Wednesday 15:00 (outside it).
SUNDAY = datetime(2026, 10, 11, 8, 0).timestamp()
CONFIG["auto_fix"]["log_dir"] = tempfile.mkdtemp(prefix="sp-autolog-") # never the real Desktop
# ...and their own auto-fix hours, so the campus can change its own on the settings page.
# ...and their own auto-fix hours, so the location can change its own on the settings page.
CONFIG["auto_fix"]["windows"] = [{"days": ["wed"], "start": "16:00", "end": "21:00"}, {"days": ["sun"], "start": "06:30", "end": "13:30"}]
WEDNESDAY = datetime(2026, 10, 7, 15, 0).timestamp()