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:
@@ -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
@@ -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
@@ -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.
@@ -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
@@ -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
|
||||
|
||||
|
||||
|
||||
@@ -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))
|
||||
|
||||
Reference in New Issue
Block a user