Names check for a copied campus; the ATEM folder says when the page may not read Google Drive

Site setup > Companion lists every connection, Dante device and MIDI session the keys use that
this campus does not report, with a map-to choice that renames it across checks, restart keys
and fixes. The ATEM section explains the Full Disk Access step when the cloud folder looks empty
to the background page. Verified on NA Ingest; Install the staged folder tested live there too.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
2026-10-14 15:09:17 -04:00
co-authored by Claude Fable 5.1
parent a33e344cae
commit 3b3a1c7245
3 changed files with 97 additions and 4 deletions
+23 -2
View File
@@ -827,7 +827,7 @@
const plain = own.filter(([, f]) => !f.advanced), adv = own.filter(([, f]) => f.advanced);
let body = own.length ? `<div class="card"><h3>Settings</h3>${plain.map(fieldHtml).join("") || "<p class='muted small'>Only numbers here; they sit under Advanced.</p>"}${adv.length ? `<details><summary>Advanced</summary>${adv.map(fieldHtml).join("")}</details>` : ""}</div>` : "";
if (which === "site") body += await programHtml();
if (which === "companion") body = (await companionHtml()) + body;
if (which === "companion") body = (await namesHtml()) + (await companionHtml()) + body;
if (which === "schedules") body = schedulesHtml(c) + body;
if (which === "network-dante") body += await danteHtml(c);
if (which === "spotify") body = spotifyHtml(c) + body;
@@ -860,6 +860,25 @@
<div class="row"><a class="btn" href="/api/site/bundle" download>Export site bundle</a>${editing() ? `<label class="btn">New campus from a bundle <input type="file" id="bundlefile" accept=".zip,.json" hidden></label>` : ""}</div>
<p class="help">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.</p></div>`;
}
async function namesHtml() {
const n = await api("/api/site/names").catch(() => null);
if (!n || !n.missing) return "";
const sel = (kind, name, cands) => editing() ? `<select data-map="${kind}|${esc(name)}"><option value="">– map it to –</option>${cands.map((c) => `<option>${esc(c)}</option>`).join("")}</select>` : "";
const conns = n.connections.filter((c) => !c.live).map((c) => `<tr><td>${lightDot("problem")}<span class="mono">${esc(c.name)}</span></td><td class="small">${esc(c.keys.join(", "))}</td><td class="small">${n.companion_ok ? "not in this Companion" : "Companion not answering"}${c.module ? ` <span class="muted">(needs module ${esc(c.module)})</span>` : ""}</td><td>${sel("conn", c.name, c.candidates)}</td></tr>`).join("");
const dante = n.dante.filter((d) => !d.seen).map((d) => `<tr><td>${lightDot("warn")}<span class="mono">${esc(d.name)}</span></td><td class="small">DANTE</td><td class="small">not seen on the Dante network (last survey)</td><td>${sel("dante", d.name, d.candidates)}</td></tr>`).join("");
const midi = n.midi.filter((m) => !m.session_seen || !m.peer_seen).map((m) => `<tr><td>${lightDot("warn")}<span class="mono">${esc(m.session)} › ${esc(m.peer)}</span></td><td class="small">${esc(m.key)}</td><td class="small">${!m.session_seen ? `session not seen${m.host ? " on " + esc((cfg().hosts[m.host] || {}).name || m.host) : ""}${m.sessions_seen.length ? " (seen: " + esc(m.sessions_seen.join(", ")) + ")" : ""}` : `peer not in the session now${m.peers_seen.length ? " (in it: " + esc(m.peers_seen.join(", ")) + ")" : ""}`}</td><td class="small muted">change it on the key</td></tr>`).join("");
return `<div class="card"><h3>Names the keys use that are not here (${n.missing})</h3><p class="small muted">A copied campus, or something renamed. Map each name to what this campus has; the keys, checks and restart keys that use it follow.</p>
<div class="tablewrap"><table><tr><th>name</th><th>used by</th><th></th><th></th></tr>${conns}${dante}${midi}</table></div></div>`;
}
function renameConn(from, to) {
const d = state.draft.config;
for (const t of d.tiles) for (const c of t.checks || []) if (c.conn === from) c.conn = to;
if (d.restart_page) for (const k of d.restart_page.keys) k.watch = (k.watch || []).map((w) => (w === "conn:" + from ? "conn:" + to : w));
for (const f of Object.values(d.fixes)) if (f.conn === from) f.conn = to;
}
function renameDante(from, to) {
for (const t of state.draft.config.tiles) for (const c of t.checks || []) if (c.type === "dante" && c.devices && from in c.devices) { c.devices[to] = c.devices[from]; delete c.devices[from]; if (c.fix && typeof c.fix === "object" && from in c.fix) { c.fix[to] = c.fix[from]; delete c.fix[from]; } }
}
async function companionHtml() {
const cc = await api("/api/companion");
const lights = Object.entries(cc.lights || {}).map(([k, v]) => `<li>${lightDot(v === true ? "ok" : v === false ? "problem" : "unknown")}${esc(k)}</li>`).join("");
@@ -893,10 +912,12 @@
const pending = state.atemPending ? `<div class="notice">A new saved switcher file is waiting for Apply. ${editing() ? `<button class="small" id="atemdiscard">Discard it</button>` : ""}</div>` : "";
return `<div class="card"><h3>The saved switcher file</h3><p class="small muted">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.</p>${pending}
${editing() ? `<div class="row"><label class="btn">Upload a saved file <input type="file" id="atemfile" accept=".xml" hidden></label><button id="atemnewest" ${folder.mounted && (folder.files || []).length ? "" : "disabled"}>Pick the newest from the folder</button></div>` : ""}
<p class="small">Folder: <span class="mono">${esc(folder.folder || "(not set)")}</span> ${folder.mounted ? `· newest: ${(folder.files || []).length ? esc(folder.files[0].name) + " (" + esc(when(folder.files[0].when)) + ")" : "none"}` : "· <b>not mounted</b>"}</p><p id="ateminfo" class="help"></p></div>`;
<p class="small">Folder: <span class="mono">${esc(folder.folder || "(not set)")}</span> ${folder.mounted ? `· newest: ${(folder.files || []).length ? esc(folder.files[0].name) + " (" + esc(when(folder.files[0].when)) + ")" : "none"}` : "· <b>not mounted</b>"}</p>
${folder.blocked ? `<div class="notice grey">The folder is there but this page is not allowed to read Google Drive (macOS keeps background programs out of cloud folders). Once, on the Ingest Mac: System Settings › Privacy & Security › Full Disk Access › add <span class="mono">/usr/bin/python3</span>, then restart the settings page from the Overview. Until then, use Upload a saved file.</div>` : ""}<p id="ateminfo" class="help"></p></div>`;
}
function wireSite() {
const q = (s) => main.querySelector(s);
for (const m of main.querySelectorAll("[data-map]")) m.onchange = () => { if (!m.value || !ensureDraft()) return; const [kind, name] = m.dataset.map.split("|"); (kind === "conn" ? renameConn : renameDante)(name, m.value); saveDraft(); route(); };
for (const b of main.querySelectorAll("[data-addconn]")) b.onclick = () => { const conn = b.dataset.addconn; if (!ensureDraft()) return; const d = state.draft.config; let id = slug(conn) || "key", n = 2; while (d.tiles.some((t) => t.id === id)) id = slug(conn) + "_" + n++; const t = { id, label: conn.toUpperCase().replace(/[_-]+/g, " ").slice(0, 10).trim(), checks: [{ type: "conn", conn, fix: "restart-connection" }] }; const pos = firstEmpty(d, "status"); if (pos) t.pos = pos; d.tiles.push(t); saveDraft(); location.hash = "#keys/" + id; };
for (const b of main.querySelectorAll("[data-addwin]")) b.onclick = () => { if (!ensureDraft()) return; const path = b.dataset.addwin; const list = (getPath(path, state.draft.config) || []).slice(); list.push({ days: ["sun"], start: "09:00", end: "12:00" }); setPath(path, list); route(); };
for (const b of main.querySelectorAll("[data-delwin]")) b.onclick = () => { if (!ensureDraft()) return; const path = b.dataset.delwin.replace(/\.\d+$/, ""), i = +b.dataset.delwin.split(".").pop(); const list = (getPath(path, state.draft.config) || []).slice(); list.splice(i, 1); setPath(path, list); route(); };