Wiring view: every machine and what connects it (Companion, MIDI, Dante, watchdog, web, Art-Net), hide types or single links, scripts per computer

RESI LIVE NOT REC follows the Resi auto-record service (custom autorec_status, not during its REC hold; stale heartbeat = no verdict);
INGEST watches AUTO REC (running, heartbeat, Resi reachable), hold restarts it. Probes list each computer's scripts by file name only.
HyperDeck addresses read from bonjourHost.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-27 13:23:02 -04:00
co-authored by Claude Opus 5.5
parent 6c78ed3221
commit 26804e4ce2
11 changed files with 655 additions and 43 deletions
+53
View File
@@ -102,6 +102,40 @@ class TestServer(unittest.TestCase):
self.assertEqual(web["light"], "ok") # from snapshot.json, not a probe
self.assertIn("mac", o["setup_steps"])
def test_wiring_shows_machines_their_connections_and_scripts(self):
# 2026-10-27: "like a wiring diagram ... how many and what scripts are running on each machine"
live = {"tracks": {"alias": "tracks", "reachable": True, "time": time.time(), "items": [],
"found": {"scripts": [{"name": "statuspanel.py", "pid": 5, "by": "", "running": True}]}}}
json.dump(live, open(os.path.join(self.data, "machines-live.json"), "w"))
snap = json.load(open(os.path.join(self.data, "snapshot.json")))
snap.update({"conns": {"ATEM": {"category": "good", "enabled": True}, "x32": {"category": "error", "enabled": True},
"HyperDeck_1": {"category": "good", "enabled": True}, "tcp-HyperDeck-1": {"category": "good", "enabled": True}},
"conn_hosts": {"ATEM": "10.1.10.2", "x32": "10.1.1.112", "HyperDeck_1": "10.1.10.11", "tcp-HyperDeck-1": "10.1.10.11"},
"remote_midi": {"tracks": {"NA Tracks": {"peers": [{"address": "10.1.13.140:5004", "name": "SuperRack-LiveBox", "state": "running"}]}}},
"dante_map": {"NA-Tracks": {"switch": "10.66.1.100", "port": "0/6", "seen": time.time()},
"FOH-SuperRack-LiveBox": {"switch": "10.66.1.102", "port": "0/9", "seen": time.time()},
"STUDIO-B-MON": {"switch": "10.66.1.100", "port": "0/2", "seen": time.time()}}})
path = os.path.join(self.data, "snapshot.json")
old = open(path).read()
try:
json.dump(snap, open(path, "w"))
w = json.loads(get(self.port, "/api/wiring")[1])
finally:
open(path, "w").write(old)
os.remove(os.path.join(self.data, "machines-live.json"))
nodes = {n["id"]: n for n in w["nodes"]}
edges = [(e["type"], e["from"], e["to"], e["light"]) for e in w["edges"]]
self.assertEqual(w["me"], "h:ingest")
self.assertEqual(nodes["h:tracks"]["scripts"][0]["name"], "statuspanel.py")
self.assertIsNone(nodes["h:prop"]["scripts"]) # not looked yet
self.assertIn(("companion", "h:ingest", "d:10.1.1.112", "problem"), edges)
self.assertEqual(sorted(nodes["d:10.1.10.11"]["names"]), ["HyperDeck_1", "tcp-HyperDeck-1"]) # one deck, one box
self.assertIn(("midi", "h:tracks", "d:10.1.13.140", "ok"), edges)
self.assertIn(("dante", "h:tracks", "sw:10.66.1.100", "ok"), edges) # NA-Tracks is the Tracks Mac
self.assertIn("FOH-SuperRack-LiveBox", nodes["d:10.1.13.140"]["names"]) # same box as its MIDI peer
self.assertIn("d:studiobmon", nodes) # a monitor, not the Studio B Mac
self.assertEqual({t["id"] for t in w["types"]} >= {"companion", "midi", "watch", "dante"}, True)
def test_gear_and_keys_and_pages(self):
gear = json.loads(get(self.port, "/api/gear")[1])
self.assertTrue(any(r["name"] == "HyperDeck_2" and "hd2" in r["watched_by"] for r in gear))
@@ -362,6 +396,25 @@ class TestBindingAndProbes(unittest.TestCase):
self.assertEqual(by["sshd"]["light"], "ok")
self.assertEqual((setup["profile"], setup["wake_on_lan"], setup["firewall_allow"]), ("Public", "Enabled", "10.1.1.1,10.6.3.222"))
self.assertIn("\\StatusPanel\\Start Arena", found["tasks"])
scripts = {x["name"]: x["running"] for x in found["scripts"]}
self.assertEqual((scripts["Start Vista"], scripts["Start rtpMIDI"]), (True, False)) # Vista.exe runs; rtpMIDI.exe does not
def test_scripts_are_listed_by_file_name_only(self):
agents = {"org.waterschurch.prop-guard": 412, "org.waterschurch.status-panel": None, "com.apple.x": 9}
lines = ["412 /usr/bin/python3 /Users/m/Library/Application Support/status-panel/prop_guard.py /Users/m/cfg --token=SECRET",
"77 /bin/bash /Users/m/backup/backup-campuses.sh", "55 /Applications/Companion.app/Contents/Resources/node main.js",
"60 node /Users/m/Library/Application Support/companion/modules/x/main.js", "9 zsh -c echo SP_SCRIPTS",
"88 /Applications/Xcode.app/Contents/Developer/Library/Frameworks/Python3.framework/Versions/3.9/Resources/"
"Python.app/Contents/MacOS/Python /Users/m/Library/Application Support/resi-autorec/resi_autorec.py"]
got = probes.parse_scripts(lines, agents)
self.assertEqual([(x["name"], x["by"], x["running"]) for x in got],
[("backup-campuses.sh", "", True), ("prop_guard.py", "org.waterschurch.prop-guard", True),
("resi_autorec.py", "", True),
("status-panel", "org.waterschurch.status-panel", False)])
self.assertNotIn("SECRET", json.dumps(got)) # never the rest of the line
win = probes.parse_scripts(['812 powershell.exe -File "C:\\ProgramData\\StatusPanel\\start-vista.ps1"'],
tasks={"\\StatusPanel\\Start Vista": "Running", "\\StatusPanel\\Start rtpMIDI": "Ready"})
self.assertEqual([(x["name"], x["running"]) for x in win], [("Start Vista", True), ("start-vista.ps1", True), ("Start rtpMIDI", False)])
def test_probe_commands_never_carry_names_from_the_settings(self):
cfg = json.loads(json.dumps(CONFIG))
+36 -18
View File
@@ -76,7 +76,8 @@ def healthy_snapshot(now):
snap.ping = {"10.1.3.227": 0, "10.6.7.13": 0, "10.1.0.1": 0}
snap.link = True
snap.launchd = {"org.waterschurch.resi-propresenter": 884, "org.waterschurch.lobby-captions": 900,
"org.waterschurch.lobby-captions-display": 901, "org.waterschurch.digico-relay": 902}
"org.waterschurch.lobby-captions-display": 901, "org.waterschurch.digico-relay": 902,
"org.waterschurch.resi-autorec": 903}
snap.launchd_time = now
snap.procs = {"/Applications/ProdDash.app/Contents/MacOS/ProdDash": True}
snap.remote = {"studiob\0pgrep -xq 'Pro Tools'": True} # the failover's check has a verdict
@@ -488,12 +489,14 @@ class TestChecks(unittest.TestCase):
db.execute("CREATE TABLE instances (id STRING, value STRING)")
rows = [("a", {"label": "Axient_1", "config": {"host": "10.1.1.101", "port": 2202}}),
("b", {"label": "OBS-Youtube", "config": {"host": " 10.1.9.126 "}}),
("c", {"label": "no-host", "config": {}}), ("d", "not json")]
("c", {"label": "no-host", "config": {}}), ("d", "not json"),
("e", {"label": "HyperDeck_1", "config": {"bonjourHost": "10.1.10.11:9993"}})]
db.executemany("INSERT INTO instances VALUES (?, ?)",
[(i, v if isinstance(v, str) else json.dumps(v)) for i, v in rows])
db.commit()
db.close()
self.assertEqual(sp.companion_conn_hosts(path), {"Axient_1": "10.1.1.101", "OBS-Youtube": "10.1.9.126"})
self.assertEqual(sp.companion_conn_hosts(path), {"Axient_1": "10.1.1.101", "OBS-Youtube": "10.1.9.126",
"HyperDeck_1": "10.1.10.11"})
self.assertEqual(sp.companion_conn_hosts(os.path.join(d, "missing.sqlite")), {})
def test_fx_lights_midi_on_the_tracks_mac(self):
@@ -1517,43 +1520,58 @@ class TestFixes(unittest.TestCase):
self.assertEqual(self.plan("rec"), [("enable", "HyperDeck_1")])
self.assertEqual(self.plan("x32"), [("enable", "x32")])
def autorec(self, status, hb=None):
"""What the Resi auto-record service publishes (custom variables)."""
self.snap.vars[("custom", "autorec_status")] = status
self.snap.vars[("custom", "autorec_hb")] = str(int(SUNDAY + 100000 if hb is None else hb))
def test_resi_live_and_no_deck_recording_is_red_and_a_hold_starts_them(self):
# 2026-10-27: "notify me if we are not recording on the hyperdecks, but resi is playing"
self.snap.vars[("resi-studio-cues", "playback_state")] = "PLAYING"
self.autorec("AUTO REC\nLIVE, NOT\nRECORDING")
self.snap.vars[("HyperDeck_2", "slotId")] = "-" # optional deck, no disk: not in use
self.snap.vars[("HyperDeck_2", "recordingTime")] = "--:--:--"
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 30)
self.assertEqual(level(tiles["hd1"]), "ok") # not yet: Resi just started
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 30, 40)
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 10)
self.assertEqual(level(tiles["hd1"]), "ok") # not yet: give it a moment
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 10, 30)
self.assertEqual(tiles["hd1"]["x"], "HD1\nRESI LIVE NOT REC")
self.assertEqual(level(tiles["rec"]), "err")
self.assertEqual(level(tiles["hd2"]), "off")
self.snap.vars[("HyperDeck_3", "status")] = "Record" # once one records, the others are NOT REC
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 70, 4)
self.autorec("AUTO REC\nRECORDING\n9:30 AM")
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 40, 4)
self.assertEqual(tiles["hd1"]["x"], "HD1\nNOT REC")
started = []
self.engine.record_executor = lambda ip, name: started.append((ip, name)) or True
self.engine.run_fixes("rec", self.snap, None, SUNDAY + 80)
self.engine.run_fixes("rec", self.snap, None, SUNDAY + 50)
time.sleep(0.2)
self.assertEqual([ip for ip, _ in started], ["10.1.10.11", "10.1.10.14", "10.1.10.15", "10.1.10.16"])
self.assertTrue(started[0][1].startswith("CAM3-") and started[0][1].endswith("-"))
self.assertTrue(started[3][1].startswith("PGM4-"))
self.assertEqual(self.engine.notes["rec"]["text"], "STARTING\nREC")
def test_resi_paused_or_its_module_down_is_not_live(self):
self.snap.vars[("resi-studio-cues", "playback_state")] = "PAUSED"
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 70)
def test_not_live_while_auto_rec_waits_or_is_stuck(self):
self.autorec("AUTO REC\nREC AT\n9:20 AM") # live early: REC held until 10 min before
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 40)
self.assertEqual(level(tiles["hd1"]), "ok")
self.snap.vars[("resi-studio-cues", "playback_state")] = "PLAYING"
self.snap.conns["resi-studio-cues"]["category"] = "error"
run_steps(self.engine, self.snap, SUNDAY + 70, 70)
self.autorec("AUTO REC\nLIVE, NOT\nRECORDING", hb=SUNDAY - 600) # a stale verdict counts for nothing...
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 40, 200)
self.assertFalse(self.engine.resi_live)
self.assertEqual(tiles["ingest"]["x"], "INGEST\nAUTO REC STUCK") # ...and says the service is stuck
ran = []
self.engine.executor = lambda cfg, fix_id, chk: ran.append(chk.get("conn")) or True
self.engine.executor = lambda cfg, fix_id, chk: ran.append(fix_id) or True
self.engine.record_executor = lambda ip, name: self.fail("must not record when Resi is not live")
self.engine.run_fixes("hd1", self.snap, None, SUNDAY + 150) # the usual hold: reconnect
self.engine.run_fixes("ingest", self.snap, None, SUNDAY + 250) # the hold restarts the service
time.sleep(0.2)
self.assertEqual(ran, ["HyperDeck_1"])
self.assertEqual(ran, ["autorec"])
def test_auto_rec_not_running_or_without_resi(self):
del self.snap.launchd["org.waterschurch.resi-autorec"]
tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 6)
self.assertEqual(tiles["ingest"]["x"], "INGEST\nAUTO REC NOT LOADED")
self.snap.launchd["org.waterschurch.resi-autorec"] = 903
self.autorec("AUTO REC\nRESI ERROR")
tiles, _ = run_steps(self.engine, self.snap, SUNDAY + 6, 200) # amber only once it lasts 3 minutes
self.assertEqual((tiles["ingest"]["x"], level(tiles["ingest"])), ("INGEST\nAUTO REC NO RESI", "warn"))
def test_not_recording_is_not_fixed_by_a_reboot(self):
for i in (1, 3):