ABLESET switch (Restart page): hold to stop AbleSet being watched and opened

The user has an AbleSet bug and needs auto-open off for now. A bypass key can now hold off fixes
(bypass.fixes): their checks stay green and nothing runs them. The status page is full, so a Restart
page key with toggle shows the switch and its hold flips it. Switched off live 2026-10-25 07:18.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-25 07:19:51 -04:00
co-authored by Claude Opus 5.5
parent 0f3471bf51
commit 0ecec5f112
7 changed files with 93 additions and 15 deletions
+1 -1
View File
@@ -256,7 +256,7 @@ The **page number key** on every page follows the ALL button. It's green when ev
| LED | VX1000 and UHD Jr: Companion connection **and** each one answers ping (the NovaStar module keeps saying "Connected" after a processor is switched off). "UHDJR NO PING" = processor off or unreachable; "UHDJR OFFLINE" = Companion's connection failed; "UHDJR DISABLED" (amber) = connection turned off in Companion. |
| DANTE | All 19 Dante devices ("19/19"), named when one drops: "WING GONE", "TRACKS GONE", "PROP MAC GONE". |
| MIDI | All six network MIDI links on the FOH / Studio B checklist, each named when it drops: **LIGHT PC** (Ingest → Lighting), **SUPERRACK** (Ingest → Livebox), **FX LIGHTS** (Tracks → Lighting, the FX lighting cues), **TRK LIVEBOX** (Tracks → Livebox), **TRK STUDIO B** (Tracks → Studio B), **PROP LIGHTS** (ProPresenter → Lighting). NA Ingest's own session is read locally; NA Tracks' and NA ProP's over SSH every 5 s. Also: the Lighting PC and NA Tracks MIDI sessions still announced on the network. A clean disconnect shows within about 10 s; a crash or pulled cable takes about a minute (macOS's own timeout, measured). Hold 2 s reconnects whichever links dropped. |
| APPS | ProPresenter API (offline or error only: its harmless yellow warning, every 15–20 min, is ignored with `"ignore_warning": true`); Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC; **AbleSet** running on the Tracks Mac (ProdDash shows it): **ABLESET NOT RUNNING**, and the hold (and auto-fix) opens it in the background and minimizes its window (`mac_start` with *Minimized*). Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections.; **Pro Tools on the Studio B Mac Studio** set to Solo Mode **AFL + Latch** ("PT SOLO NOT AFL"; hold, or Sunday auto-fix, ticks them back). A helper on that Mac (`remote-access/solo-mode-agent.swift`, installed with `install-solo-mode.sh`) reads the menu; it needs Accessibility allowed once, then a restart of the helper. "PT SOLO CHECK OFF" = that helper is not running or not allowed. |
| APPS | ProPresenter API (offline or error only: its harmless yellow warning, every 15–20 min, is ignored with `"ignore_warning": true`); Ableton OSC; tracks Mac pings; Bible TV app answering on :8080 (every 5 s); Bible TV OSC; **AbleSet** running on the Tracks Mac (ProdDash shows it): **ABLESET NOT RUNNING**, and the hold (and auto-fix) opens it in the background and minimizes its window (`mac_start` with *Minimized*). While **ABLESET** on the Restart page (row 1 col 6) says **AUTO-OPEN OFF**, AbleSet is neither checked nor opened (hold it to switch; it stays as left, through restarts: a `bypass` key with `fixes`, shown by a Restart page key with `toggle`). Messages name the app ("PROPRES OFFLINE", "BIBLE APP DOWN"). Hold 2 s restarts the ProPresenter, Ableton and Bible TV Companion connections.; **Pro Tools on the Studio B Mac Studio** set to Solo Mode **AFL + Latch** ("PT SOLO NOT AFL"; hold, or Sunday auto-fix, ticks them back). A helper on that Mac (`remote-access/solo-mode-agent.swift`, installed with `install-solo-mode.sh`) reads the menu; it needs Accessibility allowed once, then a restart of the helper. "PT SOLO CHECK OFF" = that helper is not running or not allowed. |
| VISTA | Vista on the Lighting PC: its Art-Net poll (sent every 0.5 s) must be heard within 5 s, otherwise **SILENT** (Vista closed or crashed, or the Lighting PC is off the network). Hold 2 s starts Vista if it is not running; Vista then opens **Prop Takeover** from its show folder (`E:\media\Documents\Vista 3\Shows`) by itself, through its own autoload preference (`<autoloadpath>` in `E:\media\Documents\Vista 3\.preferences`, set 2026-10-08). Resolume is the ARENA button. |
| RESI | decoder (RossTalk connection and ping, 10.1.10.203); **encoder** (ping and its web page, 10.1.10.202, LAOEncoder); resi-studio-cues; Resi cloud reachable. Only shows they are on the network, not that the encoder is streaming. |
| NET | NA Ingest's link; gateway 10.1.0.1 pings; **4+ connections from 3+ groups dropping within 10 s** (what happened Sunday 10/5 at 10:21). A blip **flashes amber** for 5 minutes, then goes by itself; press NET to stop the flashing, hold NET 2 s to clear it now. |
+22
View File
@@ -1697,6 +1697,16 @@
"watch": [
"conn:arena"
]
},
{
"id": "rs_ableset",
"label": "ABLESET",
"fix": "ableset",
"toggle": "ableset",
"pos": [
1,
6
]
}
],
"page": 20,
@@ -2657,6 +2667,18 @@
3,
7
]
},
{
"id": "ableset",
"label": "ABLESET",
"kind": "bypass",
"bypass": {
"fixes": [
"ableset"
],
"on_text": "AUTO-OPEN\nOFF",
"off_text": "AUTO-OPEN\nON"
}
}
],
"timing": {
+12 -5
View File
@@ -150,17 +150,22 @@ def restart_key(key, cfg):
names, opts = cfg["variables"], settings(cfg)
hold_ms, secs = opts["hold_ms"], "%g" % (opts["hold_ms"] / 1000.0)
stale_text = "'%s\\n?'" % key["label"]
if key.get("toggle"): # a switch kept on this page (ABLESET): its hold flips it
verb, note = "restart %s $(internal:time_unix)" % key["toggle"], (
"SWITCH key '%s' - hold %s s: switch %s off or back on (status_panel.py on %s)." % (key["id"], secs, key["label"], opts["ingest"]))
else:
verb, note = "force %s $(internal:time_unix)" % key["id"], (
"RESTART key '%s' - hold %s s: restart it even when the status page says it is fine "
"(status_panel.py on %s runs fix '%s')." % (key["id"], secs, opts["ingest"], key["fix"]))
return {
"type": "button-layered",
"style": {"layers": layers(key["id"], key, names, stale_text, opts)},
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
"canModifyStyleInApis": True,
"notes": "RESTART key '%s' - hold %s s: restart it even when the status page says it is fine "
"(status_panel.py on %s runs fix '%s')." % (key["id"], secs, opts["ingest"], key["fix"])},
"notes": note},
"feedbacks": [],
"steps": {"0": {"action_sets": {"down": [], "up": [],
str(hold_ms): [command_action(key["id"], "hold", names["command"],
"force %s $(internal:time_unix)" % key["id"])]},
str(hold_ms): [command_action(key["id"], "hold", names["command"], verb)]},
"options": {"runWhileHeld": [hold_ms]}}},
"localVariables": [],
}
@@ -214,7 +219,9 @@ def button(tid, tile, cfg):
stale_text = "'%s\\n?'" % label
press, hold = "ack " + tid, "restart " + tid
b = tile.get("bypass") or {}
note = ("Hold %s s: hand %s to an operator - its MIDI links (%s) are taken out%s and the panel stops putting them "
held = ", ".join((cfg.get("fixes", {}).get(f) or {}).get("short", f) for f in b.get("fixes") or [])
note = ("Hold %s s: switch off %s - its checks stay green and nothing opens or fixes it. Hold again: back on." % (secs, held)
if held and not b.get("peers") else None) or ("Hold %s s: hand %s to an operator - its MIDI links (%s) are taken out%s and the panel stops putting them "
"back. Hold again: everything is put back." % (secs, label, ", ".join(b.get("peers") or []),
" and Companion's %s switched off" % ", ".join(b["conns"]) if b.get("conns") else ""))
else:
Binary file not shown.
+16 -5
View File
@@ -274,7 +274,9 @@ field("tiles[].kind", "enum", None, "A special key: master (ALL), service (ALARM
"silences), bypass (hold: hand a device to an operator - its MIDI links out, its Companion connections off).",
"Keys", advanced=True, options=["master", "service", "network", "ready", "autofix", "bypass"])
field("tiles[].bypass", "dict", None, "A bypass key: peers (MIDI peer or session names to take out, e.g. Lighting), "
"hosts (the Macs whose sessions to look in; \"\" is this Mac) and conns (Companion connections to switch off).",
"hosts (the Macs whose sessions to look in; \"\" is this Mac) and conns (Companion connections to switch off); or "
"fixes (fix ids it holds off: their checks stay green and nothing runs them, e.g. ableset), with on_text / off_text "
"for the key's face.",
"Keys", advanced=True)
field("tiles[].members", "list", None, "A group: the keys it sums up.", "Keys")
field("tiles[].checks", "list", [], "What the key watches.", "Keys")
@@ -350,6 +352,8 @@ field("restart_page.keys[].id", "id", None, "", "Keys", advanced=True, required=
field("restart_page.keys[].label", "str", None, "", "Keys", required=True, apply="pages")
field("restart_page.keys[].pos", "list", None, "[row, column].", "Keys", required=True, apply="pages")
field("restart_page.keys[].fix", "str", None, "What holding it runs.", "Keys", required=True)
field("restart_page.keys[].toggle", "str", None, "A switch kept on this page: the bypass key (no position of its own) "
"it shows and a hold flips, e.g. ableset.", "Keys", apply="pages")
field("restart_page.keys[].disabled", "bool", False, "Switched off: the key stays on the page (OFF) but a hold does "
"nothing.", "Keys", apply="pages")
field("restart_page.keys[].watch", "list", [], "Keep flashing HOLD TO FIX while these fixes' checks or "
@@ -565,7 +569,7 @@ GLOSSARY = {
"ON FOH BACKUP", "BUSY", "TAKING MIDI OUT", "PUTTING MIDI BACK", "TAKING", "PUTTING", "MIDI OUT", "MIDI BACK",
],
"a switch key (AUTO-FIX, or one like LIGHTS that hands a device to an operator)": [
"OPERATOR", "MIDI OFF", "OFF IN SETTINGS", "SETTINGS", "TIL", "PAUSED",
"OPERATOR", "MIDI OFF", "AUTO-OPEN", "OFF IN SETTINGS", "SETTINGS", "TIL", "PAUSED",
],
}
@@ -732,7 +736,9 @@ def validate(cfg, live=None):
fixes_of = {c.get("fix") for c in t.get("checks", []) if isinstance(c.get("fix"), str)} | {"conn:" + c["conn"] for c in t.get("checks", []) if c.get("conn")}
if fixes_of & on_restart:
continue
if "pos" not in t and "members" not in t and t.get("id") not in in_group and t.get("kind") != "master":
toggled = {k.get("toggle") for k in (cfg.get("restart_page") or {}).get("keys", [])}
if "pos" not in t and "members" not in t and t.get("id") not in in_group and t.get("kind") != "master" \
and t.get("id") not in toggled:
warnings.append("key %s has no place on the deck and is in no group: nobody will see it" % t.get("id"))
if kinds["master"] != 1:
errors.append("there must be exactly one ALL key (kind master); found %d" % kinds["master"])
@@ -745,8 +751,11 @@ def validate(cfg, live=None):
for t in cfg["tiles"]:
if t.get("kind") == "bypass":
b = t.get("bypass") if isinstance(t.get("bypass"), dict) else {}
if not b.get("peers"):
errors.append("key %s: a bypass key needs bypass.peers (the MIDI peers it takes out)" % t.get("id"))
if not b.get("peers") and not b.get("fixes"):
errors.append("key %s: a bypass key needs bypass.peers (the MIDI peers it takes out) or bypass.fixes" % t.get("id"))
for f in b.get("fixes") or []:
if f not in cfg.get("fixes", {}):
errors.append("key %s: bypass fix %s is not a fix in the settings" % (t.get("id"), f))
for h in b.get("hosts") or []:
if h and h not in cfg.get("hosts", {}):
errors.append("key %s: bypass host %s is not a computer in the settings" % (t.get("id"), h))
@@ -857,6 +866,8 @@ def validate(cfg, live=None):
errors.append("%s: a key on the Status page has that id" % where)
if k.get("fix") not in fixes:
errors.append("%s: unknown fix %s" % (where, k.get("fix")))
if k.get("toggle") and not any(t.get("id") == k["toggle"] and t.get("kind") == "bypass" for t in cfg.get("tiles", [])):
errors.append("%s: toggle %s is not a bypass key" % (where, k["toggle"]))
for w in k.get("watch", []):
if not w.startswith("conn:") and w not in fixes:
errors.append("%s: unknown fix %s in its watch list" % (where, w))
+22 -4
View File
@@ -592,6 +592,8 @@ def bypass_apply(cfg, tile, on, entry, comp, names_ips, lock=None):
taken out ({host, session, peer, address}), so taking it back puts exactly those back; the MIDI
checks naming it are reconnected too. Its Companion connections are switched off, then on again."""
b = tile.get("bypass") or {}
if not b.get("peers") and not b.get("conns"):
return # a key that only holds off fixes: nothing to hand over
names, ips = set(b.get("peers") or []), set(names_ips[1]) if on else set()
hosts = b.get("hosts") or [""] + sorted({c["host"] for t in cfg["tiles"] for c in t.get("checks", [])
if c.get("type") == "midi_peer" and c.get("peer") in names and c.get("host")})
@@ -1410,7 +1412,7 @@ class Engine:
def evaluate(self, snap, now, service_on):
ctx = {"now": now, "service": service_on, "any_recording": False, "dante_all": {},
"ready_due": in_windows(self.ready_cfg.get("due", []), now), "ready_done": self.ready_done(now),
"bypassed": self.bypass_names()[0], "autofix_windows": (self.cfg.get("auto_fix") or {}).get("windows", [])}
"bypassed": self.bypass_names()[0], "bypassed_fixes": self.bypass_fixes(), "autofix_windows": (self.cfg.get("auto_fix") or {}).get("windows", [])}
for t in all_tiles(self.cfg):
for chk in t.get("checks", []):
if chk["type"] == "dante":
@@ -1426,6 +1428,8 @@ class Engine:
for i, chk in enumerate(t.get("checks", [])):
try:
results = CHECKS[chk["type"]](chk, snap, ctx)
if isinstance(chk.get("fix"), str) and chk["fix"] in ctx["bypassed_fixes"]:
results = [(sub, Result(OK)) for sub, _ in results] # switched off by its bypass key
except Exception as e: # a bad check must not take the panel down
log.exception("check %s/%s failed", t["id"], chk["type"])
results = [("error", Result(WARN, "CHECK BUG"))]
@@ -1748,6 +1752,10 @@ class Engine:
ips.add(str(ip))
return names, ips
def bypass_fixes(self):
"""Fixes a bypass key holds off (an ABLESET key: AbleSet not watched nor opened while it is on)."""
return {f for tid in self.bypass for f in (((self.tiles.get(tid) or {}).get("bypass") or {}).get("fixes") or [])}
def toggle_bypass(self, tile_id, comp, now):
"""A hold on a bypass key: hand it to an operator (take its MIDI links out, switch its Companion
connections off), or take it back (put everything back, as it was)."""
@@ -1763,8 +1771,13 @@ class Engine:
entry = self.bypass[tile_id] = {"since": now, "dropped": []}
on = True
self.state.set("bypass", self.bypass)
log.info("BYPASS %s: %s", tile["label"], "handed to an operator" if on else "back to the panel")
self.note(tile_id, "TAKING\nMIDI OUT" if on else "PUTTING\nMIDI BACK", "fixing", now, 6)
b = tile.get("bypass") or {}
if b.get("peers") or b.get("conns"):
log.info("BYPASS %s: %s", tile["label"], "handed to an operator" if on else "back to the panel")
self.note(tile_id, "TAKING\nMIDI OUT" if on else "PUTTING\nMIDI BACK", "fixing", now, 6)
else: # a switch that only holds off fixes: its face says it all
log.info("BYPASS %s: %s", tile["label"], "switched off (%s not watched or run)" % ", ".join(b.get("fixes") or [])
if on else "back on")
self.bypass_last = now
self.start_bypass_job(tile_id, on, entry, comp)
@@ -2409,7 +2422,9 @@ def render(engine, service_on):
if kind == "bypass":
# Handed to an operator: steady blue, says so; otherwise the panel runs it.
on = tid in engine.bypass
out[tid] = colored("%s\n%s" % (tile["label"], "OPERATOR\nMIDI OFF" if on else "AUTO"), "service" if on else "ok")
b = tile.get("bypass") or {}
face = (b.get("on_text") or "OPERATOR\nMIDI OFF") if on else (b.get("off_text") or "AUTO")
out[tid] = colored("%s\n%s" % (tile["label"], face), "service" if on else "ok")
continue
if kind == "master":
# Idle faults are expected outside service; they do not stop "ALL OK".
@@ -2461,6 +2476,9 @@ def render(engine, service_on):
else:
text = tile["label"] + "\n" + (infos[0] if infos else "OK")
out[tid] = colored(text + failover_line(engine, [tid]), level)
for kid, key in engine.restart_keys.items(): # a Restart page key that is a switch shows the switch
if key.get("toggle") in out:
out[kid] = out[key["toggle"]]
return out
+20
View File
@@ -2634,6 +2634,26 @@ class TestSwitches(unittest.TestCase):
self.assertIsNone(res)
self.assertIn("calls in", why)
def test_ableset_key_holds_off_its_fix(self):
"""ABLESET (2026-10-25, the user had an AbleSet bug): held, AbleSet is neither watched nor opened."""
engine = sp.Engine(CONFIG)
snap = sp.Snapshot()
snap.remote["tracks\0pgrep -xq AbleSet"] = False
tiles, _ = run_steps(engine, snap, SUNDAY, 4)
msgs = lambda: [res.msg for _, res, _ in engine.items.get("apps", [])]
self.assertIn("ABLESET NOT RUNNING", msgs())
self.assertEqual(tiles["ableset"]["x"], "ABLESET\nAUTO-OPEN\nON")
self.assertIn("ableset", [w for _, k, w, _ in engine.fix_plan("apps", snap)])
engine.bypass["ableset"] = {"since": SUNDAY, "dropped": []}
tiles, _ = run_steps(engine, snap, SUNDAY + 10, 4)
self.assertNotIn("ABLESET NOT RUNNING", msgs())
self.assertEqual(tiles["ableset"]["x"], "ABLESET\nAUTO-OPEN\nOFF")
self.assertEqual(tiles["rs_ableset"]["x"], "ABLESET\nAUTO-OPEN\nOFF") # its key on the Restart page
key = make_page.restart_key(next(k for k in CONFIG["restart_page"]["keys"] if k["id"] == "rs_ableset"), CONFIG)
self.assertIn("restart ableset", json.dumps(key)) # the hold flips the switch
self.assertNotIn("ableset", [w for _, k, w, _ in engine.fix_plan("apps", snap)])
sp.bypass_apply(CONFIG, engine.tiles["ableset"], True, engine.bypass["ableset"], None, (set(), set())) # nothing to hand over
def test_a_bypass_key_needs_its_peers(self):
import schema
bad = json.loads(json.dumps(self.cfg))