Never undo settings applied on the page: sync from NA Ingest, rebase stale drafts, guard the installer

- config.json is NA Ingest's live settings: the READY hours (Tue 14-22, Wed 9-19, Sun 5-14) and the
  auto-fix hours (Tue 14-22, Wed 9-21, Sun 6:30-14) set on the settings page.
- Settings page: a draft carries what its page started from. A tab that loaded the settings before
  they changed (an Apply elsewhere, a program update) now has only its own edits put on top of the
  live settings; before, it saved its whole older copy and Apply quietly undid the newer values
  (it took ready.prop_host out twice today).
- install.sh refuses (exit 4) to replace live settings that came from an Apply (their fingerprint is
  a history entry's sha_after) unless --replace-config; --keep-config still updates code only.
- Tests pin their own auto-fix and READY hours, so the campus can change its own; the exit-timeout
  test expects 310 s (the Tascam outlet power-cycle).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-15 15:02:17 -04:00
co-authored by Claude Opus 5.5
parent c9ffbc6828
commit 98739bd041
8 changed files with 1702 additions and 1613 deletions
+2
View File
@@ -19,6 +19,8 @@ sp.log.addHandler(logging.NullHandler())
sp.log.propagate = False
CONFIG = sp.load_config(os.path.join(ROOT, "config.json"))
# The tests pin their own READY hours (the campus sets its own on the settings page).
CONFIG["ready"]["due"] = [{"days": ["sat"], "start": "12:00", "end": "23:59"}, {"days": ["sun"], "start": "00:00", "end": "13:30"}]
RC = CONFIG["ready"]
WEDNESDAY = datetime(2026, 10, 14, 15, 0).timestamp() # the week before the plan: gaps are amber
SATURDAY = datetime(2026, 10, 17, 16, 0).timestamp() # inside "due": gaps are red
+3 -1
View File
@@ -295,7 +295,9 @@ class TestSchemaMatchesCode(unittest.TestCase):
def test_phrases(self):
cfg = sp.load_config(os.path.join(ROOT, "config.json"))
p = schema.phrases(cfg)
self.assertEqual(p["auto_fix"], "may run by itself on Sundays 06:30-13:30")
self.assertEqual(p["auto_fix"], "may run by itself " + sp.windows_phrase(cfg["auto_fix"]["windows"])) # the campus's own hours
self.assertEqual(schema.phrases(dict(cfg, auto_fix={"windows": [{"days": ["sun"], "start": "06:30", "end": "13:30"}]}))["auto_fix"],
"may run by itself on Sundays 06:30-13:30")
self.assertEqual(p["live"], "changes wait while a HyperDeck is recording")
self.assertEqual(p["hold"], "HOLD 2s")
self.assertEqual(sp.windows_phrase([{"days": ["sat", "sun"], "start": "18:00", "end": "00:30"}]),
+21
View File
@@ -291,6 +291,27 @@ class TestServer(unittest.TestCase):
self.assertEqual(len(v["changes"]), 1); self.assertTrue(v["changes"][0]["text"].startswith("computer Lighting PC › notes"))
self.put("/api/draft", {}, method="DELETE")
def test_a_page_that_loaded_before_a_change_never_undoes_it(self):
"""2026-10-15: a tab loaded the settings, a program update then added ready.prop_host, the tab
changed one window and saved its whole (older) copy - Apply took prop_host out again. Now the
page says what it started from and only its own change goes on top of the live settings."""
import hashlib
before = self.site.load(); base_sha = self.site.cfg_sha
newer = json.loads(json.dumps(before)); newer["debounce_seconds"] = (before.get("debounce_seconds") or 3) + 2
sp.write_json(self.site.config_path, newer) # changed under the page
self.site.load()
page = json.loads(json.dumps(before)); page["flap"] = dict(before.get("flap") or {}, count=7) # the page's own edit
st, r = self.put("/api/draft", {"config": page, "base": base_sha, "base_config": before}, who="christian")
self.assertTrue(r["rebased"])
d = self.site.draft.read()["config"]
self.assertEqual(d["flap"]["count"], 7) # the page's change
self.assertEqual(d["debounce_seconds"], newer["debounce_seconds"]) # the newer value kept
st, r = self.put("/api/draft", {"config": d, "base": self.site.cfg_sha, "base_config": d}, who="christian")
self.assertFalse(r["rebased"]) # up to date: nothing to do
self.put("/api/draft", {}, method="DELETE")
sp.write_json(self.site.config_path, before)
self.site.load()
def test_probe_is_refused_while_a_fix_runs(self):
json.dump({"fix-x": {"key": "hd2"}}, open(os.path.join(self.data, "fixes-running.json"), "w"))
try:
+4 -1
View File
@@ -30,6 +30,8 @@ CONFIG["tiles"] = [dict(t, label="SERVICE") if t["id"] == "service" else t for t
# A Sunday 08:00 (inside the service window) and a Wednesday 15:00 (outside it).
SUNDAY = datetime(2026, 10, 11, 8, 0).timestamp()
CONFIG["auto_fix"]["log_dir"] = tempfile.mkdtemp(prefix="sp-autolog-") # never the real Desktop
# ...and their own auto-fix hours, so the campus can change its own on the settings page.
CONFIG["auto_fix"]["windows"] = [{"days": ["wed"], "start": "16:00", "end": "21:00"}, {"days": ["sun"], "start": "06:30", "end": "13:30"}]
WEDNESDAY = datetime(2026, 10, 7, 15, 0).timestamp()
@@ -1447,7 +1449,8 @@ class TestFixes(unittest.TestCase):
for t in self.engine.fix_threads:
t.join(2)
self.assertEqual(self.engine.running, {})
self.assertEqual(sp.exit_timeout(CONFIG), 90) # the longest fix + its verify + 10
# the longest fix + its verify + 10: the Tascam outlet power-cycle (timeout 300)
self.assertEqual(sp.exit_timeout(CONFIG), 310)
def test_a_hyperdeck_switched_off_is_always_switched_back_on(self):
calls = []