The LTC write happens by itself: READY fixes ticked for auto-fix act on the checklist, any hour for ready_ltc
Auto-fix now looks at the READY page's keys too, for the READY fixes ticked under Settings > Auto-fix, and acts on a checklist gap while it is still amber. A fix marked anytime (only a READY fix may be) runs outside the auto-fix hours as well, never while ALARMS is muted; the window's own fixes keep their hours and start fresh each window. ready_ltc is ticked and marked anytime at North Attleboro: once the weekend's set is on Tracks, every song's timecode goes into all four campuses' plans. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
+32
-19
@@ -645,7 +645,8 @@ def effective_config(cfg):
|
||||
|
||||
def all_tiles(cfg):
|
||||
"""Every key the watchdog judges: the Status page's tiles and the READY page's keys. The READY
|
||||
page's keys are not in ALL (nor in auto-fix): a weekday gap in the checklist is not an alarm."""
|
||||
page's keys are not in ALL: a weekday gap in the checklist is not an alarm. Auto-fix does look
|
||||
at them, for the READY fixes ticked under Settings > Auto-fix."""
|
||||
return cfg["tiles"] + ready_keys(cfg)
|
||||
|
||||
# Checks whose connection a long press may restart (Dante is shared, so not that one).
|
||||
@@ -1233,12 +1234,13 @@ class Engine:
|
||||
tile = self.tiles.get(tile_id, {})
|
||||
return [c["conn"] for c in tile.get("checks", []) if c["type"] in RESTARTABLE and c.get("conn")]
|
||||
|
||||
def fix_plan(self, tile_id, snap, now=None, min_age=0, errors_only=False):
|
||||
def fix_plan(self, tile_id, snap, now=None, min_age=0, errors_only=False, levels=(ERR,)):
|
||||
"""What a hold on this button would do: fix only what is failing right now.
|
||||
|
||||
[(identity, "fix", fix id, check) or (identity, "restart", connection label, check)]
|
||||
A check's own "fix" wins; otherwise a failing Companion connection is restarted.
|
||||
Nothing is done for a healthy button, and a recording HyperDeck is never touched.
|
||||
With errors_only, only faults of these `levels` that have lasted min_age count (auto-fix).
|
||||
"""
|
||||
tile = self.tiles.get(tile_id, {})
|
||||
ids = self.ready_ids if tile.get("kind") == "ready" else (tile.get("members") or [tile_id])
|
||||
@@ -1248,7 +1250,7 @@ class Engine:
|
||||
tr = self.trackers.get(key)
|
||||
if tid not in ids or not tr or not tr.confirmed or tr.result.level not in (WARN, ERR):
|
||||
continue
|
||||
if errors_only and (tr.result.level != ERR or now - (tr.since or now) < min_age):
|
||||
if errors_only and (tr.result.level not in levels or now - (tr.since or now) < min_age):
|
||||
continue # auto-fix: only real faults that have lasted
|
||||
# A connection switched off in Companion is fixed by switching it on - never by
|
||||
# acting on the device (2026-10-08: a disabled HyperDeck connection got a deck reboot).
|
||||
@@ -1500,19 +1502,26 @@ class Engine:
|
||||
# (playing the Spotify playlist): those keep running inside the window.
|
||||
muted = not self.service.on(now)
|
||||
keep = set(a.get("while_muted", [])) if muted else None
|
||||
if not in_windows(a.get("windows", []), now) or (muted and not keep):
|
||||
hours = in_windows(a.get("windows", []), now)
|
||||
# A fix marked "anytime" (writing the timecode into Planning Center) runs by itself at any
|
||||
# hour, not only in the auto-fix hours - still never while ALARMS is muted.
|
||||
anytime = {f for f in a.get("fixes", []) if self.cfg["fixes"].get(f, {}).get("anytime")
|
||||
and not self.cfg["fixes"].get(f, {}).get("never_auto") and not self.cfg["fixes"].get(f, {}).get("disabled")}
|
||||
if not hours or (muted and not keep):
|
||||
if self.auto_on and self.autolog:
|
||||
self.autolog.write(now, "PANEL", "auto-fix off (%s)" % ("ALARMS muted" if in_windows(
|
||||
a.get("windows", []), now) else "end of the window"))
|
||||
self.autolog.write(now, "PANEL", "auto-fix off (%s)" % ("ALARMS muted" if hours else "end of the window"))
|
||||
self.auto_on = False
|
||||
self.auto_tries.clear()
|
||||
self.auto_seen.clear()
|
||||
return []
|
||||
if (not self.auto_on or self.auto_muted != muted) and self.autolog: # opens today's file, so it can be watched from the start
|
||||
ends = [w["end"] for w in a.get("windows", []) if in_windows([w], now)]
|
||||
self.autolog.write(now, "PANEL", "auto-fix on%s%s" % (" until " + ends[0] if ends else "",
|
||||
" (ALARMS muted: only %s)" % ", ".join(sorted(keep)) if muted else ""))
|
||||
self.auto_on, self.auto_muted = True, muted
|
||||
for ident in [i for i in self.auto_tries if not (i[0] == "fix" and i[1] in anytime)]:
|
||||
del self.auto_tries[ident] # the window's fixes start fresh next time
|
||||
self.auto_seen.pop(ident, None)
|
||||
if muted or not anytime:
|
||||
return []
|
||||
else:
|
||||
if (not self.auto_on or self.auto_muted != muted) and self.autolog: # opens today's file, so it can be watched from the start
|
||||
ends = [w["end"] for w in a.get("windows", []) if in_windows([w], now)]
|
||||
self.autolog.write(now, "PANEL", "auto-fix on%s%s" % (" until " + ends[0] if ends else "",
|
||||
" (ALARMS muted: only %s)" % ", ".join(sorted(keep)) if muted else ""))
|
||||
self.auto_on, self.auto_muted = True, muted
|
||||
net_down = any(self.trackers.get("net:" + k) and self.trackers["net:" + k].confirmed
|
||||
and self.trackers["net:" + k].result.level == ERR for k in ("link", "gateway"))
|
||||
if net_down or (self.burst and (self.burst["cleared"] is None
|
||||
@@ -1525,9 +1534,11 @@ class Engine:
|
||||
self.autolog.write(now, "NET", "network settled: auto-fix is back on")
|
||||
self.auto_paused = False
|
||||
gentle = {f for f in a.get("fixes", []) if not self.cfg["fixes"].get(f, {}).get("never_auto")
|
||||
and not self.cfg["fixes"].get(f, {}).get("disabled")}
|
||||
and not self.cfg["fixes"].get(f, {}).get("disabled")} if hours else anytime
|
||||
restarts = bool(hours and a.get("restart_connections"))
|
||||
ready_ids = {k["id"] for k in ready_keys(self.cfg)}
|
||||
done = []
|
||||
for t in self.cfg["tiles"]:
|
||||
for t in all_tiles(self.cfg):
|
||||
if "checks" not in t:
|
||||
continue # groups repeat their members
|
||||
gone = [k for k in self.keys_for(t["id"]) if self.meta.get(k) and self.meta[k][1]["type"] == "dante"
|
||||
@@ -1544,8 +1555,10 @@ class Engine:
|
||||
self.auto_seen[("dante-wide", t["id"])] = now
|
||||
continue
|
||||
plan = []
|
||||
for ident, kind, what, chk in self.fix_plan(t["id"], snap, now, a.get("after_seconds", 20), True):
|
||||
if not ((kind == "fix" and what in gentle) or (kind == "restart" and a.get("restart_connections"))):
|
||||
# A READY key's gap is amber until the checklist is due: a ticked READY fix acts on it anyway.
|
||||
levels = (WARN, ERR) if t["id"] in ready_ids else (ERR,)
|
||||
for ident, kind, what, chk in self.fix_plan(t["id"], snap, now, a.get("after_seconds", 20), True, levels):
|
||||
if not ((kind == "fix" and what in gentle) or (kind == "restart" and restarts)):
|
||||
continue # not gentle: stays a hold
|
||||
if keep is not None and not (kind == "fix" and what in keep):
|
||||
continue # ALARMS muted: only the "while_muted" fixes
|
||||
@@ -1567,7 +1580,7 @@ class Engine:
|
||||
try_no = max(self.auto_tries[i][0] for i, _, _, _ in plan)
|
||||
log.info("AUTO %s: fixing %s (try %d)", t["id"], ", ".join("%s %s" % (k, w) for _, k, w, _ in plan), try_no)
|
||||
faults = [r.msg for k in self.keys_for(t["id"]) for r in [self.trackers[k].result]
|
||||
if self.trackers[k].confirmed and r.level == ERR]
|
||||
if self.trackers[k].confirmed and r.level in levels]
|
||||
for f in faults:
|
||||
self.auto_watch["%s %s" % (t["label"], f)] = now
|
||||
label = t["label"]
|
||||
|
||||
Reference in New Issue
Block a user