NA: ALL AUTO-FIX key pauses or resumes auto-fix at every location; page keys keep their words under the band; rc4 ProP module at WS/SC

- LOCATIONS page key at 3,1: shows ALL ON or the paused locations; tap pauses all, tap again sends each back to its hours
- autofix auto: back to the auto-fix hours
- the page name and number shrink into the top 60% while the locations band shows
- WS and SC Propresenter-API on renewedvision-propresenter-api 1.2.0-rc4 (pinned manual, as NA)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 15:12:10 -04:00
co-authored by Claude Opus 5.5
parent 2a37cee10a
commit 3428bd81a2
7 changed files with 96 additions and 6 deletions
+10 -1
View File
@@ -269,13 +269,18 @@ REMOTE_VAR = "sp_remote" # North Attleboro's summary of the other locat
BAND_BLUE, BAND_DARK = 0x1E6FFF, 0x0B2A66
def band_on():
r = "$(custom:%s)" % REMOTE_VAR
return "$(internal:time_unix) - jsonpath(%s, '$.t') < 30 && jsonpath(%s, '$.n') > 0" % (r, r)
def locations_band(flash_var):
"""A blue band across the bottom of NA's page-number key naming the other locations with a problem
("SC", "SC WS"): it flashes until that location acknowledges, then stays. Red stays NA's own
(2026-10-21, the user: "i dont want the NA page buttons to flash red, that should he reserved for on site
problems")."""
r = "$(custom:%s)" % REMOTE_VAR
on = "$(internal:time_unix) - jsonpath(%s, '$.t') < 30 && jsonpath(%s, '$.n') > 0" % (r, r)
on = band_on()
color = "jsonpath(%s, '$.f') == 1 && $(custom:%s) == 1 ? %d : %d" % (r, flash_var, BAND_DARK, BAND_BLUE)
box = {"id": stable_id("nav", "band_box"), "name": "Locations", "usage": "auto", "type": "box", "enabled": prop(on, True),
"opacity": prop(100), "x": prop(0), "y": prop(60), "width": prop(100), "height": prop(40), "rotation": prop(0),
@@ -314,6 +319,10 @@ def nav_key(kind, names, status_page=None, hold_ms=1000, tap_page=1, cfg=None):
paint(layer.get("children", []))
paint(key["style"]["layers"])
if kind == "pagenum" and (cfg.get("nav_locations_band") or False):
# While the band shows, the page's own name and number shrink into the top 60%, so nothing is hidden.
for layer in key["style"]["layers"]:
if layer["type"] == "group":
layer["height"] = prop("(%s) ? 60 : 100" % band_on(), True)
key["style"]["layers"] += locations_band(opts["flash"])
if kind == "pagenum" and status_page:
step = key["steps"]["0"]
Binary file not shown.
Binary file not shown.
+69 -3
View File
@@ -74,9 +74,9 @@ def me(cfg, url=None):
"user": pwd.getpwuid(os.getuid()).pw_name}
# Bumped when the pages are built differently (2: "campus" became "location", 2026-10-21), so the hub
# Bumped when the pages are built differently (2: "campus" became "location"; 3: ALL AUTO-FIX key, 2026-10-21), so the hub
# re-imports them.
PAGES_VERSION = 2
PAGES_VERSION = 3
def layout_sha(cfg):
@@ -105,6 +105,9 @@ def http_json(url, body=None, timeout=3):
VIEW = {"state": "sp_state_view", "heartbeat": "sp_hb_view"}
REMOTE = make_page.REMOTE_VAR # which other locations have a problem: the blue band on NA's page-number keys
ALL_CMD = "sp_all_cmd" # a tap on NA's ALL AUTO-FIX key (LOCATIONS page)
ALL_AF = "sp_all_af" # what that key shows: {"t", "x", "paused"}
ALL_AF_POS = (3, 1) # where AUTO-FIX sits on every location's own status page
SELECT = "sp_view"
VIEW_CMD = "sp_view_cmd" # a tap or hold on the LOCATION page, passed on to that location's own watchdog
MODE = "sp_view_mode" # "" = that location's status page, "RESTART" = its Restart page
@@ -199,14 +202,51 @@ def build_overview(local_cfg, campuses, page, view_page):
make_page.place(controls, (0, col), key)
if restart:
make_page.place(controls, (1, col), restart)
make_page.place(controls, ALL_AF_POS, all_autofix_key(local_cfg))
return make_page.page_export("%d\\nLocations" % page, page, controls, local_cfg)
def all_autofix_key(local_cfg):
"""Tap: pause auto-fix at every location at once (NA too); tap again: every location back to its hours
(2026-10-21, the user: "add a key on NA to pause auto-fix at all locations"). Blue while none is paused,
grey naming the paused ones."""
v = "$(custom:%s)" % ALL_AF
stale = "$(internal:time_unix) - jsonpath(%s, '$.t') > 30" % v
key = make_page.static_button("all_autofix", "AUTO-FIX\\nALL", 0x0B57D0, 0xFFFFFF, 1,
"Tap: pause auto-fix at every location (alarms stay on, holds still fix); tap again: all "
"back to their auto-fix hours.", "STATUS tile 'all autofix'")
for layer in key["style"]["layers"]:
if layer["type"] == "box":
layer["color"] = make_page.prop("%s ? %d : (jsonpath(%s, '$.paused') > 0 ? %d : %d)" % (stale, make_page.PURPLE, v, 0x262626, 0x0B57D0), True)
elif layer["type"] == "text":
layer["text"] = make_page.prop("%s ? `AUTO-FIX\\nALL ?` : `AUTO-FIX\\n${jsonpath(%s, '$.x')}`" % (stale, v), True)
key["steps"]["0"]["action_sets"]["up"] = [make_page.command_action("all_autofix", "press", ALL_CMD, "toggle $(internal:time_unix)")]
return key
def autofix_paused(state_json):
"""True when that location's AUTO-FIX key says it was paused by hand ("AUTO-FIX\\nOFF\\nTIL ..."), None without one."""
try:
af = json.loads(state_json or "{}").get("autofix")
except ValueError:
return None
if not af:
return None
lines = str(af.get("x") or "").split("\n")
return len(lines) > 2 and lines[1] == "OFF" and lines[2].startswith("TIL")
def all_autofix_summary(states, now):
"""states: {id: that location's /api/state "state" JSON}. -> what NA's ALL AUTO-FIX key shows."""
paused = sorted(cid.upper() for cid, s in states.items() if autofix_paused(s))
return {"t": int(now), "paused": len(paused), "x": ("%s\nPAUSED" % " ".join(paused)) if paused else "ALL ON"}
def ensure_variables(cfg, ids):
from settings import campus
have, _ = campus.companion_state(cfg)
want = [n for cid in ids for n in ("sp_state_" + cid, "sp_hb_" + cid) if n not in have]
want += [n for n in (VIEW["state"], VIEW["heartbeat"], SELECT, VIEW_CMD, MODE, REMOTE) if n not in have]
want += [n for n in (VIEW["state"], VIEW["heartbeat"], SELECT, VIEW_CMD, MODE, REMOTE, ALL_CMD, ALL_AF) if n not in have]
if not want:
return
comp = cfg.get("companion") or {}
@@ -342,6 +382,31 @@ class Mirror:
save(self.site.data_dir, data)
return data
def all_autofix(self, cfg, mine, others):
"""NA's ALL AUTO-FIX key: show which locations are paused; a tap pauses them all, or resumes them all."""
states = {cid: (self.remote.get(cid) or {}).get("state") for cid in others}
states[mine] = self.site.comp.get_custom((cfg.get("variables") or {}).get("state", "sp_state"))
summary = all_autofix_summary(states, time.time())
self.site.comp.set_custom(ALL_AF, json.dumps(summary, separators=(",", ":")))
text = (self.site.comp.get_custom(ALL_CMD) or "").strip(" ;")
if not text:
return
self.site.comp.set_custom(ALL_CMD, "")
stamps = [w for w in text.replace(";", " ").split() if w.isdigit()]
if not stamps or time.time() - int(stamps[-1]) > 30:
return # pressed while this was down: not acted on later
word = "auto" if summary["paused"] else "off"
hosts = {cid: urllib.parse.urlparse(e["url"]).hostname for cid, e in others.items()}
hosts[mine] = "127.0.0.1"
for cid, host in sorted(hosts.items()):
try:
comp = self.site.comp if cid == mine else sp.Companion(host, 8000, timeout=3)
before = (comp.get_custom("sp_cmd") or "").strip()
comp.set_custom("sp_cmd", ";".join([before, "autofix " + word] if before else ["autofix " + word]))
log.info("ALL AUTO-FIX: %s %s", cid, "paused" if word == "off" else "back to its hours")
except OSError as e:
log.warning("ALL AUTO-FIX: %s not reached: %s", cid, e)
def pass_on(self, pick, others, restart=False):
"""A tap or hold on the LOCATION page goes to the shown location's watchdog, through its Companion."""
text = (self.site.comp.get_custom(VIEW_CMD) or "").strip()
@@ -388,6 +453,7 @@ class Mirror:
t.join(2)
if (data.get("pages") or {}).get("overview"):
self.site.comp.set_custom(REMOTE, json.dumps(remote_summary(others, self.remote, time.time()), separators=(",", ":")))
self.all_autofix(cfg, mine, others)
# The LOCATION page shows the tapped location: its keys moved onto their cells.
pick = (self.site.comp.get_custom(SELECT) or "").strip().lower() # held as "WS": the back key shows it
if pick not in others and others:
Binary file not shown.
+3 -2
View File
@@ -3694,9 +3694,10 @@ def handle_commands(text, engine, comp, snap, now):
engine.service.set(target, now)
log.info("SERVICE %s (button)", "ON" if target else "OFF")
elif verb == "autofix":
target = {"on": True, "off": False}.get(arg, not engine.autofix.on(now))
# "auto": back to the auto-fix hours (NA's ALL AUTO-FIX key resuming every location).
target = {"on": True, "off": False, "auto": engine.autofix.scheduled(now)}.get(arg, not engine.autofix.on(now))
engine.autofix.set(target, now)
log.info("AUTO-FIX %s (button)", "ON" if target else "OFF")
log.info("AUTO-FIX %s (%s)", "ON" if target else "OFF", "back to its hours" if arg == "auto" else "button")
elif verb in ("restart", "force"):
# Holds carry Companion's clock ("force rs_x 1791496225"). One pressed while the watchdog
# was down must not fire minutes later in a burst with the others.
+14
View File
@@ -602,3 +602,17 @@ class TestLocationsBand(unittest.TestCase):
band = [l for l in make_page.nav_key("pagenum", na["variables"], 19, cfg=dict(na, nav_locations_band=True))["style"]["layers"]
if l.get("name") == "Locations"][0]
self.assertNotIn(str(0xFF0000), json.dumps(band)) # never red
class TestAllAutoFix(unittest.TestCase):
def test_the_key_names_the_paused_locations_and_sits_where_auto_fix_does(self):
from settings import campuses as cv
st = lambda x: json.dumps({"autofix": {"x": x}})
states = {"na": st("AUTO-FIX\nON\nTIL 9PM"), "sc": st("AUTO-FIX\nOFF\nTIL SUN 5AM"), "ws": st("AUTO-FIX\nOFF"), "ab": None}
self.assertEqual(cv.all_autofix_summary(states, 100.0), {"t": 100, "paused": 1, "x": "SC\nPAUSED"}) # WS: outside its hours
self.assertEqual(cv.all_autofix_summary({"na": st("AUTO-FIX\nON\nTIL 9PM")}, 100.0)["x"], "ALL ON")
na = json.load(open(os.path.join(ROOT, "config.json")))
page = cv.build_overview(na, {"na": {"name": "North Attleboro"}, "sc": {"name": "Southcoast"}}, 98, 99)
key = page["page"]["controls"]["3"]["1"]
self.assertIn("all autofix", key["options"]["notes"])
self.assertIn("sp_all_cmd", json.dumps(key["steps"]))