/* The settings page, step 1: every screen, read-only. Plain JS, no framework. */
(function () {
"use strict";
const main = document.getElementById("main");
const state = { config: null, schema: null, keys: null, editing: null, name: null };
const esc = (s) => String(s == null ? "" : s).replace(/[&<>"']/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" }[c]));
const api = async (path, opts) => {
const r = await fetch(path, opts);
if (!r.ok) throw new Error((await r.json().catch(() => ({}))).error || r.statusText);
return r.json();
};
const post = (path, body) => api(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body || {}) });
const ago = (s) => s == null ? "" : s < 90 ? Math.round(s) + " s ago" : s < 5400 ? Math.round(s / 60) + " min ago" : Math.round(s / 3600) + " h ago";
const when = (t) => t ? new Date(t * 1000).toLocaleString([], { dateStyle: "medium", timeStyle: "short" }) : "";
const hex = (n) => "#" + (n >>> 0).toString(16).padStart(6, "0").slice(-6);
const lightDot = (l) => ``;
const wordOf = (w) => `${esc(w)}`;
// ---- top bar -------------------------------------------------------------
document.getElementById("legendbtn").onclick = () => { const l = document.getElementById("legend"); l.hidden = !l.hidden; };
const editbtn = document.getElementById("editbtn");
editbtn.onclick = () => {
if (state.editing) { state.editing = null; editbtn.textContent = "Editing: off"; editbtn.classList.remove("on"); return; }
dialog(`
Switch Editing on
The editing code is in the campus's settings file in the repo; ask Chris. In this step nothing can be changed yet: the switch only proves the code works from here.
`, (box) => {
box.querySelector("#dcancel").onclick = closeDialog;
box.querySelector("#dok").onclick = async () => {
const code = box.querySelector("#dcode").value, name = box.querySelector("#dname").value.trim();
const r = await post("/api/editing/check", { code });
if (r.ok && name) { state.editing = code; state.name = name; editbtn.textContent = "Editing as " + name; editbtn.classList.add("on"); closeDialog(); }
else box.querySelector("#dmsg").textContent = r.locked_for ? `Locked for ${Math.ceil(r.locked_for / 60)} minutes after too many tries.` : r.no_code ? "No editing code is set in the config yet." : !name ? "Your first name, please." : `Not the code (${r.tries_left} tries left).`;
};
});
};
function dialog(html, wire) { const d = document.getElementById("dialog"); d.innerHTML = `
`;
for (const b of main.querySelectorAll("[data-f]")) b.onclick = () => { logFilter = b.dataset.f; logs(rest); };
const pre = main.querySelector("pre.log"); pre.scrollTop = pre.scrollHeight;
}
// ---- History -------------------------------------------------------------
async function history() {
const h = await api("/api/history");
main.innerHTML = `
History
Every version of the settings that was ever applied. ${esc(h.note)}
when
file
size
${h.entries.map((e) => `
${esc(e.when)}
${esc(e.file)}
${esc(e.size)}
`).join("") || "
nothing yet
"}
`;
}
// ---- Help ----------------------------------------------------------------
async function help(rest) {
const facts = await api("/api/facts");
const gl = state.schema.glossary;
const cards = [
["Read a red key", "Green is fine. A flashing red key names the problem. Press it once to acknowledge (it stops flashing, stays dark red). Hold it " + esc(state.schema.fields.page_hold_ms ? "2 s" : "2 s") + " and it runs the fix for what is failing."],
["Purple means the watchdog is down", "Every key turns purple when the watchdog on the Ingest Mac has been silent for 15 s. Open this page: if the Overview says Companion is not answering, the problem is Companion; otherwise restart the watchdog (step 2 adds the button; until then: ask Chris)."],
["Which computer or device is that?", "Computers › Map shows every computer and the keys that turn red if it dies; Computers › Gear lists the switcher, decks, recorders, wireless and LED gear with their addresses."],
["What is a connection?", "Companion's link to one device (a HyperDeck, the Tascam, the switcher). The keys watch those connections; Site setup › Companion lists them all and which key watches each."],
].map(([t, b]) => `
First-day walkthrough (10 minutes, no code needed)
Overview: the live deck. Green, red, purple.
Click a red key (or any key): read what it checks and what holding it will do.
Logs › Watchdog, filter "holds": find a hold and what it did.
Keys › Restart page: each key restarts one thing, even when green.
Overview: when alarms are on and when auto-fix may act by itself.
Computers › Map: which computer a key talks to, and what runs there.
Help › What each key does: the whole deck in words.
The words a key can show
${glossary}
Things only a person can do
Run the set-up script on a new computer (ARD on a Mac, PowerShell as administrator on a PC), allow Accessibility on the Studio B Mac, click Allow on the Spotify prompt once.
Create Companion's custom variables and the blink trigger, add pages, install modules, name the Stream Decks.
Type the Netgear login into the keychain; keep Google Drive signed in for the saved switcher files.
Save a new switcher file from ATEM Software Control when the show changes.
Set up once. Each section opens on the few things a person changes; the numbers sit under Advanced. Editing arrives with step 2.
${tabs}
${body}`;
}
// ---- Fixes ---------------------------------------------------------------
async function fixes(rest) {
const cfg = state.config, auto = new Set(cfg.auto_fix.fixes || []);
const used = {};
for (const t of cfg.tiles) for (const c of t.checks || []) { const fx = typeof c.fix === "object" && c.fix ? Object.values(c.fix) : [c.fix]; for (const f of fx) if (f) (used[f] = used[f] || new Set()).add(t.label); if (t.hold_always) (used[t.hold_always] = used[t.hold_always] || new Set()).add(t.label); }
for (const k of (cfg.restart_page || { keys: [] }).keys) (used[k.fix] = used[k.fix] || new Set()).add("restart key " + k.label);
const rows = Object.entries(cfg.fixes).map(([id, f]) => `
${esc(f.short || id)}
${esc(f.label || "")}
${esc(f.kind || "ssh")}${f.host ? " on " + esc((cfg.hosts[f.host] || {}).name || f.host) : f.kind === "local" || f.kind === "spotify" ? " on this Mac" : ""}
${[...(used[id] || [])].join(", ")}
${auto.has(id) ? "may run by itself" : f.never_auto ? "never by itself" : "hold only"}
`).join("");
const f = cfg.fixes[rest[0]];
const detail = f ? `
${esc(f.short || rest[0])}
${esc(f.label || "")}
Advanced
${Object.entries(f).map(([k, v]) => `
${esc(k)}
${esc(typeof v === "object" ? JSON.stringify(v) : v)}
`).join("")}
Builders (fixes made from fields instead of commands) arrive with step 2.
` : "";
main.innerHTML = `
Fixes
Everything a hold can run. ${Object.keys(cfg.fixes).length} today.