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
+29
View File
@@ -178,6 +178,35 @@ def page_name(host, port, page):
return r.read().decode()
def push(host, port, page, file_path, expected_name):
"""Upload a built page file and import it into `page` (the caller has checked safe_target)."""
data = open(file_path, "rb").read()
built = json.loads(gzip.decompress(data))
if (built.get("page") or {}).get("name") != expected_name:
raise ValueError("%s builds page %r, not %r" % (os.path.basename(file_path), (built.get("page") or {}).get("name"), expected_name))
ws = WebSocket(host, port, "/trpc")
try:
api = Trpc(ws)
for attempt in range(40):
try:
session = api.mutation("importExport.prepareImport.start", {"name": os.path.basename(file_path), "size": len(data)})
break
except RuntimeError as e:
if "already exists" not in str(e) or attempt == 39:
raise
time.sleep(3)
for offset in range(0, len(data), CHUNK):
api.mutation("importExport.prepareImport.uploadChunk",
{"sessionId": session, "offset": offset, "data": base64.b64encode(data[offset:offset + CHUNK]).decode()})
summary = api.mutation("importExport.prepareImport.complete",
{"sessionId": session, "expectedChecksum": hashlib.sha1(data).hexdigest(), "userData": None})
if isinstance(summary, list) and summary and summary[0]:
raise RuntimeError("Companion could not read the file: %s" % summary[0])
api.mutation("importExport.importSinglePage", {"targetPage": page, "sourcePage": 1, "connectionIdRemapping": {}})
finally:
ws.close()
def main():
cfg = json.load(open(os.path.join(HERE, "config.json")))
ap = argparse.ArgumentParser(description=__doc__.splitlines()[0])