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. | | 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". | | 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. | | 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. | | 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. | | 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. | | 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": [ "watch": [
"conn:arena" "conn:arena"
] ]
},
{
"id": "rs_ableset",
"label": "ABLESET",
"fix": "ableset",
"toggle": "ableset",
"pos": [
1,
6
]
} }
], ],
"page": 20, "page": 20,
@@ -2657,6 +2667,18 @@
3, 3,
7 7
] ]
},
{
"id": "ableset",
"label": "ABLESET",
"kind": "bypass",
"bypass": {
"fixes": [
"ableset"
],
"on_text": "AUTO-OPEN\nOFF",
"off_text": "AUTO-OPEN\nON"
}
} }
], ],
"timing": { "timing": {
+12 -5
View File
@@ -150,17 +150,22 @@ def restart_key(key, cfg):
names, opts = cfg["variables"], settings(cfg) names, opts = cfg["variables"], settings(cfg)
hold_ms, secs = opts["hold_ms"], "%g" % (opts["hold_ms"] / 1000.0) hold_ms, secs = opts["hold_ms"], "%g" % (opts["hold_ms"] / 1000.0)
stale_text = "'%s\\n?'" % key["label"] 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 { return {
"type": "button-layered", "type": "button-layered",
"style": {"layers": layers(key["id"], key, names, stale_text, opts)}, "style": {"layers": layers(key["id"], key, names, stale_text, opts)},
"options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False, "options": {"stepProgression": "auto", "stepExpression": "", "rotaryActions": False,
"canModifyStyleInApis": True, "canModifyStyleInApis": True,
"notes": "RESTART key '%s' - hold %s s: restart it even when the status page says it is fine " "notes": note},
"(status_panel.py on %s runs fix '%s')." % (key["id"], secs, opts["ingest"], key["fix"])},
"feedbacks": [], "feedbacks": [],
"steps": {"0": {"action_sets": {"down": [], "up": [], "steps": {"0": {"action_sets": {"down": [], "up": [],
str(hold_ms): [command_action(key["id"], "hold", names["command"], str(hold_ms): [command_action(key["id"], "hold", names["command"], verb)]},
"force %s $(internal:time_unix)" % key["id"])]},
"options": {"runWhileHeld": [hold_ms]}}}, "options": {"runWhileHeld": [hold_ms]}}},
"localVariables": [], "localVariables": [],
} }
@@ -214,7 +219,9 @@ def button(tid, tile, cfg):
stale_text = "'%s\\n?'" % label stale_text = "'%s\\n?'" % label
press, hold = "ack " + tid, "restart " + tid press, hold = "ack " + tid, "restart " + tid
b = tile.get("bypass") or {} 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 []), "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 "")) " and Companion's %s switched off" % ", ".join(b["conns"]) if b.get("conns") else ""))
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).", "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"]) "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), " 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) "Keys", advanced=True)
field("tiles[].members", "list", None, "A group: the keys it sums up.", "Keys") field("tiles[].members", "list", None, "A group: the keys it sums up.", "Keys")
field("tiles[].checks", "list", [], "What the key watches.", "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[].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[].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[].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 " field("restart_page.keys[].disabled", "bool", False, "Switched off: the key stays on the page (OFF) but a hold does "
"nothing.", "Keys", apply="pages") "nothing.", "Keys", apply="pages")
field("restart_page.keys[].watch", "list", [], "Keep flashing HOLD TO FIX while these fixes' checks or " 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", "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)": [ "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")} 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: if fixes_of & on_restart:
continue 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")) 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: if kinds["master"] != 1:
errors.append("there must be exactly one ALL key (kind master); found %d" % kinds["master"]) 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"]: for t in cfg["tiles"]:
if t.get("kind") == "bypass": if t.get("kind") == "bypass":
b = t.get("bypass") if isinstance(t.get("bypass"), dict) else {} b = t.get("bypass") if isinstance(t.get("bypass"), dict) else {}
if not b.get("peers"): 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)" % t.get("id")) 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 []: for h in b.get("hosts") or []:
if h and h not in cfg.get("hosts", {}): 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)) 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) errors.append("%s: a key on the Status page has that id" % where)
if k.get("fix") not in fixes: if k.get("fix") not in fixes:
errors.append("%s: unknown fix %s" % (where, k.get("fix"))) 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", []): for w in k.get("watch", []):
if not w.startswith("conn:") and w not in fixes: if not w.startswith("conn:") and w not in fixes:
errors.append("%s: unknown fix %s in its watch list" % (where, w)) 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 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.""" checks naming it are reconnected too. Its Companion connections are switched off, then on again."""
b = tile.get("bypass") or {} 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() 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", []) 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")}) 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): def evaluate(self, snap, now, service_on):
ctx = {"now": now, "service": service_on, "any_recording": False, "dante_all": {}, 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), "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 t in all_tiles(self.cfg):
for chk in t.get("checks", []): for chk in t.get("checks", []):
if chk["type"] == "dante": if chk["type"] == "dante":
@@ -1426,6 +1428,8 @@ class Engine:
for i, chk in enumerate(t.get("checks", [])): for i, chk in enumerate(t.get("checks", [])):
try: try:
results = CHECKS[chk["type"]](chk, snap, ctx) 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 except Exception as e: # a bad check must not take the panel down
log.exception("check %s/%s failed", t["id"], chk["type"]) log.exception("check %s/%s failed", t["id"], chk["type"])
results = [("error", Result(WARN, "CHECK BUG"))] results = [("error", Result(WARN, "CHECK BUG"))]
@@ -1748,6 +1752,10 @@ class Engine:
ips.add(str(ip)) ips.add(str(ip))
return names, ips 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): 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 """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).""" 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": []} entry = self.bypass[tile_id] = {"since": now, "dropped": []}
on = True on = True
self.state.set("bypass", self.bypass) self.state.set("bypass", self.bypass)
log.info("BYPASS %s: %s", tile["label"], "handed to an operator" if on else "back to the panel") b = tile.get("bypass") or {}
self.note(tile_id, "TAKING\nMIDI OUT" if on else "PUTTING\nMIDI BACK", "fixing", now, 6) 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.bypass_last = now
self.start_bypass_job(tile_id, on, entry, comp) self.start_bypass_job(tile_id, on, entry, comp)
@@ -2409,7 +2422,9 @@ def render(engine, service_on):
if kind == "bypass": if kind == "bypass":
# Handed to an operator: steady blue, says so; otherwise the panel runs it. # Handed to an operator: steady blue, says so; otherwise the panel runs it.
on = tid in engine.bypass 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 continue
if kind == "master": if kind == "master":
# Idle faults are expected outside service; they do not stop "ALL OK". # Idle faults are expected outside service; they do not stop "ALL OK".
@@ -2461,6 +2476,9 @@ def render(engine, service_on):
else: else:
text = tile["label"] + "\n" + (infos[0] if infos else "OK") text = tile["label"] + "\n" + (infos[0] if infos else "OK")
out[tid] = colored(text + failover_line(engine, [tid]), level) 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 return out
+20
View File
@@ -2634,6 +2634,26 @@ class TestSwitches(unittest.TestCase):
self.assertIsNone(res) self.assertIsNone(res)
self.assertIn("calls in", why) 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): def test_a_bypass_key_needs_its_peers(self):
import schema import schema
bad = json.loads(json.dumps(self.cfg)) bad = json.loads(json.dumps(self.cfg))