/* 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 = `
${html}
`; d.hidden = false; wire(d.querySelector(".box")); } function closeDialog() { document.getElementById("dialog").hidden = true; } // ---- search -------------------------------------------------------------- const search = document.getElementById("search"), results = document.getElementById("searchresults"); search.oninput = () => { const q = search.value.trim().toLowerCase(); if (!q || !state.config) { results.hidden = true; return; } const hits = []; const cfg = state.config; for (const t of cfg.tiles) if ((t.label + " " + t.id).toLowerCase().includes(q)) hits.push([`Key ${t.label}`, `#keys/${t.id}`]); for (const k of (cfg.restart_page || { keys: [] }).keys) if (k.label.toLowerCase().includes(q)) hits.push([`Restart key ${k.label}`, `#keys/restart/${k.id}`]); for (const [a, h] of Object.entries(cfg.hosts || {})) if ((h.name || a).toLowerCase().includes(q) || (h.role || "").toLowerCase().includes(q)) hits.push([`Computer ${h.name || a}`, `#computers/${a}`]); for (const [id, f] of Object.entries(cfg.fixes || {})) if ((id + " " + (f.label || "") + " " + (f.short || "")).toLowerCase().includes(q)) hits.push([`Fix ${f.short || id}: ${f.label || ""}`, `#fixes/${id}`]); for (const [path, f] of Object.entries(state.schema ? state.schema.fields : {})) if (!path.includes("[]") && !path.includes("*") && (path + " " + f.help).toLowerCase().includes(q)) hits.push([`Setting ${path}`, `#site/${f.screen.toLowerCase().replace(/[^a-z]+/g, "-")}`]); results.innerHTML = hits.slice(0, 20).map(([t, h]) => `${esc(t)}`).join("") || `Nothing called "${esc(q)}"`; results.hidden = false; }; search.onblur = () => setTimeout(() => { results.hidden = true; }, 200); // ---- routing ------------------------------------------------------------- const screens = { overview, keys, computers, logs, history, help, site, fixes }; let timer = null; async function route() { const [name, ...rest] = (location.hash || "#overview").slice(1).split("/"); for (const a of document.querySelectorAll(".menu a")) a.classList.toggle("active", a.getAttribute("href") === "#" + name); clearInterval(timer); timer = null; if (!state.config) { const [c, s] = await Promise.all([api("/api/config"), api("/api/schema")]); state.config = c.config; state.schema = s; document.getElementById("sitename").textContent = (state.config.site || {}).name || ""; } const fn = screens[name] || overview; try { await fn(rest); } catch (e) { main.innerHTML = `

Could not load this screen: ${esc(e.message)}

`; } if (name === "overview" || name === "computers") timer = setInterval(() => fn(rest, true).catch(() => {}), 5000); } window.addEventListener("hashchange", route); route(); // ---- Overview ------------------------------------------------------------ async function overview(_, quiet) { const o = await api("/api/overview"); const dot = document.getElementById("hbdot"); dot.className = "dot " + (o.heartbeat_age != null && o.heartbeat_age < 15 ? "ok" : "bad"); const prob = o.problems.length ? o.problems.map((p) => `
  • ${esc(p.text)}${p.acked ? ' (acknowledged)' : ""} — ${esc(p.action)} open the key
  • `).join("") : `
  • Everything is fine.
  • `; const needs = o.needs.length ? o.needs.map((n) => `
    Needs Chris: ${esc(n)}
    `).join("") : ""; const live = o.live && o.live.live ? `
    Live now: ${esc(o.live.reason)}. Changes to the panel wait.
    ` : ""; const comps = o.computers.map((c) => `${wordOf(c.word)}${esc(c.name)}${esc(c.reason)}`).join(""); const w = o.watchdog; main.innerHTML = `

    ${o.problems.length ? o.problems.length + " problem" + (o.problems.length > 1 ? "s" : "") + " in the room" : "Everything is fine"}

    Is the room OK, and what do I do. Problems mirror the red keys; hold the key, or open it for the details.

    ${needs}${live}

    Live deck ${o.deck ? "as the Stream Deck looks now · click a key" : "(Companion is not answering)"}

    ${deckHtml(o.deck, state.config, "status")}

    Computers

    ${comps}
    Watchdog
    ${w.running ? "OK, running" : "not running"}${w.since ? " since " + esc(when(w.since)) : ""}${w.version ? ` code ${esc(w.version)}` : ""}${o.heartbeat_age != null ? ` · heartbeat ${esc(ago(o.heartbeat_age))}` : ""}
    Companion
    ${o.companion_ok ? "OK" : "not answering"}${o.pages.status ? (o.pages.status.ok ? ", pages OK" : ", page " + o.pages.status.page + " is called " + esc(o.pages.status.live || "?")) : ""}
    Alarms
    ${o.service_on ? "ON" : "silenced until midnight"}${o.alarms_override != null ? " (by hand)" : ""} · ${esc(o.phrases.alarms)}
    Auto-fix
    ${esc(o.auto_fix.phrase)}${o.auto_fix.state && o.auto_fix.state.on ? " · may act now" : ""}${o.auto_fix.state && o.auto_fix.state.paused ? " (waiting out a network blip)" : ""}
    Running now
    ${Object.keys(o.running || {}).length ? Object.values(o.running).map((r) => esc(r.what + " on " + r.key)).join(", ") : "no fix is running"}
    Last change
    ${o.last_change ? esc(o.last_change.when + " · " + o.last_change.note) : "—"} History
    `; if (!quiet) window.scrollTo(0, 0); } function deckHtml(deck, cfg, which, selected) { const cells = {}; const rp = cfg.restart_page; if (which === "restart" && rp) { for (const k of rp.keys) cells[k.pos] = { id: k.id, label: k.label }; cells[rp.back_pos] = { nav: "STATUS\nPAGE" }; } else { for (const t of cfg.tiles) if (t.pos) cells[t.pos] = { id: t.id, label: t.label }; if (rp) cells[rp.jump_pos] = { nav: "RESTART\nPAGE" }; } const nav = { "0,0": "UP", "1,0": String(which === "restart" && rp ? rp.page : cfg.page), "2,0": "DOWN" }; let out = `
    `; for (let r = 0; r < 4; r++) for (let c = 0; c < 8; c++) { const k = `${r},${c}`, cell = cells[k]; if (nav[k]) out += ``; else if (cell && cell.nav) out += ``; else if (cell) { const s = deck && deck[cell.id]; const style = s ? `--b1:${hex(s.b)};--f1:${hex(s.f)};--b2:${hex(s.b2)};--f2:${hex(s.f2)};background:${hex(s.b)};color:${hex(s.f)}` : "background:#444;color:#ddd"; const blink = s && s.b !== s.b2 ? " blink" : ""; out += `
    ${esc(s ? s.x : cell.label)}
    `; } else out += `
    `; } return out + `
    `; } // ---- Keys ---------------------------------------------------------------- async function keys(rest) { const k = state.keys || (state.keys = await api("/api/keys")); const o = await api("/api/overview").catch(() => ({ deck: null })); const restart = rest[0] === "restart"; const sel = restart ? rest[1] : rest[0]; const tabs = `
    ${k.restart_page ? `` : ""}
    `; let panel = `

    Click a key to see what it watches and what holding it does.

    `; if (sel && !restart) { const t = k.tiles.find((x) => x.id === sel); if (t) panel = keyPanel(t, k); } else if (sel && restart) { const r = k.restart_page.keys.find((x) => x.id === sel); if (r) panel = `

    ${esc(r.label)}

    row ${r.pos[0]}, column ${r.pos[1]}

    HOLDING ${esc(k.hold.replace("HOLD ", ""))} WILL: ${esc(r.holding)}, even when the Status page says it is fine.

    ${r.watch ? `

    Keeps flashing HOLD TO FIX while down: ${esc(r.watch.join(", "))}

    ` : ""}
    Advanced
    id
    ${esc(r.id)}
    fix
    ${esc(r.fix)}

    Editing arrives with step 2.

    `; } const tray = k.tiles.filter((t) => !t.pos && t.kind !== "master").map((t) => ``).join(" "); main.innerHTML = `

    Keys

    The deck as people hold it. In this step you can look; moving and editing keys arrive with steps 2 and 3.

    ${tabs}
    ${deckHtml(o.deck, state.config, restart ? "restart" : "status", sel)}${!restart && tray ? `

    Shown inside their group, no key of their own: ${tray}

    ` : ""}
    ${panel}
    `; } function keyPanel(t, k) { const cfg = state.config; const checks = (t.checks || []).map((c, i) => `
  • ${esc(c.sentence)}
    Advanced
    ${Object.entries(c).filter(([n]) => !["sentence", "identity"].includes(n)).map(([n, v]) => `
    ${esc(n)}
    ${esc(typeof v === "object" ? JSON.stringify(v) : v)}
    `).join("")}
  • `).join(""); const members = t.members ? `

    Sums up: ${t.members.map((m) => `${esc((cfg.tiles.find((x) => x.id === m) || {}).label || m)}`).join(", ")}

    ` : ""; return `

    ${esc(t.label)}

    ${t.pos ? `row ${t.pos[0]}, column ${t.pos[1]}` : "inside its group"}${t.kind ? ` · ${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}` : ""}

    ${t.says && t.says.length ? `

    WHAT THIS KEY WILL SAY: ${esc(t.says.join(", "))}

    ` : ""}

    HOLDING ${esc(k.hold.replace("HOLD ", ""))} WILL: ${esc(t.holding)}${t.hold_always ? " also when green" : ""}

    ${members}${checks ? `

    Checks:

      ${checks}
    ` : ""}
    Advanced
    id
    ${esc(t.id)}

    Editing arrives with step 2.

    `; } // ---- Computers ----------------------------------------------------------- let compTab = "map"; async function computers(rest, quiet) { const m = await api("/api/machines"); const selected = rest[0] && rest[0] !== "gear" && rest[0] !== "setup" ? rest[0] : null; if (rest[0] === "gear" || rest[0] === "setup") compTab = rest[0]; else if (!quiet || !compTab) compTab = selected ? "map" : compTab || "map"; const tabs = `
    `; let body = ""; if (compTab === "gear") body = await gearHtml(); else if (compTab === "setup") body = setupHtml(m); else { const rows = m.computers.slice().sort((a, b) => (a.word === "Problem" ? 0 : 1) - (b.word === "Problem" ? 0 : 1)).map((c) => `${wordOf(c.word)}${esc(c.name)}${c.self ? ' (this Mac)' : ""}${esc(c.role)}${c.keys.map((k) => `${esc((state.config.tiles.find((t) => t.id === k) || { label: k }).label)}`).join(" ")}${esc(c.reason)}${c.looked ? `
    looked ${esc(ago(Date.now() / 1000 - c.looked))}` : ""}`).join(""); body = `
    ${rows}
    statuscomputerwhat it does for the panelkeys that turn red if it dies
    `; const c = m.computers.find((x) => x.alias === selected); if (c) body += computerCard(c); } main.innerHTML = `

    Computers

    What runs where. One row per computer; click it for everything that runs there, with a light each.

    ${tabs}${body}`; const pb = document.getElementById("probe"); if (pb) pb.onclick = async () => { pb.disabled = true; pb.textContent = "Looking…"; try { await post("/api/machines/probe"); } catch (e) { alertBox(e.message); } setTimeout(() => computers(rest, true), 4000); }; } function computerCard(c) { const items = c.items.map((i) => `${lightDot(i.light)}${esc(i.light === "ok" ? "OK" : i.light === "problem" ? "Problem" : i.light === "not_needed" ? "Not needed" : "Can't see from here")}${esc(i.name)}${i.watched === false ? ' (not watched)' : ""}${esc(i.reason || i.detail || (i.detail && JSON.stringify(i.detail)) || "")}`).join(""); const found = c.found || {}; const foundHtml = Object.entries(found).filter(([, v]) => Array.isArray(v) && v.length).map(([k, v]) => `

    ${esc(k)}: ${esc(typeof v[0] === "object" ? v.map((x) => x.iface + " " + x.ip + (x.active ? "" : " (down)")).join(", ") : v.join(", "))}

    `).join("") || `

    Press "Check computers now" to look.

    `; const done = Object.entries(c.manual_done || {}).map(([k, v]) => `
  • ${lightDot("hand")}${esc(k.replace(/_/g, " "))}: done by hand (${esc(v.by)}, ${esc(v.on)})
  • `).join(""); return `

    ${esc(c.name)} ${wordOf(c.word)}

    ${esc(c.role)}${c.ssh ? ` · logs in as ${esc(c.ssh)}` : ""}${c.mac ? ` · wake-on-LAN ${esc(c.mac)}` : ""}${c.pingable === false ? " · does not answer pings" : c.ping != null ? ` · ping ${c.ping ? "OK" : "no answer"}` : ""}${c.error ? ` · ${esc(c.error)}` : ""}

    ${c.notes ? `

    ${esc(c.notes)}

    ` : ""}

    What runs here

    ${items}
    ${done ? `` : ""}
    Everything found on this computer${foundHtml}
    ${c.self ? `
    This Mac's set-up lights
    ${Object.entries(c.setup || {}).map(([k, v]) => `
    ${esc(k.replace(/_/g, " "))}
    ${typeof v === "boolean" ? lightDot(v ? "ok" : "problem") + (v ? "OK" : "Problem") : esc(JSON.stringify(v))}
    `).join("")}
    ` : ""}
    `; } async function gearHtml() { const g = await api("/api/gear"); return `

    Devices that are not computers, from the keys that watch them. Where each one lives is typed once on this list (step 2).

    ${g.map((r) => ``).join("")}
    lightdevicekindaddresswhere / noteswatched by
    ${lightDot(r.light)}${esc(r.light === "ok" ? "OK" : r.light === "problem" ? "Problem" : "–")}${esc(r.name)}${esc(r.kind)}${esc(r.address || "")}${r.detail ? `
    ${esc(r.detail)}` : ""}
    ${esc(r.location || "")} ${esc(r.notes || "")} ${esc(r.reason || "")}${r.watched_by.map((k) => `${esc((state.config.tiles.find((t) => t.id === k) || { label: k }).label)}`).join(", ")}
    `; } function setupHtml(m) { return m.computers.map((c) => { const kind = c.self ? "self" : c.os === "windows" ? "windows" : "mac"; const steps = m.setup_steps[kind]; const lightFor = (step) => { const s = c.setup || {}; if (/login key/.test(step)) return s.key_ok === true || s.login_key === true ? "ok" : s.key_ok === false || s.login_key === false ? "problem" : "unknown"; if (/python/.test(step)) return s.python_ok === true ? "ok" : s.python_ok === false ? "problem" : "unknown"; if (/SSH service/.test(step)) return c.reachable ? "ok" : "problem"; if (/firewall/.test(step)) return s.firewall_allow ? "ok" : "unknown"; if (/wake-on-LAN/.test(step)) return s.wake_on_lan === "Enabled" ? "ok" : s.wake_on_lan ? "problem" : "unknown"; if (/Public/.test(step)) return s.profile === "Public" ? "ok" : s.profile ? "problem" : "unknown"; if (/DVS helper/.test(step)) { const it = (c.items || []).find((i) => i.kind === "helper_dvs"); return it ? it.light : "not_needed"; } if (/Pro Tools helper/.test(step)) { const it = (c.items || []).find((i) => i.kind === "helper_solo"); return it ? it.light : "not_needed"; } if (/StatusPanel/.test(step)) { const ts = (c.items || []).filter((i) => i.kind === "task"); return ts.length ? (ts.every((i) => i.light === "ok") ? "ok" : "problem") : "unknown"; } if (/rtpMIDI/.test(step)) { const it = (c.items || []).find((i) => i.kind === "service"); return it ? it.light : "not_needed"; } if (/custom variables/.test(step)) return "unknown"; if (/Netgear/.test(step)) return s.netgear_keychain === true ? "ok" : s.netgear_keychain === false ? "problem" : "not_needed"; if (/Google Drive/.test(step)) { const it = (c.items || []).find((i) => i.kind === "folder"); return it ? it.light : "not_needed"; } if (/watchdog and the settings/.test(step)) { const ok = (c.items || []).filter((i) => i.kind === "launchagent" && /watchdog|settings/.test(i.name)); return ok.length && ok.every((i) => i.light === "ok") ? "ok" : "unknown"; } if (/Wi-Fi/.test(step)) return c.manual_done && c.manual_done.wifi_power ? "hand" : "unknown"; if (/BIOS/.test(step)) return c.manual_done && c.manual_done.bios_wol ? "hand" : "unknown"; return "unknown"; }; return `

    ${esc(c.name)}

    Generated set-up scripts and the [how] steps arrive with step 5.

    `; }).join(""); } // ---- Logs ---------------------------------------------------------------- let logTab = "watchdog", logFilter = ""; async function logs(rest) { logTab = rest[0] || logTab; const tabs = ["watchdog", "errors", "autofix", "settings"].map((n) => ``).join(""); const filters = logTab === "watchdog" ? `
    ${["", "faults", "holds", "fixes", "auto", "errors"].map((f) => ``).join("")}
    ` : ""; let data; if (logTab === "autofix") { data = await api("/api/logs/autofix" + (rest[1] ? "?file=" + encodeURIComponent(rest[1]) : "")); } else data = await api(`/api/logs/${logTab}?filter=${encodeURIComponent(logFilter)}`); const files = logTab === "autofix" ? `

    ${(data.files || []).slice().reverse().map((f) => `${esc(f.replace(" Auto-Fix.log", ""))}`).join(" · ") || "no auto-fix days yet"}

    ` : ""; main.innerHTML = `

    Logs

    What the watchdog did and saw. Newest lines at the bottom.

    ${tabs}
    ${filters}${files}
    ${esc((data.lines || []).join("\n")) || "(empty)"}
    `; 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)}

    ${h.entries.map((e) => ``).join("") || ""}
    whenfilesize
    ${esc(e.when)}${esc(e.file)}${esc(e.size)}
    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]) => `

    ${t}

    ${b}

    `).join(""); const glossary = Object.entries(gl).filter(([g]) => !/while you hold/.test(g)).map(([g, words]) => `
    ${esc(g)}

    ${esc(words.join(" · "))}

    `).join(""); main.innerHTML = `

    Help

    Start with the four cards. The pages below are made from the settings, so they are never out of date.

    ${cards}

    Pages

    What each key does · Cheat sheet (one page) · Where things are

    First-day walkthrough (10 minutes, no code needed)

    1. Overview: the live deck. Green, red, purple.
    2. Click a red key (or any key): read what it checks and what holding it will do.
    3. Logs › Watchdog, filter "holds": find a hold and what it did.
    4. Keys › Restart page: each key restarts one thing, even when green.
    5. Overview: when alarms are on and when auto-fix may act by itself.
    6. Computers › Map: which computer a key talks to, and what runs there.
    7. Help › What each key does: the whole deck in words.

    The words a key can show

    ${glossary}

    Things only a person can do

    Companion facts (found by trying)

    ${facts.facts.map(([q, a]) => `
    ${esc(q)}
    ${esc(a)}
    `).join("")}
    `; } // ---- Site setup (read-only) ---------------------------------------------- async function site(rest) { const cfg = state.config, F = state.schema.fields; const sections = { site: "Site", schedules: "Schedules", companion: "Companion", "network-dante": "Network & Dante", spotify: "Spotify", atem: "ATEM" }; const which = sections[rest[0]] ? rest[0] : "site"; const tabs = Object.entries(sections).map(([k, n]) => ``).join(""); const screen = sections[which]; const get = (path) => path.split(".").reduce((o, p) => (o && o[p] !== undefined ? o[p] : undefined), cfg); const fieldHtml = (path, f) => { const v = get(path); return `
    ${v === undefined ? `(default: ${esc(JSON.stringify(f.default))})` : esc(typeof v === "object" ? JSON.stringify(v) : String(v))}
    `; }; const own = Object.entries(F).filter(([p, f]) => f.screen === screen && !p.includes("[]") && !p.includes("*") && !["tiles", "fixes", "hosts", "gear"].includes(p)); let body = `

    Settings

    ${own.filter(([, f]) => !f.advanced).map(([p, f]) => fieldHtml(p, f)).join("")}
    Advanced${own.filter(([, f]) => f.advanced).map(([p, f]) => fieldHtml(p, f)).join("")}
    `; if (which === "companion") { const c = await api("/api/companion"); const lights = Object.entries(c.lights).map(([k, v]) => `
  • ${lightDot(v === true ? "ok" : v === false ? "problem" : "unknown")}${esc(k)}
  • `).join(""); const conns = c.connections.map((r) => `${lightDot(r.category === "good" ? "ok" : r.enabled ? "problem" : "not_needed")}${esc(r.label)}${esc(r.module || "")}${esc(r.enabled ? r.category || "" : "off")}${r.message ? ` ${esc(r.message)}` : ""}${r.watched_by.length ? r.watched_by.map((k) => `${esc(k.toUpperCase())}`).join(", ") : 'not watched'}`).join(""); body = `

    Companion ${c.ok ? "OK" : "not answering"}${c.build ? ` ${esc(c.build)}` : ""}

    Set up once by the person who installed Companion. You normally only come here to see which connection a key watches.

    ${c.missing.length ? `

    Keys watch connections Companion does not have: ${esc(c.missing.join(", "))}

    ` : ""}

    Page-number buttons coloured on ${c.page_number_buttons.pages_coloured} pages (${esc(c.page_number_buttons.recorded_in)}).

    Connections (live)

    ${conns}
    connectionmodulestatuswatched by
    ` + body; } if (which === "network-dante") { const d = (cfg.tiles.flatMap((t) => t.checks || []).find((c) => c.type === "dante") || {}).devices || {}; const snap = await api("/api/snapshot"); const map = snap.dante_map || {}; body += `

    Dante device list (the DANTE key's devices)

    ${Object.entries(d).map(([n, a]) => { const e = map[n] || {}; return ``; }).join("")}
    Dante namekey textaddressswitch / portPoE
    ${esc(n)}${esc(a)}${esc(e.ip || "")}${e.switch ? esc(e.switch + " port " + e.port) : ""}${e.poe ? "yes" : ""}

    Map from the last survey ${snap.dante_map_age != null ? esc(ago(snap.dante_map_age)) : "(none yet)"}.

    `; } if (which === "schedules") { const ph = await api("/api/overview").then((o) => o.phrases).catch(() => ({})); body = `
    ${esc(ph.alarms || "")} Auto-fix ${esc(ph.auto_fix || "")}. ${esc(((ph.live || "")[0] || "").toUpperCase() + (ph.live || "").slice(1))}.

    Fixes auto-fix may run

    ${(cfg.auto_fix.fixes || []).map((f) => `${esc((cfg.fixes[f] || {}).short || f)}`).join(" ")}

    ` + body; } main.innerHTML = `

    Site setup

    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.

    ${rows}
    key textplain wordsbuilt asused byby itself
    ${detail}`; } function alertBox(msg) { dialog(`

    ${esc(msg)}

    `, (b) => { b.querySelector("#dok").onclick = closeDialog; }); } })();