ALARMS and AUTO-FIX as two keys; LIGHTS at Southcoast hands the lights to an operator

- kind autofix: its own switch on the auto-fix hours; ALARMS then only silences (NA keeps the old
  combined key until it has room for one)
- kind bypass: hold takes every network MIDI link to a peer out (here and on listed Macs), checks stop
  calling it lost, re-taken every 10 s; hold again puts back exactly what was taken plus every watched link
- a MIDI peer with no address is never called (it connected SC Tracks to itself)
- holds from NA's CAMPUS page flip switch keys; a rebuild keeps them; new campuses get AUTO-FIX
- key kind changes restart the watchdog; settings page has separate alarm / auto-fix buttons
- WS and SC configs: AUTO-FIX at 3,1; SC LIGHTS at 1,3; NA config synced from live

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 13:33:48 -04:00
co-authored by Claude Opus 5.5
parent 2f8e3d47a3
commit d0c755824b
15 changed files with 605 additions and 44 deletions
+15
View File
@@ -531,3 +531,18 @@ class TestStandalone(unittest.TestCase):
self.assertIn("--standalone", inst)
self.assertNotIn("10.1.1.1", inst)
self.assertNotIn("authorized_keys", inst) # no one else's key is let in
class TestSwitchKeys(unittest.TestCase):
def test_a_new_campus_gets_auto_fix_by_alarms_and_a_rebuild_keeps_its_own_switches(self):
new, _ = ac.build(BASE, small(), {"id": "sc", "name": "Southcoast"}, "1234")
af = [t for t in new["tiles"] if t.get("kind") == "autofix"]
self.assertEqual([t["pos"] for t in af], [[3, 1]]) # beside ALARMS
self.assertEqual(schema.validate(new)[0], [])
old = json.loads(json.dumps(new))
old["tiles"].append({"id": "lights", "label": "LIGHTS", "kind": "bypass", "pos": [2, 7], "bypass": {"peers": ["Lighting"]}})
again, _ = ac.build(BASE, small(), {"id": "sc", "name": "Southcoast"}, "1234")
kept_cfg, kept = ac.keep_from(again, old)
lights = [t for t in kept_cfg["tiles"] if t["id"] == "lights"]
self.assertEqual((len(lights), lights[0]["pos"], "LIGHTS" in kept), (1, [2, 7], True))
self.assertEqual(len([t for t in kept_cfg["tiles"] if t.get("kind") == "autofix"]), 1)
+1 -1
View File
@@ -85,7 +85,7 @@ class TestSites(unittest.TestCase):
errors, _ = schema.validate(cfg)
self.assertEqual(errors, [])
make_page.build(cfg)
self.assertEqual(sorted(t["kind"] for t in cfg["tiles"]), ["master", "network", "service"])
self.assertEqual(sorted(t["kind"] for t in cfg["tiles"]), ["autofix", "master", "network", "service"])
self.assertEqual([a for a, h in cfg["hosts"].items() if h.get("self")], ["ingest"])
self.assertEqual(cfg["hosts"]["ingest"]["networks"], [])
self.assertNotIn("Attleboro", json.dumps(cfg))
+126
View File
@@ -2451,3 +2451,129 @@ class TestDanteNewcomers(unittest.TestCase):
self.assertFalse(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 100))
self.engine.any_recording = False
self.assertTrue(sp.watch_dante_newcomers(self.snap, CONFIG, self.comp, self.engine, now + 200))
class TestSwitches(unittest.TestCase):
"""ALARMS and AUTO-FIX as two keys (2026-10-21), and a key that hands a device to an operator
(Southcoast's LIGHTS: a lighting op runs the desk, so nothing may send it MIDI)."""
def setUp(self):
self.snap = healthy_snapshot(SUNDAY)
self.cfg = json.loads(json.dumps(CONFIG))
arena = next(t for t in self.cfg["tiles"] if t.get("pos") == [3, 7])
arena.pop("pos") # NA's page is full: make room
self.cfg["tiles"].append({"id": "autofix", "label": "AUTO-FIX", "kind": "autofix", "pos": [3, 7]})
self.midi = next(c for t in self.cfg["tiles"] for c in t.get("checks", []) if c["type"] == "midi_peer")
self.cfg["tiles"].append({"id": "lights", "label": "LIGHTS", "kind": "bypass",
"bypass": {"peers": [self.midi["peer"]], "hosts": [""], "conns": ["x32"]}})
self.state = os.path.join(tempfile.mkdtemp(), "state.json")
self.engine = self.new_engine()
def new_engine(self):
e = sp.Engine(self.cfg, self.state)
e.executor = e.poe_executor = lambda *a: True
e.autolog = sp.AutoLog(tempfile.mkdtemp())
return e
def test_alarms_silence_only_and_auto_fix_has_its_own_key(self):
import schema
self.assertEqual(schema.validate(self.cfg)[0], [])
ran = []
self.engine.executor = lambda cfg, fix_id, chk: ran.append(fix_id) or True
self.snap.remote["tracks\0pgrep -xq Live"] = False # Ableton closed
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 4)
self.assertEqual(tiles["service"]["x"], "SERVICE\nON")
self.assertEqual(tiles["autofix"]["x"], "AUTO-FIX\nON\nTIL 1:30PM") # Sunday's hours end then
self.engine.service.set(False, SUNDAY + 5) # ALARMS: silence only
run_steps(self.engine, self.snap, SUNDAY + 5, 25)
self.engine.auto_fix(self.snap, mock_comp(), SUNDAY + 30)
time.sleep(0.2)
self.assertEqual(ran, ["ableton"]) # auto-fix carried on
self.engine.autofix.set(False, SUNDAY + 40) # AUTO-FIX pressed: paused
tiles = self.engine.step(self.snap, SUNDAY + 41)
self.assertEqual(tiles["autofix"]["x"], "AUTO-FIX\nOFF\nTIL WED 4PM") # back on with Wednesday's hours
self.assertEqual(level(tiles["autofix"]), "off")
self.assertEqual(self.engine.auto_fix(self.snap, mock_comp(), SUNDAY + 200), [])
self.assertFalse(self.new_engine().autofix.on(SUNDAY + 300)) # survives a restart
self.assertTrue(self.engine.autofix.on(datetime(2026, 10, 14, 16, 30).timestamp())) # Wednesday's hours: back by itself
weekday = SUNDAY - 3 * 86400 # Thursday: pressed = runs anyway
self.engine.autofix.set(True, weekday)
self.assertTrue(self.engine.autofix.on(weekday + 60))
page = make_page.build(self.cfg)["page"]["controls"]
notes = {(ctl.get("options") or {}).get("notes", "") for row in page.values() for ctl in row.values() if ctl}
self.assertTrue(any("Auto-fix is the AUTO-FIX key" in n for n in notes))
self.assertTrue(any("pause auto-fix" in n for n in notes))
def test_a_hold_from_north_attleboro_flips_a_switch(self):
self.engine.run_fixes("autofix", self.snap, mock_comp(), SUNDAY)
self.assertFalse(self.engine.autofix.on(SUNDAY + 1))
self.engine.run_fixes("autofix", self.snap, mock_comp(), SUNDAY + 2)
self.assertTrue(self.engine.autofix.on(SUNDAY + 3))
def test_the_operator_key_takes_the_midi_out_and_puts_it_back(self):
calls = []
here = {self.midi["session"]: {"port": 5004, "peers": [{"name": self.midi["peer"], "address": self.midi["address"]},
{"name": "Other", "address": "10.9.9.9:5004"}]}}
real = (sp.midi_sessions, sp.midi_peer)
sp.midi_sessions = lambda cfg, host: here
sp.midi_peer = lambda cfg, host, action, session, peer, address=None, repoint=False: calls.append((action, host, session, peer)) or ("asked", None)
class Comp:
def __init__(self):
self.on = True
self.log = []
def connections(self):
return [{"label": "x32", "id": "id-x32", "enabled": self.on}]
def disable_connection(self, cid):
self.on = False; self.log.append("off"); return True
def enable_connection(self, cid):
self.on = True; self.log.append("on"); return True
comp = Comp()
try:
if self.midi.get("host"):
self.snap.remote_midi[self.midi["host"]] = {} # its Mac reports: no peers
self.snap.remote_midi_time[self.midi["host"]] = SUNDAY + 100
else:
self.snap.midi, self.snap.midi_time = {self.midi["session"]: {"port": 5004, "peers": []}}, SUNDAY + 100
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 100, 3)
self.assertEqual(level(tiles["lights"]), "ok")
self.engine.run_fixes("lights", self.snap, comp, SUNDAY + 104) # hold: an operator has it
for _ in range(50):
if not self.engine.bypass_busy:
break
time.sleep(0.05)
self.assertEqual(calls, [("disconnect", "", self.midi["session"], self.midi["peer"])]) # never "Other"
self.assertEqual(comp.log, ["off"])
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 105, 3)
midi_tile = next(t["id"] for t in self.cfg["tiles"] if self.midi in t.get("checks", []))
tag = self.midi.get("tag") or self.midi["peer"]
self.assertNotIn(tag + " MIDI", tiles[midi_tile]["x"]) # taken out on purpose: no alarm
self.assertEqual(tiles["lights"]["x"], "LIGHTS\nTAKING\nMIDI OUT")
tiles = self.engine.step(self.snap, SUNDAY + 120)
self.assertEqual(tiles["lights"]["x"], "LIGHTS\nOPERATOR\nMIDI OFF")
self.assertIn("lights", self.new_engine().bypass) # survives a restart
calls.clear()
self.engine.run_fixes("lights", self.snap, comp, SUNDAY + 130) # hold again: put back
for _ in range(50):
if not self.engine.bypass_busy:
break
time.sleep(0.05)
self.assertEqual(calls[0], ("connect", "", self.midi["session"], self.midi["peer"])) # what was taken out
self.assertEqual({c[0] for c in calls}, {"connect"}) # and every link the checks watch
self.assertEqual(comp.log, ["off", "on"])
self.assertNotIn("lights", self.engine.bypass)
finally:
sp.midi_sessions, sp.midi_peer = real
def test_a_peer_without_an_address_is_never_called(self):
res, why = sp.midi_peer(self.cfg, "", "connect", "SC Tracks NEW", "Lighting", ":5004")
self.assertIsNone(res)
self.assertIn("calls in", why)
def test_a_bypass_key_needs_its_peers(self):
import schema
bad = json.loads(json.dumps(self.cfg))
next(t for t in bad["tiles"] if t["id"] == "lights")["bypass"] = {"hosts": ["nowhere"]}
errors = schema.validate(bad)[0]
self.assertTrue(any("bypass.peers" in e for e in errors))
self.assertTrue(any("nowhere" in e for e in errors))