diff --git a/settings/server.py b/settings/server.py index 46f8e50..7873113 100644 --- a/settings/server.py +++ b/settings/server.py @@ -644,12 +644,21 @@ class Handler(http.server.BaseHTTPRequestHandler): return self.send(200, spotify_info(url)) if path == "/api/atem-reference/folder": folder = os.path.expanduser((site.cfg.get("atem") or {}).get("source_folder") or "") - files = sorted(glob.glob(os.path.join(folder, "*.xml")), key=os.path.getmtime, reverse=True) if folder and os.path.isdir(folder) else [] - return self.send(200, {"folder": folder, "mounted": bool(folder and os.path.isdir(folder)), + mounted = bool(folder and os.path.isdir(folder)) + files = sorted(glob.glob(os.path.join(folder, "*.xml")), key=os.path.getmtime, reverse=True) if mounted else [] + blocked = False + if mounted and not files: + try: + blocked = not os.listdir(folder) and "/Library/CloudStorage/" in folder # a cloud folder that looks empty to a background process + except OSError: + blocked = True + return self.send(200, {"folder": folder, "mounted": mounted, "blocked": blocked, "files": [{"name": os.path.basename(f), "when": os.path.getmtime(f)} for f in files[:10]]}) if path == "/api/live/midi": snap = site.read_json("snapshot.json", {}) or {} return self.send(200, {"local": snap.get("midi"), "remote": snap.get("remote_midi")}) + if path == "/api/site/names": + return self.send(200, self.names_check((site.draft.read() or {}).get("config") or site.load())) if path == "/api/setup": return self.send(200, {"computers": scripts.setup_all(site.load())}) if path == "/api/program/staged": @@ -878,6 +887,51 @@ class Handler(http.server.BaseHTTPRequestHandler): log.exception("%s %s", self.command, path) return self.send(500, {"error": str(e)}) + def names_check(self, cfg): + """Every device name the keys use, against what this Companion, the Dante network and the Macs report now: + the mapping step of a copied campus.""" + site = self.site + try: + live = {c["label"]: c.get("moduleId") for c in site.comp.connections()} + companion_ok = True + except Exception: + live, companion_ok = {}, False + used = {} + for t in cfg.get("tiles", []): + for c in t.get("checks", []): + if c.get("conn") and c["type"] != "dante" or (c["type"] == "dante" and c.get("conn")): + used.setdefault(c["conn"], {"keys": set(), "module": schema.EXPECTED_MODULE.get(c["type"])})["keys"].add(t.get("label", t["id"])) + for k in (cfg.get("restart_page") or {}).get("keys", []): + for w in k.get("watch", []): + if w.startswith("conn:"): + used.setdefault(w[5:], {"keys": set(), "module": None})["keys"].add("restart key " + k.get("label", k["id"])) + conns = [] + for name, u in sorted(used.items()): + cands = [l for l, m in live.items() if l not in used and (not u["module"] or not m or m == u["module"])] + conns.append({"name": name, "keys": sorted(u["keys"]), "live": name in live, "module": u["module"], + "live_module": live.get(name), "candidates": sorted(cands)}) + snap = site.read_json("snapshot.json", {}) or {} + dmap = snap.get("dante_map") or {} + dante = [] + for t in cfg.get("tiles", []): + for c in t.get("checks", []): + if c["type"] == "dante": + for n in c.get("devices", {}): + dante.append({"name": n, "seen": n in dmap, "candidates": sorted(x for x in dmap if x not in c["devices"])}) + local, remote = snap.get("midi") or {}, snap.get("remote_midi") or {} + midi = [] + for t in cfg.get("tiles", []): + for c in t.get("checks", []): + if c["type"] == "midi_peer": + src = (remote.get(c["host"]) or {}) if c.get("host") else local + sess = src.get(c.get("session")) if isinstance(src, dict) else None + peers = [p.get("name") for p in (sess or {}).get("peers", [])] if sess else [] + midi.append({"session": c.get("session"), "peer": c.get("peer"), "host": c.get("host"), "key": t.get("label", t["id"]), + "session_seen": sess is not None, "peer_seen": c.get("peer") in peers, + "sessions_seen": sorted(src) if isinstance(src, dict) else [], "peers_seen": peers}) + return {"companion_ok": companion_ok, "connections": conns, "dante": dante, "midi": midi, + "missing": sum(1 for c in conns if not c["live"]) + sum(1 for d in dante if not d["seen"]) + sum(1 for m in midi if not m["session_seen"] or not m["peer_seen"])} + def staged(self): """The staged folder (~/status-panel-install) against the running code.""" src = os.path.expanduser("~/status-panel-install") diff --git a/settings/ui/app.js b/settings/ui/app.js index 22b542e..4e3cad3 100644 --- a/settings/ui/app.js +++ b/settings/ui/app.js @@ -827,7 +827,7 @@ const plain = own.filter(([, f]) => !f.advanced), adv = own.filter(([, f]) => f.advanced); let body = own.length ? `
Only numbers here; they sit under Advanced.
"}${adv.length ? `New campus from a bundle: this campus's name, addresses and editing code stay; every computer from the bundle is flagged "copied from … - check it" until you touch it; keys, checks and fixes come along. Then Review & Apply.
`; } + async function namesHtml() { + const n = await api("/api/site/names").catch(() => null); + if (!n || !n.missing) return ""; + const sel = (kind, name, cands) => editing() ? `` : ""; + const conns = n.connections.filter((c) => !c.live).map((c) => `A copied campus, or something renamed. Map each name to what this campus has; the keys, checks and restart keys that use it follow.
+| name | used by |
|---|
The panel keeps the switcher's audio and camera settings in line with this file (atem-reference.xml). Save a new one from ATEM Software Control when the show changes.
${pending} ${editing() ? `` : ""} -Folder: ${esc(folder.folder || "(not set)")} ${folder.mounted ? `· newest: ${(folder.files || []).length ? esc(folder.files[0].name) + " (" + esc(when(folder.files[0].when)) + ")" : "none"}` : "· not mounted"}
Folder: ${esc(folder.folder || "(not set)")} ${folder.mounted ? `· newest: ${(folder.files || []).length ? esc(folder.files[0].name) + " (" + esc(when(folder.files[0].when)) + ")" : "none"}` : "· not mounted"}
+ ${folder.blocked ? `