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
+11 -1
View File
@@ -863,12 +863,22 @@ class Handler(http.server.BaseHTTPRequestHandler):
return
if path == "/api/draft" and self.command == "PUT":
cfg = schema.unmask(body.get("config") or {}, site.load())
# A page that loaded the settings before they changed here (an Apply from another tab, a
# program update) sends what it started from: only its own changes are put on top of the
# live settings, so newer values are never quietly undone (2026-10-15).
rebased = False
if body.get("base") and body["base"] != site.cfg_sha and isinstance(body.get("base_config"), dict):
base_cfg = schema.unmask(body["base_config"], site.load())
cfg, _ = applying.rebase({"config": cfg}, site.load(), base_cfg)
rebased = True
log.info("draft from %s rebased: the page started from %s, the settings are now %s",
who, str(body["base"])[:10], (site.cfg_sha or "")[:10])
draft, conflict = site.draft.update(cfg, who, site.cfg_sha, body.get("expect"))
if conflict:
changed = applying.changed_paths(cfg, conflict["config"])
return self.send(409, {"error": "%s changed this a moment ago" % conflict.get("by", "someone"), "draft": self.masked_draft(conflict),
"changed": [applying.words_for(p, a, b, cfg, conflict["config"])["where"] for p, a, b in changed[:5]]})
return self.send(200, {"draft": self.masked_draft(draft)})
return self.send(200, {"draft": self.masked_draft(draft), "rebased": rebased, "live_sha": site.cfg_sha})
if path == "/api/draft" and self.command == "DELETE":
site.draft.clear()
self.discard_atem()