From 0b38e9247b7a5bcc538d496e5b21ccf2ac1a2b1c Mon Sep 17 00:00:00 2001 From: christian Date: Thu, 8 Oct 2026 18:57:04 -0400 Subject: [PATCH] Restart keys flash when their thing is down; Ableton reopens its last set - A Restart page key flashes red "HOLD TO FIX" while any check that its fix (or a fix/connection in its "watch") covers is down, e.g. DVS TRACKS when DANTE misses NA-Tracks. - Ableton's start and restart reopen the set Live last opened (read from its Log.txt before quitting; templates skipped) instead of the default template. Tested live: ABLETON and TRANSLATE restart keys. Co-Authored-By: Claude Opus 5.5 --- README.md | 4 ++-- config.json | 16 ++++++++-------- status_panel.py | 25 +++++++++++++++++++++++++ tests/test_status_panel.py | 23 +++++++++++++++++++++++ 4 files changed, 58 insertions(+), 10 deletions(-) diff --git a/README.md b/README.md index c3b4732..459876c 100644 --- a/README.md +++ b/README.md @@ -68,13 +68,13 @@ Row 0 holds the group buttons. Each one shows the first problem in its group, pl ## Restart page (page 20) -Tap **RESTART PAGE** on the status page (row 2). Each key restarts one thing **even when the status page says it is fine**; hold it 2 s. It shows RESTARTING, then DONE (or FAILED / what to do). The same thing will not run twice within a minute. **STATUS PAGE** (bottom left) goes back. +Tap **RESTART PAGE** on the status page (row 2). Each key restarts one thing **even when the status page says it is fine**; hold it 2 s. A key **flashes red (HOLD TO FIX)** while the thing it restarts is down, so you can see which one to hold. It shows RESTARTING, then DONE (or FAILED / what to do). The same thing will not run twice within a minute. **STATUS PAGE** (bottom left) goes back. | Key | Restarts | |---|---| | DVS PROP / TRACKS / TRANSL / INGEST | Dante Virtual Soundcard's engine on that Mac (audio to and from it drops 5–10 s). DVS INGEST also restarts ProdCom afterwards, which otherwise keeps a dead input. | | PROPRES | ProPresenter (quits and reopens the newest version) | -| ABLETON | Ableton on the Tracks Mac (quits it: unsaved set changes are lost, and Live may ask to recover - click it on the Tracks Mac) | +| ABLETON | Ableton on the Tracks Mac, reopening the set it had open (from Live's own log; templates skipped). Unsaved changes are lost, and Live may ask to recover - click it on the Tracks Mac. APPS's hold, which starts a closed Ableton, opens that set too. | | TRANSLATE | Church Translate on NA Translation | | PRODCOM | ProdCom on NA Ingest (lobby captions blank for 5–15 s) | | PRODDASH / CAPTIONS / BRIDGE | that service on NA Ingest | diff --git a/config.json b/config.json index 5387384..6b2bc4e 100644 --- a/config.json +++ b/config.json @@ -40,8 +40,8 @@ "fixes": { "propresenter": { "host": "prop", "short": "PROPRES", "label": "restart ProPresenter (newest installed)", "run": "APP=$(ls -d /Applications/ProPresenter*.app | sort -V | tail -1); if pgrep -xq ProPresenter; then pkill -TERM -x ProPresenter; for i in 1 2 3 4 5 6 7 8 9 10; do pgrep -xq ProPresenter || break; sleep 1; done; pkill -KILL -x ProPresenter; sleep 1; fi; open \"$APP\"" }, - "ableton": { "host": "tracks", "short": "ABLETON", "label": "start Ableton if it is not running", - "run": "pgrep -xq Live || open -a 'Ableton Live 11 Suite'" }, + "ableton": { "host": "tracks", "short": "ABLETON", "label": "start Ableton if it is not running, with the set it last had open", + "run": "pgrep -xq Live || { L=$(ls -td ~/Library/Preferences/Ableton/Live*/ | head -1); S=$(grep -a 'info: Loading document \"' \"${L}Log.txt\" | grep -v '/Templates/' | tail -1 | sed -E 's/^.*Loading document \"(.*)\"[^\"]*$/\\1/'); if [ -n \"$S\" ] && [ -f \"$S\" ]; then open -a 'Ableton Live 11 Suite' \"$S\"; else open -a 'Ableton Live 11 Suite'; fi; }" }, "translate": { "host": "translation", "short": "TRANSLATE", "label": "start Church Translate if it is not running", "run": "pgrep -xq 'Church Translate' || open -a 'Church Translate'" }, "dvs_prop": { "host": "prop", "short": "DVS", "label": "restart Dante Virtual Soundcard on the ProPresenter Mac (its engine restarts; audio to and from that Mac drops for 5-10 s)", "run": "sudo -n /usr/local/sbin/status-panel-dvs-start && open -g -a 'Dante Virtual Soundcard'" }, @@ -68,8 +68,8 @@ "run": "pkill -TERM -x ProdDash; i=0; while pgrep -xq ProdDash && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; open -g /Applications/ProdDash.app" }, "prodcom": { "kind": "local", "short": "PRODCOM", "label": "restart ProdCom (transcription; lobby captions blank for 5-15 s)", "run": "pkill -TERM -x ProdCom; i=0; while pgrep -xq ProdCom && [ $i -lt 20 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq ProdCom && pkill -KILL -x ProdCom; sleep 1; open -g /Applications/ProdCom.app" }, - "ableton_restart": { "host": "tracks", "short": "ABLETON", "label": "restart Ableton on the Tracks Mac (quits it; unsaved set changes are lost, and Live may ask to recover on the Tracks Mac)", "conn": "ableton", "reconnect": true, "timeout": 60, - "run": "pkill -TERM -x 'Live'; i=0; while pgrep -xq 'Live' && [ $i -lt 30 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq 'Live' && pkill -KILL -x 'Live'; sleep 1; open -a 'Ableton Live 11 Suite'" }, + "ableton_restart": { "host": "tracks", "short": "ABLETON", "label": "restart Ableton on the Tracks Mac and reopen the set it had open (unsaved changes are lost; Live may ask to recover on the Tracks Mac)", "conn": "ableton", "reconnect": true, "timeout": 60, + "run": "L=$(ls -td ~/Library/Preferences/Ableton/Live*/ | head -1); S=$(grep -a 'info: Loading document \"' \"${L}Log.txt\" | grep -v '/Templates/' | tail -1 | sed -E 's/^.*Loading document \"(.*)\"[^\"]*$/\\1/'); pkill -TERM -x 'Live'; i=0; while pgrep -xq 'Live' && [ $i -lt 30 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq 'Live' && pkill -KILL -x 'Live'; sleep 1; if [ -n \"$S\" ] && [ -f \"$S\" ]; then open -a 'Ableton Live 11 Suite' \"$S\"; else open -a 'Ableton Live 11 Suite'; fi" }, "translate_restart": { "host": "translation", "short": "TRANSLATE", "label": "restart Church Translate on NA Translation (and its translation server)", "timeout": 60, "run": "pg=$(ps -o pgid= -p \"$(pgrep -x 'Church Translate' | head -1)\" 2>/dev/null | tr -d ' '); pkill -TERM -x 'Church Translate'; i=0; while pgrep -xq 'Church Translate' && [ $i -lt 30 ]; do sleep 0.5; i=$((i+1)); done; pgrep -xq 'Church Translate' && pkill -KILL -x 'Church Translate'; [ -n \"$pg\" ] && [ \"$pg\" != \"$(ps -o pgid= -p $$ | tr -d ' ')\" ] && kill -KILL -- -\"$pg\" 2>/dev/null; sleep 1; open -a 'Church Translate'" }, "tascam_reconnect": { "kind": "reconnect", "conn": "tascam", "short": "TASCAM", "verify_seconds": 20, "hung_note": "HUNG - POWER\nCYCLE IT", @@ -85,13 +85,13 @@ { "id": "rs_dvs_transl", "label": "DVS TRANSL", "pos": [0, 3], "fix": "dvs_translation" }, { "id": "rs_dvs_ingest", "label": "DVS INGEST", "pos": [0, 4], "fix": "dvs_ingest" }, { "id": "rs_propres", "label": "PROPRES", "pos": [1, 1], "fix": "propresenter" }, - { "id": "rs_ableton", "label": "ABLETON", "pos": [1, 2], "fix": "ableton_restart" }, - { "id": "rs_translate", "label": "TRANSLATE", "pos": [1, 3], "fix": "translate_restart" }, + { "id": "rs_ableton", "label": "ABLETON", "pos": [1, 2], "fix": "ableton_restart", "watch": ["ableton", "conn:ableton"] }, + { "id": "rs_translate", "label": "TRANSLATE", "pos": [1, 3], "fix": "translate_restart", "watch": ["translate"] }, { "id": "rs_prodcom", "label": "PRODCOM", "pos": [2, 1], "fix": "prodcom" }, { "id": "rs_proddash", "label": "PRODDASH", "pos": [2, 2], "fix": "proddash" }, - { "id": "rs_captions", "label": "CAPTIONS", "pos": [2, 3], "fix": "captions" }, + { "id": "rs_captions", "label": "CAPTIONS", "pos": [2, 3], "fix": "captions", "watch": ["caption_screen"] }, { "id": "rs_bridge", "label": "BRIDGE", "pos": [2, 4], "fix": "bridge" }, - { "id": "rs_tascam", "label": "TASCAM", "pos": [3, 1], "fix": "tascam_reconnect" } + { "id": "rs_tascam", "label": "TASCAM", "pos": [3, 1], "fix": "tascam_reconnect", "watch": ["conn:tascam"] } ] }, diff --git a/status_panel.py b/status_panel.py index 51369ab..752c296 100644 --- a/status_panel.py +++ b/status_panel.py @@ -863,6 +863,23 @@ class Engine: plan.append((ident, "restart", chk["conn"], chk)) return plan + def failing_targets(self): + """(fix ids, Companion connections) of every check that is down right now: a Restart page + key whose fix (or a fix/connection in its "watch") is among them flashes.""" + fixes, conns = set(), set() + for key, tr in self.trackers.items(): + if not tr.confirmed or tr.result.level != ERR or key not in self.meta: + continue + _, chk, sub = self.meta[key] + fix_id = chk.get("fix") + if isinstance(fix_id, dict): + fix_id = fix_id.get(sub, fix_id.get("*")) + if fix_id: + fixes.add(fix_id) + if chk.get("conn"): + conns.add(chk["conn"]) + return fixes, conns + def failing(self, tile_id): """Is anything on this button (or its members) in a confirmed fault right now?""" for key in self.keys_for(tile_id): @@ -1062,10 +1079,15 @@ def colored(text, level): def render(engine, service_on): out = {} + down_fixes, down_conns = engine.failing_targets() for kid, key in engine.restart_keys.items(): note = engine.notes.get(kid) + watch = [key["fix"]] + key.get("watch", []) + down = any(w[5:] in down_conns if w.startswith("conn:") else w in down_fixes for w in watch) if note and engine.now < note["until"]: out[kid] = colored(key["label"] + "\n" + note["text"], note["level"]) + elif down: # flashes: this is the one to hold + out[kid] = colored(key["label"] + "\nHOLD TO FIX", "err") else: out[kid] = colored(key["label"] + "\nHOLD 2s", "idle") for tile in engine.cfg["tiles"]: @@ -1697,6 +1719,9 @@ def load_config(path): raise ValueError("restart key %s has a tile's id" % k["id"]) if k["fix"] not in cfg.get("fixes", {}): raise ValueError("restart key %s: unknown fix %s" % (k["id"], k["fix"])) + for w in k.get("watch", []): + if not w.startswith("conn:") and w not in cfg.get("fixes", {}): + raise ValueError("restart key %s: unknown fix %s in watch" % (k["id"], w)) return cfg diff --git a/tests/test_status_panel.py b/tests/test_status_panel.py index 6506551..abe6245 100644 --- a/tests/test_status_panel.py +++ b/tests/test_status_panel.py @@ -660,6 +660,29 @@ class TestFixes(unittest.TestCase): open(os.path.join(os.path.dirname(os.path.abspath(sp.__file__)), "remote-access", "mac-dvs-helper.sh")).read()) self.assertIn("open -g /Applications/ProdCom.app", CONFIG["fixes"]["dvs_ingest"]["run"]) # ProdCom after DVS + def test_restart_keys_flash_when_their_thing_is_down(self): + devices = json.loads(self.snap.vars[("audinate-dantecontroller", "devices")]) + devices.remove("NA-Tracks") # DVS on the Tracks Mac gone + self.snap.vars[("audinate-dantecontroller", "devices")] = json.dumps(devices) + self.snap.remote["tracks\0pgrep -xq Live"] = False # Ableton not running + self.snap.conns["tascam"]["category"] = "error" + tiles, _ = run_steps(self.engine, self.snap, SUNDAY, 4) + flashing = {k for k, v in tiles.items() if k.startswith("rs_") and level(v) == "err"} + self.assertEqual(flashing, {"rs_dvs_tracks", "rs_ableton", "rs_tascam"}) + self.assertEqual(tiles["rs_dvs_tracks"]["x"], "DVS TRACKS\nHOLD TO FIX") + self.assertEqual(level(tiles["rs_prodcom"]), "idle") + self.engine.ack("all") # acked on the status page: + tiles = self.engine.step(self.snap, SUNDAY + 5) # still shows what to hold + self.assertEqual(level(tiles["rs_ableton"]), "err") + + def test_ableton_reopens_the_set_it_had_open(self): + for fix_id in ("ableton", "ableton_restart"): + run = CONFIG["fixes"][fix_id]["run"] + self.assertIn("Log.txt", run) + self.assertIn("grep -v '/Templates/'", run) + self.assertIn('open -a \'Ableton Live 11 Suite\' "$S"', run) + self.assertTrue(CONFIG["fixes"]["ableton"]["run"].startswith("pgrep -xq Live || {")) + def test_tascam_hold_always_reconnects(self): ran = [] self.engine.executor = lambda cfg, fix_id, chk: ran.append(fix_id) or "HUNG - POWER\nCYCLE IT"