Step 2: the settings page can change things - shared draft, Review & Apply, rollback, history, undo

- settings/apply.py: the Apply pipeline (check, keep history, write, restart the watchdog, re-import
  pages by the marker-only rule, recolour page keys), rollback when the watchdog does not come back,
  the recording guard with the Emergency (LIVE) override, history entries and Go back / Undo.
- settings/server.py: the write routes behind the editing code (draft PUT/DELETE with 409 on a
  conflict, validate, apply, restore, undo, watchdog restart/stop/start, commands to the watchdog,
  the switcher file upload / pick newest / discard, gear notes).
- settings/ui/app.js: every screen grows inputs with Editing on; keys (checks, fixes, hold-always,
  members), computers, fixes, site setup (schedules, Dante devices, Spotify, ATEM), Review & Apply
  with live progress, History with Go back and Undo, Run this fix now.
- schema.unmask, push_page.push, nav_keys.colour; tests for all of it (163 pass).
- Verified live on NA Ingest: a page-only Apply, a restart Apply, Undo, history.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 14:06:42 -04:00
co-authored by Claude Fable 5.1
parent ae3e85f152
commit 02d0c77cd9
10 changed files with 1686 additions and 145 deletions
+22
View File
@@ -644,6 +644,28 @@ def mask_secrets(obj):
return walk(copy.deepcopy(obj), "")
def unmask(draft, live):
"""A draft that came back from the page carries masked secrets ("set (N characters)"): put the
live values back so a masked value never overwrites a real one."""
secret_paths = {p for p, f in F.items() if f["secret"]}
def walk(node, ref, path):
if isinstance(node, dict):
out = {}
for k, v in node.items():
p = "%s.%s" % (path, k) if path else k
if (p in secret_paths or k in secret_paths) and isinstance(v, str) and (v.startswith("set (") or v == "") \
and isinstance(ref, dict) and isinstance(ref.get(k), str) and ref.get(k):
out[k] = ref[k] if v.startswith("set (") else ""
else:
out[k] = walk(v, ref.get(k) if isinstance(ref, dict) else None, p)
return out
if isinstance(node, list):
return [walk(v, ref[i] if isinstance(ref, list) and i < len(ref) else None, path + "[]") for i, v in enumerate(node)]
return node
return walk(copy.deepcopy(draft), live, "")
def phrases(cfg):
"""The sentences the page and the logs build from the config."""
a = cfg.get("auto_fix") or {}