"Location", never "campus", in everything people read; SC FOH joined with its MIDI links

- pages (NA 98 Locations / 99 Location, re-imported via PAGES_VERSION), keys, settings page, setup and
  install messages, alerts, docs, the Apollo Beach kit; python3 -m settings.location and /location/...
  addresses (the old names still answer for installed Macs)
- Apply reads the before settings from the file: an in-place edit skipped the watchdog restart
- SC: SUPERRACK (+SoundGrid) and X32 EDIT from SC FOH; FOH TRACKS and TRACKS LIGHTS MIDI links watched

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-21 13:42:22 -04:00
co-authored by Claude Opus 5.5
parent d0c755824b
commit 7068244ba4
31 changed files with 951 additions and 788 deletions
+12 -12
View File
@@ -858,7 +858,7 @@
<li>Settings › MIDI links: add one to the MIDI key; Run the fix on that check.</li>
<li>Keys › Fixes › VISTA: change the program file to Vista2.exe, watch the command rebuild; put it back.</li>
<li>Computers › Setup › Lighting PC: open the script, Copy. Computers › Lighting PC › Remove (read, then Cancel).</li>
<li>Settings › Switcher: Pick the newest file; Discard. Settings › Program & campus: Export bundle.</li>
<li>Settings › Switcher: Pick the newest file; Discard. Settings › Program & location: Export bundle.</li>
<li>A real change (a note on Computers › Gear): APPLY, then History › Undo, APPLY again.</li></ol></details>
<details><summary><b>The words a key can show</b></summary>${glossary}</details>
<details><summary><b>Only a person can</b></summary><ul class="small"><li>Run the set-up script on a new computer, allow Accessibility on the Studio B Mac, click Allow on the Spotify prompt once.</li><li>Create Companion's custom variables and the blink trigger, add pages, install modules, name the Stream Decks.</li><li>Type the Netgear login into the keychain; keep Google Drive signed in.</li><li>Save a new switcher file from ATEM Software Control when the show changes, then pick it under Settings › Switcher.</li></ul></details>
@@ -882,9 +882,9 @@
["outlets", "Outlets", "Smart plugs the panel may switch, for a device that only a power cycle brings back"],
["companion", "Companion", "Connections, and names the keys use that are not here"],
["network", "Network", "This Mac's network port and the Dante switches"],
["setup", "Auto-setup", "What this Mac found, the line each computer runs to join, and the line that sets up another campus"],
["site", "Site & PIN", "The campus, contacts, this Mac, the PIN"],
["program", "Program & campus", "Update the code, export or start a campus"],
["setup", "Auto-setup", "What this Mac found, the line each computer runs to join, and the line that sets up another location"],
["site", "Site & PIN", "The location, contacts, this Mac, the PIN"],
["program", "Program & location", "Update the code, export or start a location"],
["advanced", "Advanced", "Every number and name, by screen"],
];
async function settings(rest) {
@@ -917,7 +917,7 @@
wireSettings(which);
}
function readySettingsHtml(c) {
if (!c.ready_page) return "<p class='muted'>This campus has no READY page.</p>";
if (!c.ready_page) return "<p class='muted'>This location has no READY page.</p>";
const lists = JSON.stringify((c.ready || {}).prop_lists || [], null, 1);
return `<div class="card"><p style="margin:0">${onOff("ready", c.ready || {}, "the READY page")} ${(c.ready || {}).disabled ? "<b>The READY page is off:</b> its keys show OFF and nothing is looked at. Everything is kept." : "<b>The READY page</b> looks at this weekend every 2 minutes."}</p></div><div class="card"><h3 style="margin-top:0">When gaps turn red${hint("Before this, a READY key that is not done is amber; inside it, still missing is red and blinks")}</h3>${calendarHtml("ready.due", (c.ready || {}).due)}</div>`
+ fieldsHtml("Ready")
@@ -935,7 +935,7 @@
const screens = [...new Set(Object.values(state.schema.fields).map((f) => f.screen))];
return screens.map((s) => { const h = fieldsHtml(s, true); return h ? `<h3>${esc(s)}</h3>${h}` : ""; }).join("") || "<p class='muted'>Nothing.</p>";
}
// Auto-setup: what was found and added, the join lines, and the install line for another campus.
// Auto-setup: what was found and added, the join lines, and the install line for another location.
async function setupHtml(c) {
const d = await api("/api/discover").catch(() => null);
if (!d) return "<p class='muted'>Auto-setup is not answering.</p>";
@@ -944,7 +944,7 @@
const comps = Object.entries(inv.computers || {}).map(([a, x]) => `<tr><td>${lightDot(x.reachable ? "ok" : "problem", x.reachable ? "logs in" : x.error || "does not let this Mac in yet")}${esc(x.name || a)}</td><td class="small">${esc(x.os || "")}</td><td class="mono small">${esc(x.ip || "")}</td><td class="small muted">${x.reachable ? "" : esc(x.error || "run the join line on it")}</td></tr>`).join("");
const joined = Object.entries(d.joined || {}).filter(([n]) => !Object.values(inv.computers || {}).some((x) => x.name === n)).map(([n, j]) => `<tr><td>${lightDot("warn", "joined; the next look sets it up")}${esc(n)}</td><td class="small">${esc(j.os)}</td><td class="mono small">${esc((j.ips || [])[0] || "")}</td><td class="small muted">joined ${esc(when(j.at))}</td></tr>`).join("");
const list = (xs) => xs && xs.length ? `<ul class="small">${xs.map((x) => `<li>${esc(x)}</li>`).join("")}</ul>` : "<p class='small muted'>nothing</p>";
const campuses = [["ab", "Apollo Beach"], ["sc", "Southcoast"], ["ws", "Woonsocket"], ["na", "North Attleboro"]].filter(([k]) => k !== (c.site || {}).id);
const locations = [["ab", "Apollo Beach"], ["sc", "Southcoast"], ["ws", "Woonsocket"], ["na", "North Attleboro"]].filter(([k]) => k !== (c.site || {}).id);
return `<div class="card"><p style="margin-top:0">${inputHtml("discover.auto", F("discover.auto"), (c.discover || {}).auto)}</p>
<dl class="kv small"><dt>last look</dt><dd>${last.at ? esc(when(last.at)) : "not yet"}${last.error ? ` · <span style="color:var(--problem)">${esc(last.error)}</span>` : ""}</dd>
<dt>found</dt><dd>${inv.connections != null ? `${inv.connections} Companion connections · ${inv.dante || 0} Dante devices · ${inv.midi_sessions || 0} MIDI sessions · wired network ${esc(inv.wired || "?")}` : "–"}</dd></dl>
@@ -954,8 +954,8 @@
<div class="card"><h3 style="margin-top:0">Computers${hint("Each computer runs the join line once: it lets this Mac log in with its key and says it is here. The next look gives its programs keys.")}</h3>
<div class="tablewrap"><table>${comps}${joined}${comps || joined ? "" : "<tr><td class='muted'>none yet</td></tr>"}</table></div>
${line(d.join.mac, "Mac: Terminal, paste")}${line(d.join.pc, "PC: PowerShell as administrator, paste")}</div>
${d.hub ? `<div class="card"><h3 style="margin-top:0">Set up another campus${hint("On that campus's Mac that runs Companion: Terminal, paste. It installs the panel from here, finds the gear and makes the pages. Then each computer there runs that Mac's join line.")}</h3>
${campuses.map(([k, n]) => line(`curl -fsSL "${d.base}/campus/install?id=${k}" | zsh`, n)).join("")}</div>` : ""}`;
${d.hub ? `<div class="card"><h3 style="margin-top:0">Set up another location${hint("On that location's Mac that runs Companion: Terminal, paste. It installs the panel from here, finds the gear and makes the pages. Then each computer there runs that Mac's join line.")}</h3>
${locations.map(([k, n]) => line(`curl -fsSL "${d.base}/location/install?id=${k}" | zsh`, n)).join("")}</div>` : ""}`;
}
function siteHtml(c) {
const f = F("site.editing_code");
@@ -1286,7 +1286,7 @@
return `<div class="card"><dl class="kv"><dt>running</dt><dd>code ${esc(p.running_version || "?")}</dd><dt>staged</dt><dd>${staged}${same ? " <span class='muted'>· the same</span>" : ""}</dd><dt>folder</dt><dd class="mono small">${esc(p.folder)}</dd></dl>
<div class="row" style="margin-top:8px"><button id="installbtn" ${editing() && p.present ? "" : "disabled"} data-hint="Copy the new folder here with scp first. install.sh runs by itself, the config stays, the watchdog restarts, this page comes back.">Install the staged folder</button>${p.job === "running" ? " <span class='muted'>running…</span>" : ""}</div>
${p.log && p.log.length ? `<details><summary>last install log${p.log_when ? " (" + esc(when(p.log_when)) + ")" : ""}</summary><pre class="log small">${esc(p.log.join("\n"))}</pre></details>` : ""}</div>
<div class="card"><div class="row"><a class="btn" href="/api/site/bundle" download data-hint="A zip of the settings, the saved switcher file and the page-key record: drop it in the repo as sites/${esc((cfg().site || {}).id || "site")}/ or feed it to another campus">Export campus bundle</a>${editing() ? `<label class="btn" data-hint="This campus's name, addresses and PIN stay; every copied computer is flagged until you touch it">New campus from a bundle <input type="file" id="bundlefile" accept=".zip,.json" hidden></label>` : ""}</div></div>`;
<div class="card"><div class="row"><a class="btn" href="/api/site/bundle" download data-hint="A zip of the settings, the saved switcher file and the page-key record: drop it in the repo as sites/${esc((cfg().site || {}).id || "site")}/ or feed it to another location">Export location bundle</a>${editing() ? `<label class="btn" data-hint="This location's name, addresses and PIN stay; every copied computer is flagged until you touch it">New location from a bundle <input type="file" id="bundlefile" accept=".zip,.json" hidden></label>` : ""}</div></div>`;
}
async function outletsHtml() {
const o = await api("/api/outlets");
@@ -1318,7 +1318,7 @@
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 ${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</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 style="margin-top:0">Names the keys use that are not here (${n.missing})${hint("A copied campus, or something renamed. Map each name to what this campus has; everything that uses it follows.")}</h3>
return `<div class="card"><h3 style="margin-top:0">Names the keys use that are not here (${n.missing})${hint("A copied location, or something renamed. Map each name to what this location has; everything that uses it follows.")}</h3>
<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) {
@@ -1511,7 +1511,7 @@
const spname = q("#spname"); if (spname) spname.onchange = () => setPath("spotify.name", spname.value.trim());
const splink = q("#splink"); if (splink) splink.onchange = () => { const m = splink.value.match(/(?:playlist[/:])([A-Za-z0-9]{22})/); if (m) setPath("spotify.playlist", "spotify:playlist:" + m[1]); else if (splink.value.trim()) alertBox("That is not a playlist link."); };
const ib = q("#installbtn"); if (ib) ib.onclick = () => confirmBox("Install the staged folder? The watchdog restarts (the deck goes purple for a few seconds) and this page restarts too.", "Install", async () => { try { const r = await post("/api/program/install", {}); alertBox(r.ok ? r.detail : "Not started: " + r.error); setTimeout(() => location.reload(), 25000); } catch (e) { alertBox(e.message); } });
const bf = q("#bundlefile"); if (bf) bf.onchange = async () => { const f = bf.files[0]; if (!f) return; confirmBox(`Start a new campus from ${f.name}? Your current draft is replaced.`, "Start", async () => { try { const r = await fetch("/api/site/import", { method: "POST", headers: { "X-Edit-Key": state.editing, "X-Editor": state.name, "Content-Type": "application/octet-stream" }, body: await f.arrayBuffer() }).then((x) => x.json()); if (r.error) return alertBox(r.error); state.draft = r.draft; state.atemPending = true; badge(); alertBox(`Copied from ${r.from}. ${r.errors.length ? "Not applicable yet: " + r.errors.slice(0, 3).join("; ") : "Look through Computers, Keys and the Companion names, then Apply."}`); location.hash = "#settings/companion"; } catch (e) { alertBox(e.message); } }); };
const bf = q("#bundlefile"); if (bf) bf.onchange = async () => { const f = bf.files[0]; if (!f) return; confirmBox(`Start a new location from ${f.name}? Your current draft is replaced.`, "Start", async () => { try { const r = await fetch("/api/site/import", { method: "POST", headers: { "X-Edit-Key": state.editing, "X-Editor": state.name, "Content-Type": "application/octet-stream" }, body: await f.arrayBuffer() }).then((x) => x.json()); if (r.error) return alertBox(r.error); state.draft = r.draft; state.atemPending = true; badge(); alertBox(`Copied from ${r.from}. ${r.errors.length ? "Not applicable yet: " + r.errors.slice(0, 3).join("; ") : "Look through Computers, Keys and the Companion names, then Apply."}`); location.hash = "#settings/companion"; } catch (e) { alertBox(e.message); } }); };
const af = q("#atemfile"); if (af) af.onchange = async () => { const f = af.files[0]; if (!f) return; try { const r = await fetch("/api/atem-reference", { method: "POST", headers: { "X-Edit-Key": state.editing, "X-Editor": state.name, "Content-Type": "application/xml" }, body: await f.arrayBuffer() }).then((x) => x.json()); showAtem(r); } catch (e) { alertBox(e.message); } };
const an = q("#atemnewest"); if (an) an.onclick = async () => { an.disabled = true; an.textContent = "Reading…"; try { showAtem(await post("/api/atem-reference/pick-newest")); } catch (e) { alertBox(e.message); route(); } };
const ad = q("#atemdiscard"); if (ad) ad.onclick = async () => { try { await post("/api/atem-reference/discard"); state.atemPending = false; badge(); route(); } catch (e) { alertBox(e.message); } };