dialog(`<h3>Switch Editing on</h3><p class="help">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.</p>
<input id="dcode" placeholder="editing code" autocomplete="off"><input id="dname" placeholder="your first name" autocomplete="off">
if(r.ok&&name){state.editing=code;state.name=name;editbtn.textContent="Editing as "+name;editbtn.classList.add("on");closeDialog();}
elsebox.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).`;
<h1>${o.problems.length?o.problems.length+" problem"+(o.problems.length>1?"s":"")+" in the room":"Everything is fine"}</h1>
<p class="what">Is the room OK, and what do I do. Problems mirror the red keys; hold the key, or open it for the details.</p>
<ul class="problems">${prob}</ul>${needs}${live}
<div class="cols">
<div><h2>Live deck <span class="muted small">${o.deck?"as the Stream Deck looks now · click a key":"(Companion is not answering)"}</span></h2>${deckHtml(o.deck,state.config,"status")}
<div class="row" style="margin-top:10px"><button disabled title="step 4">Change the playlist</button><button disabled title="step 4">Add or remove a HyperDeck</button><button disabled title="step 4">Add a MIDI link</button></div></div>
<dt>Companion</dt><dd>${o.companion_ok?"OK":"<b>not answering</b>"}${o.pages.status?(o.pages.status.ok?", pages OK":", page "+o.pages.status.page+" is called "+esc(o.pages.status.live||"?")):""}</dd>
<dt>Auto-fix</dt><dd>${esc(o.auto_fix.phrase)}${o.auto_fix.state&&o.auto_fix.state.on?" · <b>may act now</b>":""}${o.auto_fix.state&&o.auto_fix.state.paused?" (waiting out a network blip)":""}</dd>
<dt>Running now</dt><dd>${Object.keys(o.running||{}).length ? Object.values(o.running).map((r) => esc(r.what + " on " + r.key)).join(", ") : "no fix is running"}</dd>
letpanel=`<p class="muted">Click a key to see what it watches and what holding it does.</p>`;
if(sel&&!restart){
constt=k.tiles.find((x)=>x.id===sel);
if(t)panel=keyPanel(t,k);
}elseif(sel&&restart){
constr=k.restart_page.keys.find((x)=>x.id===sel);
if(r)panel=`<div class="card"><h2>${esc(r.label)}</h2><p class="muted">row ${r.pos[0]}, column ${r.pos[1]}</p><p><b>HOLDING ${esc(k.hold.replace("HOLD ",""))} WILL:</b> ${esc(r.holding)}, even when the Status page says it is fine.</p>${r.watch?`<p class="small muted">Keeps flashing HOLD TO FIX while down: ${esc(r.watch.join(", "))}</p>`:""}<details><summary>Advanced</summary><dl class="kv"><dt>id</dt><dd class="mono">${esc(r.id)}</dd><dt>fix</dt><dd class="mono">${esc(r.fix)}</dd></dl></details><p class="help">Editing arrives with step 2.</p></div>`;
main.innerHTML=`<h1>Keys</h1><p class="what">The deck as people hold it. In this step you can look; moving and editing keys arrive with steps 2 and 3.</p>${tabs}
<div class="cols"><div>${deckHtml(o.deck,state.config,restart?"restart":"status",sel)}${!restart&&tray?`<p class="small muted" style="margin-top:8px">Shown inside their group, no key of their own: ${tray}</p>`:""}</div><div>${panel}</div></div>`;
body=`<div class="tablewrap"><table><tr><th>status</th><th>computer</th><th>what it does for the panel</th><th>keys that turn red if it dies</th><th></th></tr>${rows}</table></div>`;
constc=m.computers.find((x)=>x.alias===selected);
if(c)body+=computerCard(c);
}
main.innerHTML=`<h1>Computers</h1><p class="what">What runs where. One row per computer; click it for everything that runs there, with a light each.</p>${tabs}${body}`;
constdone=Object.entries(c.manual_done||{}).map(([k,v])=>`<li>${lightDot("hand")}${esc(k.replace(/_/g," "))}: done by hand (${esc(v.by)}, ${esc(v.on)})</li>`).join("");
return`<div class="card" id="card"><h2>${esc(c.name)}${wordOf(c.word)}</h2><p class="muted">${esc(c.role)}${c.ssh?` · logs in as <span class="mono">${esc(c.ssh)}</span>`:""}${c.mac?` · wake-on-LAN ${esc(c.mac)}`:""}${c.pingable===false?" · does not answer pings":c.ping!=null?` · ping ${c.ping?"OK":"<b>no answer</b>"}`:""}${c.error?` · <b>${esc(c.error)}</b>`:""}</p>
return`<p class="small muted">Devices that are not computers, from the keys that watch them. Where each one lives is typed once on this list (step 2).</p><div class="tablewrap"><table><tr><th>light</th><th>device</th><th>kind</th><th>address</th><th>where / notes</th><th>watched by</th></tr>
if(/watchdog and the settings/.test(step)){constok=(c.items||[]).filter((i)=>i.kind==="launchagent"&&/watchdog|settings/.test(i.name));returnok.length&&ok.every((i)=>i.light==="ok")?"ok":"unknown";}
return`<div class="card"><h3>${esc(c.name)}</h3><ul style="list-style:none;padding:0;margin:0">${steps.map((s)=>{constl=lightFor(s);return`<li>${lightDot(l)}${esc(s)} <span class="muted small">${l==="hand"?"(done by hand)":l==="unknown"?"– can't see from here":""}</span></li>`;}).join("")}</ul><p class="help">Generated set-up scripts and the [how] steps arrive with step 5.</p></div>`;
constfiles=logTab==="autofix"?`<p class="small">${(data.files||[]).slice().reverse().map((f)=>`<a href="#logs/autofix/${encodeURIComponent(f)}" class="${data.file===f?"pill":""}">${esc(f.replace(" Auto-Fix.log",""))}</a>`).join(" · ")||"no auto-fix days yet"}</p>`:"";
main.innerHTML=`<h1>Logs</h1><p class="what">What the watchdog did and saw. Newest lines at the bottom.</p><div class="tabs">${tabs}</div>${filters}${files}<pre class="log">${esc((data.lines||[]).join("\n"))||"(empty)"}</pre>`;
// ---- History -------------------------------------------------------------
asyncfunctionhistory(){
consth=awaitapi("/api/history");
main.innerHTML=`<h1>History</h1><p class="what">Every version of the settings that was ever applied. ${esc(h.note)}</p><div class="tablewrap"><table><tr><th>when</th><th>file</th><th>size</th></tr>${h.entries.map((e)=>`<tr><td>${esc(e.when)}</td><td class="mono small">${esc(e.file)}</td><td>${esc(e.size)}</td></tr>`).join("")||"<tr><td colspan=3 class='muted'>nothing yet</td></tr>"}</table></div>`;
}
// ---- Help ----------------------------------------------------------------
asyncfunctionhelp(rest){
constfacts=awaitapi("/api/facts");
constgl=state.schema.glossary;
constcards=[
["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."],
constglossary=Object.entries(gl).filter(([g])=>!/while you hold/.test(g)).map(([g,words])=>`<details open><summary><b>${esc(g)}</b></summary><p class="mono small">${esc(words.join(" · "))}</p></details>`).join("");
main.innerHTML=`<h1>Help</h1><p class="what">Start with the four cards. The pages below are made from the settings, so they are never out of date.</p><div class="cards">${cards}</div>
<h2>Pages</h2><p><a href="/key-guide.html" target="_blank">What each key does</a> · <a href="/cheat.html" target="_blank">Cheat sheet (one page)</a> · <a href="/where.html" target="_blank">Where things are</a></p>
<h2>First-day walkthrough (10 minutes, no code needed)</h2><ol><li>Overview: the live deck. Green, red, purple.</li><li>Click a red key (or any key): read what it checks and what holding it will do.</li><li>Logs › Watchdog, filter "holds": find a hold and what it did.</li><li>Keys › Restart page: each key restarts one thing, even when green.</li><li>Overview: when alarms are on and when auto-fix may act by itself.</li><li>Computers › Map: which computer a key talks to, and what runs there.</li><li>Help › What each key does: the whole deck in words.</li></ol>
<h2>The words a key can show</h2>${glossary}
<h2>Things only a person can do</h2><ul><li>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.</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 for the saved switcher files.</li><li>Save a new switcher file from ATEM Software Control when the show changes.</li></ul>
<h2>Companion facts (found by trying)</h2><dl class="kv">${facts.facts.map(([q,a])=>`<dt>${esc(q)}</dt><dd>${esc(a)}</dd>`).join("")}</dl>`;
}
// ---- Site setup (read-only) ----------------------------------------------
body=`<div class="card"><h3>Companion ${c.ok?"OK":"<b>not answering</b>"}${c.build?` <span class="pill">${esc(c.build)}</span>`:""}</h3><p class="small muted">Set up once by the person who installed Companion. You normally only come here to see which connection a key watches.</p><ul style="list-style:none;padding:0">${lights}</ul>
${c.missing.length?`<p class="notice">Keys watch connections Companion does not have: ${esc(c.missing.join(", "))}</p>`:""}
<p class="small">Page-number buttons coloured on ${c.page_number_buttons.pages_coloured} pages (${esc(c.page_number_buttons.recorded_in)}).</p>
main.innerHTML=`<h1>Site setup</h1><p class="what">Set up once. Each section opens on the few things a person changes; the numbers sit under Advanced. Editing arrives with step 2.</p><div class="tabs">${tabs}</div>${body}`;
constrows=Object.entries(cfg.fixes).map(([id,f])=>`<tr class="click${rest[0]===id?" sel":""}" onclick="location.hash='#fixes/${esc(id)}'"><td><b>${esc(f.short||id)}</b></td><td>${esc(f.label||"")}</td><td class="small">${esc(f.kind||"ssh")}${f.host?" on "+esc((cfg.hosts[f.host]||{}).name || f.host) : f.kind === "local" || f.kind === "spotify" ? " on this Mac" : ""}</td><td class="small">${[...(used[id]||[])].join(", ")}</td><td class="small">${auto.has(id)?"may run by itself":f.never_auto?"never by itself":"hold only"}</td></tr>`).join("");
constf=cfg.fixes[rest[0]];
constdetail=f?`<div class="card"><h2>${esc(f.short||rest[0])}</h2><p>${esc(f.label||"")}</p><details open><summary>Advanced</summary><dl class="kv">${Object.entries(f).map(([k,v])=>`<dt>${esc(k)}</dt><dd class="mono small">${esc(typeofv==="object"?JSON.stringify(v):v)}</dd>`).join("")}</dl></details><p class="help">Builders (fixes made from fields instead of commands) arrive with step 2.</p></div>`:"";
main.innerHTML=`<h1>Fixes</h1><p class="what">Everything a hold can run. ${Object.keys(cfg.fixes).length} today.</p><div class="tablewrap"><table><tr><th>key text</th><th>plain words</th><th>built as</th><th>used by</th><th>by itself</th></tr>${rows}</table></div>${detail}`;