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()
+12 -2
View File
@@ -77,12 +77,15 @@
async function loadAll() {
const [c, s, d, b] = await Promise.all([api("/api/config"), api("/api/schema"), api("/api/draft"), api("/api/builders")]);
state.live = c.config; state.liveSha = c.sha; state.schema = s; state.draft = d.draft; state.draftStale = !!d.stale; state.atemPending = !!d.atem_pending; state.builders = b;
// What this page's draft was made from: a save carries it, so the Mac can put the change on top of
// settings that moved on meanwhile (an Apply elsewhere, a program update) instead of undoing them.
if (state.draft && !state.draft.base && !d.stale) { state.draft.base = c.sha; state.draft.baseConfig = deep(c.config); }
document.getElementById("sitename").textContent = (state.live.site || {}).name || "";
badge();
}
function ensureDraft() {
if (!needEditing()) return false;
if (!state.draft) state.draft = { config: deep(state.live), version: null, by: state.name };
if (!state.draft) state.draft = { config: deep(state.live), version: null, by: state.name, base: state.liveSha, baseConfig: deep(state.live) };
return true;
}
function changed() {
@@ -110,8 +113,15 @@
state.saving = new Promise((done) => {
state.saveTimer = setTimeout(async () => {
try {
const r = await post("/api/draft", { config: state.draft.config, expect: state.draft.version }, "PUT");
const r = await post("/api/draft", { config: state.draft.config, expect: state.draft.version, base: state.draft.base, base_config: state.draft.base ? state.draft.baseConfig : undefined }, "PUT");
state.draft.version = r.draft.version; state.draft.by = r.draft.by; state.draft.started = r.draft.started; state.draft.started_by = r.draft.started_by;
if (r.rebased) { // the settings moved on since this page loaded: keep both
const c = await api("/api/config");
state.live = c.config; state.liveSha = c.sha;
state.draft.config = r.draft.config; state.draft.base = c.sha; state.draft.baseConfig = deep(c.config);
badge(); route();
alertBox("The settings changed on the Mac since this page loaded (an Apply elsewhere, or a program update). Your changes were put on top of the new settings; nothing of either was lost.");
} else if (r.live_sha) { state.draft.base = r.live_sha; }
} catch (e) {
if (e.status === 409) { alertBox(`${e.body.error}: ${(e.body.changed || []).join(", ") || "the draft"}. The page now shows their version.`); state.draft = e.body.draft; route(); }
else if (e.status === 401 || e.status === 403) { alertBox(e.message); state.editing = null; state.name = null; store.set("sp-code", null); showEditing(); }