elsebox.querySelector("#dmsg").textContent=r.locked_for?`Locked for ${Math.ceil(r.locked_for/60)} minutes after too many tries.`:r.no_code?"No PIN is set yet (Settings › Site).":!name?"Your first name, please.":`Not the PIN (${r.tries_left} tries left).`;
alertBox("The settings changed on the Mac since this page loaded (an Apply elsewhere, or a program update). Your changes were put on top of the new settings; nothing of either was lost.");
if(e.status===409){alertBox(`${e.body.error}: ${(e.body.changed||[]).join(", ")||"the draft"}. The page now shows their version.`);state.draft=e.body.draft;route();}
constLABELS={after_seconds:"wait before acting (seconds)",note_seconds:"keep showing ON … after (seconds)",interval_seconds:"look every (seconds)",failures:"misses before it counts",debounce_seconds:"a fault must last (seconds)",editing_code:"PIN",ssh:"log-in address",mac:"hardware address",ingest_ips:"this Mac's addresses",ingest_name:"this Mac's name",ingest_hostname:"network name",hold_always:"hold also when green",page_hold_ms:"hold time (ms)",optional:"may have no disk"};
// An On/Off switch like Companion's connection toggle: switched off = kept in the settings, not used.
// `path` is the object ("tiles.3", "fixes.obs", "tiles.3.checks.1"); off writes "disabled": true there.
functiononOff(path,obj,what){
conston=!(obj&&obj.disabled);
return`<label class="onoff${on?" on":""}" data-hint="${esc(on?`On. Switch off to keep ${what||"it"} here without using it.`:`Off: kept, not used. Switch on to use ${what||"it"} again.`)}"><input type="checkbox" data-onoff="${esc(path)}" ${on?"checked":""}${editing()?"":"disabled"}><span>${on?"ON":"OFF"}</span></label>`;
constfailed=o.failed_last?`<div class="notice grey">The last Apply was put back: ${esc(o.failed_last.why||"")} <a href="#history">History</a></div>`:"";
<h1>${o.problems.length?o.problems.length+" problem"+(o.problems.length>1?"s":""):"Everything is fine"}${hint("The red keys, in words. Hold the key on the deck, or open it.")}</h1>
${chip(o.companion_ok,o.companion_ok?"Companion ✓":"Companion down",o.pages&&o.pages.status?(o.pages.status.ok?"Pages OK":"Page "+o.pages.status.page+" is called "+(o.pages.status.live||"?")):"")}
${chip(null,"Auto-fix "+(o.auto_fix.state&&o.auto_fix.state.on?"active":"waiting"),o.auto_fix.phrase+(o.auto_fix.state&&o.auto_fix.state.paused?" (waiting out a network blip)":""))}
<button id="restartwd" ${dis} data-hint="${w.running?"Restart the watchdog: the deck goes purple for about 5 s; a running fix finishes first":"Start the watchdog"}">${w.running?"Restart watchdog":"Start watchdog"}</button>
<button id="alarms" ${dis} data-hint="${o.service_on===false?"Alarms back on now":"Same as the ALARMS key: quiet until midnight"}">${o.service_on===false?"Alarms on":"Silence alarms"}</button>
${w.running?`<button id="stopwd" ${dis} data-hint="Stop the watchdog: every key purple, no alarms until it is started again">Stop</button>`:""}
main.querySelector("#restartwd").onclick=()=>confirmBox(w.running?"Restart the watchdog? The deck goes purple for about 5 seconds; a fix that is running finishes first.":"Start the watchdog?",w.running?"Restart":"Start",async()=>{try{constr=awaitpost(w.running?"/api/watchdog/restart":"/api/watchdog/start");alertBox(r.ok?"Done: "+r.detail:"Not done: "+r.detail);}catch(e){alertBox(e.message);}});
conststop=main.querySelector("#stopwd");if(stop)stop.onclick=()=>confirmBox("Stop the watchdog? The deck goes purple and every alarm is off until it is started again.","Stop",async()=>{try{constr=awaitpost("/api/watchdog/stop",{word:"STOP"});alertBox(r.ok?"Stopped.":"Not stopped: "+r.detail);}catch(e){alertBox(e.message);}},"STOP");
${editing()?`<span class="small muted" style="margin-left:auto">page <input class="pagenum" data-which="status" type="number" min="1" value="${c.page}" data-hint="The Status page's number in Companion">${c.restart_page?` · restart page <input class="pagenum" data-which="restart" type="number" min="1" value="${c.restart_page.page}">`:""} <label class="inline"><input type="checkbox" id="hasrestart" ${c.restart_page?"checked":""}> Restart page${hint("A second page where each key restarts one thing, even when green")}</label></span>`:""}</div>`;
letpanel=`<p class="muted small">Click a key.${editing()?" Drag to move; drop on the tray to hide.":""}</p>`;
if(state.draft){try{constv=awaitpost("/api/validate",{});if(v.plan&&v.plan.pages.length)note=`<div class="notice">Layout changed: Apply re-imports the ${v.plan.pages.filter((p)=>p==="status"||p==="restart"||p==="ready").map((p)=>p[0].toUpperCase()+p.slice(1)).join(" and ")} page.</div>`;}catch(e){}}
${!restart&&!ready?`<p class="small muted tray" id="tray" style="margin-top:8px">In a group only${hint("Keys with no spot of their own: they show inside their group key. Drop a key here to hide it.")}: ${tray||"<span class='muted'>none</span>"}</p>`:""}
// A new READY page key: a checklist line the page checks itself, a tap-to-confirm line, or any check.
functionnewReadyKey(){
constc=cfg();
dialog(`<h3>New READY key</h3><input id="nlabel" placeholder="label, as the key shows it (GREEN ROOM)">
<select id="nkind"><option value="manual">Tap to confirm: a person taps it done (resets every week)</option><option value="ready">A checklist line the page checks by itself</option><option value="any">Something else: I pick its checks next (a Companion connection, a ping, a program...)</option></select>
if(!m){if(allowBlank&&!text.trim())returnnull;alertBox("Row, column: two numbers, like 2, 5.");returnfalse;}
constpos=[+m[1],+m[2]];
if(pos[0]>3||pos[1]>7){alertBox("The deck has rows 0-3 and columns 0-7.");returnfalse;}
if(takenCells(c,which).has(String(pos))){alertBox(`Row ${pos[0]} column ${pos[1]} is taken. Hide or move a key first`+(allowBlank?", or leave the position blank.":"."));returnfalse;}
tray.addEventListener("drop",(e)=>{e.preventDefault();constwhat=e.dataTransfer.getData("text/plain")||dragging;if(!what||!what.startsWith("tile:"))return;constt=cfg().tiles.find((x)=>x.id===what.slice(5));if(t&&t.kind)returnalertBox("That key always has a spot on the deck.");move(what,null);});
if(!hr.checked)confirmBox("Switch the Restart page off? Its keys are removed from the settings; page "+c.restart_page.page+" stays in Companion until someone clears it.","Switch it off",()=>{deletec.restart_page;saveDraft();location.hash="#keys";route();},null);
else{constpos=firstEmpty(c,"status");if(!pos)returnalertBox("No free spot on the Status page for the RESTART PAGE key.");c.restart_page={page:c.page+1,page_name:`${c.page+1}\nRestart`,jump_pos:pos,back_pos:pos,keys:[]};saveDraft();location.hash="#keys/restart";route();}
constnk=main.querySelector("#newkey");if(nk)nk.onclick=()=>dialog(`<h3>New key</h3><input id="nlabel" placeholder="label, as the key shows it (HD7)"><input id="nid" placeholder="id – filled from the label"><input id="npos" placeholder="row, column (blank = in a group only)" value="${(firstEmpty(cfg(),"status")||[]).join(", ")}"><div class="row"><button class="primary" id="dok">Add</button><button id="dcancel">Cancel</button></div>`,(b)=>{
constfixOptions=(cur)=>`<option value="">none</option><option value="restart-connection" ${cur==="restart-connection"?"selected":""}>reconnect its Companion connection</option><option value="dante-auto" ${cur==="dante-auto"?"selected":""}>Dante auto (PoE port or the Dante module)</option>${Object.entries(c.fixes).map(([id,f])=>`<option value="${esc(id)}" ${cur===id?"selected":""}>${esc(f.short||id)}–${esc(f.label||"")}</option>`).join("")}`;
?`<div class="field"><span class="fl">checklist line${hint("What the READY page looks at for this key")}</span><select data-path="${base}item"><option value="">–</option>${Object.entries(READY_WORDS).map(([k2,w])=>`<option value="${k2}" ${chk.item===k2?"selected":""}>${esc(k2)}–${esc(w.slice(0,70))}</option>`).join("")}</select></div>`
:f==="item"&&chk.type==="ready_manual"
?inputHtml(base+f,Object.assign({},F("tiles[].checks[].item"),{help:"A short name for what is tapped done, like green_room (its tap is remembered by this name for the week)"}),chk[f])
constfixSel=typeofchk.fix==="object"&&chk.fix?`<div class="field"><span class="fl">fix per device${hint('A table: device → fix, "*" for everything else')}</span><textarea data-path="${base}fix" rows="2" class="fixtable mono">${esc(JSON.stringify(chk.fix))}</textarea></div>`
:`<div class="field"><span class="fl">fix${hint("What holding the key runs for this check")}</span><select data-path="${base}fix" class="fixsel">${fixOptions(chk.fix)}</select></div>`;
construn=typeofchk.fix==="string"&&chk.fix&&canRunNow(chk.fix)?`<button class="small" data-run="${esc(t.id)}|${esc(chk.fix)}|${j}" data-hint="Through the watchdog, exactly like a hold">Run the fix</button>`:"";
constholdAlways=editing()&&plain&&!onReady?`<div class="field"><span class="fl">hold also when green${hint("A fix that runs on every hold, even with nothing failing")}</span><select data-path="tiles.${i}.hold_always" class="holdsel"><option value="">no</option>${Object.entries(c.fixes).map(([id,f])=>`<option value="${esc(id)}" ${t.hold_always===id?"selected":""}>${esc(f.short||id)}–${esc(f.label||"")}</option>`).join("")}</select></div>`:"";
${kt.says&&kt.says.length?`<details class="fold"><summary>Words it can show <span class="muted">${kt.says.length}</span></summary><p class="mono small">${esc(kt.says.join(" · "))}</p></details>`:""}
constdk=main.querySelector("#delkey");if(dk)dk.onclick=()=>confirmBox(`Delete the key ${keyList(c,list)[i].label}? Its checks go with it.`,"Delete",()=>{if(!ensureDraft())return;constcc=state.draft.config;constkeys=keyList(cc,list);constid=keys[i].id;keys.splice(i,1);for(consttofcc.tiles)if(t.members)t.members=t.members.filter((m)=>m!==id);saveDraft();location.hash=list==="tiles"?"#keys":"#keys/ready";});
constdr=main.querySelector("#delrkey");if(dr)dr.onclick=()=>confirmBox("Delete this restart key?","Delete",()=>{if(!ensureDraft())return;constks=state.draft.config.restart_page.keys;ks.splice(ks.findIndex((x)=>x.id===sel),1);saveDraft();location.hash="#keys/restart";});
for(constbofmain.querySelectorAll("[data-removecheck]"))b.onclick=()=>confirmBox("Remove this check from the key?","Remove",()=>{if(!ensureDraft())return;mine().checks.splice(+b.dataset.removecheck,1);saveDraft();route();});
for(consttaofmain.querySelectorAll("textarea.fixtable"))ta.onchange=()=>{try{setPath(ta.dataset.path,JSON.parse(ta.value));}catch(e){alertBox("Not a valid table.");}};
// ---- The READY page: what each checklist key looks at, and why it is amber or red right now ----
constREADY_WORDS={
tracks:"This weekend's Ableton set exists, has a song group for every Planning Center song, and is the set Live last opened.",
mix_foh:"Every song is mixed (a song whose tracks are all still at 0 dB is not) and its stems go out to the console, not to Master.",
lights:"The lighting cue track has a clip under every song.",
prop:"The ProPresenter playlist for the plan's date has every Planning Center item; MC announcements have a slide each and this week's notes; every person listed for baptisms or dedications has a slide.",
timecode:"Every song's presentation has a timeline, and Planning Center's LTC note for every song is the timecode the tracks play at that song's start.",
backgrounds:"Every song has at least one slide with a background.",
if(i<0)return`<div class="card"><h2 style="margin-top:4px">The weekly checklist</h2>${plan}<p class="small">Amber: still to do. Blinking red: still missing once it is due (${esc(((c.ready||{}).due || []).map((w) => w.days.join("/") + " " + w.start + "-" + w.end).join(", "))}). BOH MIX is tapped done on the deck and reset every ${esc((c.ready||{}).reset_day || "mon")}. Hold any key ${esc(hold)} to fix it or look again; hold READY to fix everything.</p><p class="small muted">Click a key to see what it checks and why it is amber or red.</p></div>`;
if(k.kind==="ready")what=`Sums up the page: how many keys still have something to do, and which to look at first. <b>Hold ${esc(hold)}</b>: fix everything on the page that can be fixed.`;
elseif(chk.type==="ready_manual")what=`Only a person can check this. <b>Tap</b> on the deck when it is done; tap again to take it back. It resets every ${esc((c.ready||{}).reset_day || "mon")}.`;
elseif(chk.type==="ready")what=`${esc(READY_WORDS[chk.item]||chk.item)} <b>Hold ${esc(hold)}</b>: ${esc(fix&&fix.action!=="recheck"?fix.label:"look again now")}.`;
elsewhat=`Watches ${esc((k.checks||[]).map((x)=>x.tag||x.msg||x.type).join(", "))}. <b>Hold ${esc(hold)}</b>: fix the one that dropped.`;
<div class="field"><span class="fl">flash while down${hint("Keeps flashing HOLD TO FIX while these are down; hold cmd/ctrl to pick several")}</span><select multiple size="5" class="watchsel" data-path="restart_page.keys.${i}.watch">${Object.keys(c.fixes).map((id)=>`<option value="${esc(id)}" ${(r.watch||[]).includes(id)?"selected":""}>fix ${esc(id)}</option>`).join("")}${[...conns].sort().map((cn)=>`<option value="conn:${esc(cn)}" ${(r.watch||[]).includes("conn:"+cn)?"selected":""}>connection ${esc(cn)}</option>`).join("")}</select></div>`:"";
return`<div class="card"><h2 style="margin-top:4px">${onOff(`restart_page.keys.${i}`,r,"this key")}${esc(r.label)} <span class="muted small">row ${r.pos[0]}, col ${r.pos[1]}</span></h2><p class="small"><b>Hold ${esc(hold)}</b> ${esc(f.label||r.fix)}, even when the Status page says it is fine.</p>${edit}
consttabs=`<div class="tabs"><button class="${compTab==="map"?"on":""}" onclick="location.hash='#computers'">Map</button><button class="${compTab==="gear"?"on":""}" onclick="location.hash='#computers/gear'" data-hint="Devices that are not computers: switcher, decks, wireless, LED">Gear</button><button class="${compTab==="setup"?"on":""}" onclick="location.hash='#computers/setup'" data-hint="The one-time set-up per computer, with the scripts">Setup</button>
body=`<div class="row" style="justify-content:flex-end;margin-bottom:6px"><span class="muted small">${looked?"looked "+esc(ago(Date.now()/1000-looked)):"not looked yet"}</span><button id="probe" class="small" ${m.probing?"disabled":""} data-hint="Log in to every computer now and refresh the lights (read-only)">${m.probing?"Looking…":"Check now"}</button></div>
<div class="tablewrap"><table><tr><th></th><th>status</th><th>computer</th><th>does</th><th>keys${hint("The keys that turn red if it dies")}</th><th></th></tr>${rows}</table></div>`;
for(constbofmain.querySelectorAll("[data-geardel]"))b.onclick=()=>confirmBox(`Remove ${b.dataset.geardel} from the gear list?`,"Remove",async()=>{try{constr=awaitpost("/api/gear",{name:b.dataset.geardel,remove:true});state.draft=r.draft;badge();route();}catch(e){alertBox(e.message);}});
construns=main.querySelector("#runs");if(runs)runs.onchange=()=>{if(!ensureDraft())return;try{state.draft.config.hosts[selected].runs=runs.value.split("\n").map((l)=>l.trim()).filter(Boolean).map((l)=>JSON.parse(l));saveDraft();}catch(err){alertBox("Each line must be a valid {...} entry.");}};
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("");
${c.os==="windows"?`<button class="small" data-job="pcprogram">+ Program this PC can start</button>`:""}</details>`:"";
constremove=editing()&&!c.self?`<p style="margin-top:12px"><button class="small danger" id="removehost" data-hint="Drops the fixes and checks that used it, and says which">Remove computer</button></p>`:"";
constwhat=[`the computer ${h.name||alias}`].concat(fixes.length?[`${fixes.length} fix${fixes.length>1?"es":""} that ran on it (${fixes.join(", ")})`]:[],checks.length?[`${checks.length} check${checks.length>1?"s":""} on the keys ${[...newSet(checks.map((x)=>x[2]))].join(", ")}`]:[],rkeys.length?[`the restart keys ${rkeys.join(", ")}`]:[]);
confirmBox(`Remove ${what.join(", ")}? The keys stay; a key left with nothing to check will say OK.`,"Remove",()=>{
return`<div class="tablewrap"><table><tr><th></th><th>device</th><th>kind</th><th>address</th><th>where${hint("Where it is in the building, and notes")}</th><th>watched by</th></tr>
dialog(`<h3>${existing?"Gear":"New gear"}${hint("A device that is not a computer. Give it a kind and an address; choose whether a key watches it.")}</h3>
<input id="gname" placeholder="name (Tascam, LED wall left, …)" value="${esc(cur.name||"")}" ${existing?"readonly":""}>
<input id="gaddr" placeholder="address (10.1.x.x), if it has one" value="${esc(cur.address||"")}">
<input id="gloc" placeholder="where it is" value="${esc(cur.location||"")}"><input id="gnotes" placeholder="notes" value="${esc(cur.notes||"")}">
<select id="gwatch"><option value="">not watched by a key</option><option value="ping">watch it: ping the address</option>${conns.map((n)=>`<option value="conn:${esc(n)}">watch its Companion connection ${esc(n)}</option>`).join("")}</select>
<select id="gkey"><option value="__new">on a new key</option>${plainKeys.map((t)=>`<option value="${esc(t.id)}">on the ${esc(t.label)} key</option>`).join("")}</select>
if(/watchdog and the settings/.test(step)){constok=(c.items||[]).filter((i)=>i.kind==="launchagent"&&/watchdog|settings/i.test(i.name));returnok.length&&ok.every((i)=>i.light==="ok")?"ok":"unknown";}
main.innerHTML=`<h1>Logs${hint("What the watchdog did and saw. Newest at the bottom.")}</h1><div class="tabs">${tabs}</div>${filters}${files}<pre class="log">${esc((data.lines||[]).join("\n"))||"(empty)"}</pre>`;
detail=`<div class="card"><h3>${esc(rest[0])}</h3>${d.changes_to_now.length?`<ul class="small">${d.changes_to_now.map((c)=>`<li>${esc(c.text)}</li>`).join("")}</ul>`:"<p class='muted small'>The same as now.</p>"}
${editing()?`<button class="primary" id="goback" data-hint="It becomes your draft; Apply puts it live">Go back to this</button>`:""}</div>`;
main.innerHTML=`<h1>History${hint("Every version ever applied: who, when, the note. Open one to see what differs from now.")}${editing()&&h.entries.length?`<button id="undo" class="small" style="margin-left:auto" data-hint="The version before the newest becomes your draft">Undo last change</button>`:""}</h1>
constun=main.querySelector("#undo");if(un)un.onclick=()=>confirmBox("Undo the last change? The version before it becomes your draft; Apply puts it live.","Undo",async()=>{try{constr=awaitpost("/api/undo");state.draft=r.draft;badge();location.hash="#review";}catch(e){alertBox(e.message);}});
?`<li><details><summary>${esc(c.text)}${c.command?' <span class="pill" data-hint="A command the watchdog will run; it never runs before Apply">command</span>':""}</summary>${c.before!=null?`<div class="small"><span class="muted">was</span> <span class="${c.command?"mono":""}">${esc(c.before)}</span></div>`:""}${c.after!=null?`<div class="small"><span class="muted">now</span> <span class="${c.command?"mono":""}">${esc(c.after)}</span></div>`:""}</details></li>`
:`<li>${esc(c.text)}</li>`).join("")+(r.atem_pending?"<li>A new saved switcher file replaces the current one.</li>":"");
main.innerHTML=`<h1>Apply${hint("Your changes in words, and what will happen on the deck. Nothing is live until you press APPLY.")}</h1>${state.draftStale?"<p class='notice'>The live settings changed since this draft was started and could not be merged: look through the list before applying.</p>":""}
${errors?`<div class="notice problem"><b>Not possible yet:</b><ul>${errors}</ul></div>`:""}${warnings?`<div class="notice grey"><ul>${warnings}</ul></div>`:""}${live}
<div class="row"><input id="note" placeholder="note for History" maxlength="200" style="flex:1;min-width:160px;padding:6px 8px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--fg);font:inherit"><button class="primary" id="apply" ${errors||(r.live&&r.live.live)||!editing()||!changes?"disabled":""}>APPLY</button><button id="discard" ${editing()?"":"disabled"} data-hint="Every change since the last Apply is lost">Throw away</button></div></div>
constem=main.querySelector("#emergency");if(em)em.onclick=()=>dialog(`<h3>Apply during a live service</h3><p>${esc(r.feel)} Say why, and type LIVE.</p><input id="why" placeholder="why it cannot wait"><input id="word" placeholder="type LIVE"><div class="row"><button class="primary danger" id="dok">Apply now</button><button id="dcancel">Cancel</button></div>`,(b)=>{b.querySelector("#dcancel").onclick=closeDialog;b.querySelector("#dok").onclick=()=>{constwhy=b.querySelector("#why").value.trim(),word=b.querySelector("#word").value.trim();if(word!=="LIVE"||!why)return;closeDialog();go({word,reason:why})();};});
main.querySelector("#discard").onclick=()=>confirmBox("Throw the draft away? Every change since the last Apply is lost.","Throw it away",async()=>{try{awaitpost("/api/draft",{},"DELETE");state.draft=null;state.atemPending=false;badge();review();}catch(e){alertBox(e.message);}});
["Red key","It names the problem. Press once to acknowledge (stays dark red). Hold 2 s: it runs the fix for what is failing."],
["Purple keys","The watchdog on the Ingest Mac has been silent 15 s. Overview › Restart watchdog, unless Companion itself is down."],
["Which computer or device?","Computers › Map: every computer and the keys that turn red if it dies. Computers › Gear: switcher, decks, wireless, LED."],
["Changing something","Edit (top right, the PIN). Change it where it lives: a key on Keys, a computer on Computers, the rest on Settings. The badge counts your changes and opens Apply."],
<p style="margin-top:12px"><a class="btn" href="/key-guide.html" target="_blank" data-hint="One entry per key: what it checks, what holding does, every word it can show">What each key does</a> <a class="btn" href="/cheat.html" target="_blank" data-hint="One printable page: both decks, the colours, press = acknowledge, hold = fix">Cheat sheet</a> <a class="btn" href="/where.html" target="_blank" data-hint="Every file, folder, log and port per computer">Where things are</a></p>
<details open><summary><b>How do I add…</b> (everything is done here; nothing needs a terminal)</summary><ul class="small howto"><li><b>Watch a new Companion connection.</b> Edit (top right, the PIN) › Settings › Companion. Find it marked “not watched” › <b>+ Key</b>. When the Status page is full it asks which group key to put it in (AUDIO, SHOW…): its problems show on that key, and holding the key reconnects it. Then the badge › Apply.</li><li><b>Add a connection to a key that is already there.</b> Keys › click the key › <b>+ check…</b> › “A Companion connection is good” › pick the connection from the list › fix: “reconnect its Companion connection” › Apply.</li><li><b>Watch that a device is on the network.</b> Keys › the key › <b>+ check…</b> › “An address answers ping” › type the address and the name the key should show (it says “NAME NO PING”).</li><li><b>Watch a program on another computer, and restart it.</b> Keys › the key › <b>+ check…</b> › “A command on another computer succeeds” › built as “A program is running on a Mac” › pick the computer and the program (on a Windows PC: Custom, e.g. <span class=\"mono\">tasklist | find /I \"OBS.exe\"</span>). Then fix › a fix that starts it (make one first: next line).</li><li><b>Make a new fix (something a hold can do).</b> Keys › Fixes › <b>+ Fix</b> › pick how it is built (start a Windows program, restart a Mac program, restart a helper, reopen Ableton…) and fill in the fields; the command is written for you. Then choose it as a check's “fix”, and use “Run the fix” to try it.</li><li><b>Let a fix run by itself on Sundays.</b> Settings › Auto-fix › tick it, and paint the hours on the calendar. A fix that closes a running program asks first.</li><li><b>A fallback button (the stream audio switches to FOH Backup by itself when Studio B dies).</b> Keys › Fixes › <b>+ Fix</b> › built as “Press a Companion button” › page, row, column of the button in Companion (page 4, row 0, column 5 is FOH Backup) › under more, tick failover (and pick the fix that switches back as undo). Then Keys › <b>+ Key</b> (or an existing key) › <b>+ check…</b> for what must be alive (an address answers ping, a program is running on a Mac, a Dante device) › fix: that press. Then Settings › <b>Auto switch</b>: tick it, paint the hours, and tune what counts as down (how often, how many misses). The watchdog presses once per fault, within about 5 s, even while ALARMS is muted, and never switches back by itself.</li><li><b>Add a line to the weekly checklist.</b> Keys › READY page › <b>+ READY key</b> › “Tap to confirm” (a person taps it done; resets Monday), “a checklist line the page checks by itself”, or “something else” (then + check… like any key: a connection, a ping, a program). Drag it where you want it.</li><li><b>Change where the READY page looks.</b> Settings › READY page: the Planning Center service type, the ProPresenter and Resolume addresses, the Tracks Mac and its projects folder, which locations get tracks, the lineup TV, the bundle folder, and when gaps turn red.</li><li><b>A new computer.</b> Computers › <b>+ Computer</b> (its log-in address), then Computers › Setup › that computer › copy its set-up script and run it there once.</li><li><b>Made a mistake?.</b> Nothing is live until Apply. After Apply: History › Undo, then Apply again.</li></ul></details>
<details><summary><b>Try it</b> (ten minutes with Editing on; nothing is live until APPLY)</summary><ol class="small">
<li>Keys › HD2: untick "may have no disk"; the badge counts it; Apply reads it back. Throw away.</li>
<li>Keys: drag LED onto the tray and back. + Key "HD7", add a check (Is a recorder OK?), delete it.</li>
<li>Settings › HyperDecks: untick 5 and 6, look at Keys (cells stay empty). Throw away.</li>
<li>Settings › Playlist: paste a link, Look it up; the name follows everywhere.</li>
<li>Settings › Auto-fix: paint Sunday morning on the calendar.</li>
<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>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>
<details><summary><b>Companion facts</b> (found by trying)</summary><dl class="kv small">${(facts.facts||[]).map(([q,a])=>`<dt>${esc(q)}</dt><dd>${esc(a)}</dd>`).join("")}</dl></details>`;
["failover","Auto switch","Press a Companion button by itself when something dies (Studio B -> FOH Backup): which button, when, and what counts as down"],
main.innerHTML=`<h1>Settings${editing()?"":hint("Switch Editing on (Edit, top right) to change anything")}</h1><div class="settings">${nav}<div><h2 style="margin-top:0">${esc(sec[1])}${hint(sec[2])}</h2>${body}</div></div>`;
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>`
+`<div class="card"><div class="field"><span class="fl">lists the slides must match${hint('PCO items whose description lists things: {"match": a word in the item title, "kind": "count" (one slide per - line) or "names" (every name has a slide), "short": what the key calls it}')}</span><textarea data-jsonpath="ready.prop_lists" rows="${Math.min(12,lists.split("\n").length+1)}" class="mono" ${editing()?"":"disabled"}>${esc(lists)}</textarea></div>
<p class="help">The keys themselves (add, move, delete, what each checks) are on Keys › READY page.</p></div>`;
return`<div class="card">${inputHtml("site.editing_code",Object.assign({}, f, { help: "The PIN that switches Editing on. Digits are easiest. It lives in the settings file, so everyone with the repo has it." }), (c.site || {}).editing_code)}</div>`+fieldsHtml("Site");
constsw=[];c.tiles.forEach((t,ti)=>(t.checks||[]).forEach((ch,j)=>{if(ch.type==="spotify")sw.push(`<p style="margin-top:0">${onOff(`tiles.${ti}.checks.${j}`,ch,"the Spotify check")} Watch that Spotify plays this playlist</p>`);}));
return`<div class="card">${sw.join("")}<div class="field"><span class="fl">playlist link${hint("Paste the link from Spotify (Share › Copy link). Look it up fills the name.")}</span><span class="row"><input id="splink" value="${esc(s.playlist||"")}" placeholder="https://open.spotify.com/playlist/…" ${editing()?"":"disabled"} style="flex:1;min-width:200px"> ${editing()?`<button class="small" id="splook">Look it up</button>`:""}</span></div>
constfixes=Object.entries(c.fixes).map(([id,f])=>{constcl=closes(f);conston=(c.auto_fix.fixes||[]).includes(id);return`<label class="inline" data-hint="${esc(f.label||id)}${cl?" · closes a program: ticking it allows that by itself":f.never_auto?" · marked never by itself: ticking it lifts that":f.anytime?" · any hour, not only these":""}"><input type="checkbox" data-autofix="${esc(id)}" ${on?"checked":""}${editing()?"":"disabled"}> ${esc(f.short||id)}${cl||f.restart_ok?" ⚠":""}</label>`;}).join(" ");
return`<div class="card"><p style="margin-top:0">${onOff("auto_fix",c.auto_fix,"auto-fix")} <b>Auto-fix</b> ${c.auto_fix.disabled?"is off: nothing is fixed by itself (holds still work).":"fixes the ticked fixes by itself in the painted hours."}</p>${calendarHtml("auto_fix.windows",c.auto_fix.windows)}</div>
<div class="card"><h3 style="margin-top:0">Fixes it may run${hint("Ticked fixes run by themselves inside the painted hours. ⚠ = it closes a running program to do its job; ticking it allows that without a hold.")}</h3><div class="chips">${fixes}</div></div><p class="help">Pressing a Companion button by itself when something dies is under Settings › <a href="#settings/failover">Auto switch</a>.</p>`+fieldsHtml("Schedules");
<div class="field"><span class="fl">show it on</span><select id="tkey">${keys.map((t)=>`<option value="${esc(t.id)}" ${home&&home.id===t.id?"selected":""}>${esc(t.label)}</option>`).join("")}<option value="">a new key inside a group…</option></select></div>
<div class="field" id="tnewkey" hidden><input id="tklabel" placeholder="the new key's name (STA AUDIO)"><select id="tgroup">${groups.map((g)=>`<option value="${esc(g.id)}">inside ${esc(g.label)}</option>`).join("")}</select></div>
constr=awaitpost("/api/build-check",{check:{type:"remote",template:"mac_program",params:{host,process:proc},msg:proc.toUpperCase()+" NOT RUNNING",fix,debounce_seconds:1}});
<td>${canRunNow(id)&&editing()?`<button class="small" data-runpress="${esc(id)}">Press it now</button>`:""}${editing()?` <button class="small danger" data-delpress="${esc(id)}" data-hint="Remove this button from the panel (Companion keeps it)">Remove</button>`:""}</td></tr>`).join("");
// What counts as down: every check whose fix is a switch, on whichever key it sits.
?`It presses <b>${esc(names.join(" / "))}</b> by itself when anything below goes down, once it has been down <b>${esc(wait)} s</b>, during <b>${esc(words)}</b>. It never switches back by itself.`
:"Nothing switches by itself yet: tick a button below.";
return`<div class="card"><p style="margin:0">${onOff("failover",fo,"the auto switch")}${fo.disabled?"<b>The auto switch is off:</b> nothing is pressed by itself. Everything below is kept.":summary}</p>${editing()?"":`<p class="help">Edit (top right, your PIN) to change anything on this screen; then Apply.</p>`}</div>
<div class="card"><h3 style="margin-top:0">What counts as down${hint("Any one of these switches. Faster checks switch sooner: ping every 1 s with 2 misses, a program every 2 s.")}</h3>
<p class="help" style="margin-top:0">How sure it must be before switching: <b>look every</b> = how often it checks; <b>misses before it counts</b> = pings that must fail in a row; <b>a fault must last</b> = seconds the fault must hold; then <b>wait before acting</b> (below) and it presses.</p>
<details><summary>Hours: <b>${esc(words)}</b> <span class="muted small">(open to paint)</span></summary>${calendarHtml("failover.windows",windows)}</details></div>
<div class="card"><h3 style="margin-top:0">The buttons${hint("Companion buttons the panel may press. Ticked = it presses it by itself when something above fails, once per fault; it never switches back by itself.")}</h3>
${presses.length?`<div class="tablewrap"><table><tr><th>switch by itself</th><th>Companion button</th><th>switch back with (by hand)</th><th></th></tr>${buttons}</table></div>`:"<p class='muted'>No Companion buttons yet.</p>"}
return`<div class="calwrap"><div class="cal ${editing()?"":"ro"}" data-calpath="${esc(path)}">${grid}</div><div class="small muted" style="max-width:220px"><b>${words}</b>${editing()?"<br>Click or drag to paint; drag over painted cells to clear.":""}</div></div>`;
constrows=decks.map((d)=>{constk=keyFor(d.label);constchk=k&&k.checks.find((ch)=>ch.conn===d.label);constki=k?c.tiles.indexOf(k):-1;return`<tr class="${k&&k.disabled?"off":""}"><td>${k?onOff(`tiles.${ki}`,k,"this deck's key"):""}</td><td><label class="inline"><input type="checkbox" data-deck="${esc(d.label)}" ${k?"checked":""}${editing()?"":"disabled"}> ${esc(d.label)}</label>${d.missing?` <span class="muted small" data-hint="A key watches it but Companion has no such connection">not in Companion</span>`:""}</td><td>${k?`<span data-hint="${k.pos?`row ${k.pos[0]}, column ${k.pos[1]}`:"in a group only"}">${esc(k.label)}</span>`:"<span class='muted'>no key</span>"}</td><td><label class="inline" data-hint="NO DISK is a warning, not an alarm"><input type="checkbox" data-nodisk="${esc(d.label)}" ${chk&&chk.optional?"checked":""}${editing()&&k?"":"disabled"}> no disk OK</label></td></tr>`;}).join("");
return`<div class="card">${!cc?"<p class='notice grey'>Companion is not answering; the list is from the keys.</p>":""}<div class="tablewrap"><table><tr><th></th><th>in use${hint("Tick a deck and it gets an HD key in the REC group; untick and the key goes (its cell stays empty)")}</th><th>key</th><th></th></tr>${rows||"<tr><td colspan=3 class='muted'>No HyperDeck connections in Companion.</td></tr>"}</table></div>
return`<div class="card"><div class="tablewrap"><table><tr><th></th><th>computer</th><th>kind</th><th>log-in</th><th>does</th><th>programs it can start</th></tr>${rows}</table></div>
${editing()?`<div class="row" style="margin-top:8px"><button class="small" data-job="computer">+ Computer</button><button class="small" data-job="pcprogram" data-hint="A program on a Windows PC the panel may start (a scheduled task)">+ PC program</button></div>`:""}</div>`;
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>
<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>`;
return`<div class="card"><div class="field"><span class="fl">SmartLife / Tuya keys${hint("Access ID and Access Secret of a Cloud Project on iot.tuya.com with your SmartLife app linked to it. They stay on this Mac in a private file, never in the settings file.")}</span><span class="row"><input id="oid" placeholder="Access ID" value="${esc(o.access_id||"")}" ${dis} style="width:14em"><input id="osecret" type="password" placeholder="${o.configured?"Access Secret (kept)":"Access Secret"}" ${dis} style="width:16em"><select id="oregion" ${dis}>${["us","eu","in","cn"].map((r)=>`<option ${o.region===r?"selected":""}>${r}</option>`).join("")}</select>${editing()?`<button class="small" id="okeys">Save & test</button>`:""}</span></div>
${o.configured?"":`<details><summary>how to get the keys</summary><ol class="small"><li>iot.tuya.com, free account, Cloud › Create Cloud Project (Smart Home, your region's data center).</li><li>In the project: Devices › Link App Account › scan the code with the SmartLife app (Me › scan icon).</li><li>Overview tab: copy Access ID and Access Secret here.</li></ol></details>`}</div>
${used?`<p class="small" style="margin-top:8px">Used by fixes: ${used}</p>`:""}
<p class="help">A fix "Power-cycle a smart outlet" (Keys › Fixes) cuts the power only when the device does not answer, never while a HyperDeck is recording.</p></div>`;
<input id="oname" placeholder="short name the fixes use (tascam)"><div class="row"><button class="primary" id="dok">Add</button><button id="dcancel">Cancel</button></div>`,(b)=>{
constsel=(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>`:"";
constconns=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("");
constdante=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("");
constmidi=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`<label class="onoff${on?" on":""}" data-hint="${on?"Watched. Switch off to stop watching it (the checks stay in the settings).":"Not watched (switched off). Switch on to watch it again."}"><input type="checkbox" data-connoff="${esc(label)}" ${on?"checked":""}${editing()?"":"disabled"}><span>${on?"ON":"OFF"}</span></label>`;
}
// The DANTE key's own device list: the Dante check with the most devices (STB AUDIO also watches one Dante device).
constrows=Object.entries(chk.devices||{}).map(([n,a])=>{conste=map[n]||{};constoff=offs.includes(n);return`<tr class="${off?"off":""}"><td><label class="onoff${off?"":" on"}" data-hint="${off?"Off: kept in the list, not watched":"On: watched. Switch off to keep it in the list without watching it."}"><input type="checkbox" data-danteoff="${esc(n)}" ${off?"":"checked"}${editing()?"":"disabled"}><span>${off?"OFF":"ON"}</span></label></td><td class="mono small">${esc(n)}</td><td>${editing()?`<input data-dantekey="${esc(n)}" value="${esc(a)}" size="10">`:esc(a)}</td><td class="mono small">${esc(e.ip||"")}</td><td class="small">${e.switch?esc(e.switch+" port "+e.port):""}</td><td>${e.poe?"yes":""}</td><td>${fixSel(n)}</td><td>${editing()?`<button class="small danger" data-dantedel="${esc(n)}">Remove</button>`:""}</td></tr>`;}).join("");
<p class="small muted">Map from the last survey ${snap.dante_map_age!=null?esc(ago(snap.dante_map_age)):"(none yet)"}.${editing()?` <button class="small" id="resurvey">Check the Dante network now</button>`:""}</p>
constpending=state.atemPending?`<div class="notice">A new saved switcher file is waiting for Apply. ${editing()?`<button class="small" id="atemdiscard">Discard it</button>`:""}</div>`:"";
${folder.blocked?`<div class="notice grey">The folder is there but this page cannot read it. Use Upload, or let the panel's key log in to this Mac (Computers › Setup).</div>`:""}<p id="ateminfo" class="help"></p></div>`;
confirmBox(`Remove the ${(d.fixes[id].params||{}).button || id} button from the panel?`+(users.length?` It is the switch for: ${users.join(", ")}; those checks lose their switch.`:"")+" Companion keeps the button itself.","Remove",()=>{
for(consttaofmain.querySelectorAll("[data-jsonpath]"))ta.onchange=()=>{try{setPath(ta.dataset.jsonpath,JSON.parse(ta.value));}catch(e){alertBox("That is not valid: "+e.message);}};
constok=q("#okeys");if(ok)ok.onclick=async()=>{ok.disabled=true;ok.textContent="Testing…";try{constr=awaitpost("/api/outlets/keys",{access_id:q("#oid").value,access_secret:q("#osecret").value,region:q("#oregion").value});alertBox(r.ok?`Works: ${r.devices} devices on the account.`:r.error);route();}catch(e){alertBox(e.message);route();}};
constoa=q("#oadd");if(oa)oa.onclick=outletDialog;
for(constbofmain.querySelectorAll("[data-oswitch]"))b.onclick=()=>{const[n,on]=b.dataset.oswitch.split("|");confirmBox(`Switch the ${n} outlet ${on==="1"?"on":"OFF"}?${on==="1"?"":" Whatever is plugged into it loses power."}`,on==="1"?"On":"Off",async()=>{try{constr=awaitpost("/api/outlets/switch",{name:n,on:on==="1"});if(!r.ok)alertBox(r.error);setTimeout(route,1500);}catch(e){alertBox(e.message);}});};
for(constbofmain.querySelectorAll("[data-oremove]"))b.onclick=()=>confirmBox(`Remove the ${b.dataset.oremove} outlet from the list? Fixes that use it stop working.`,"Remove",async()=>{try{awaitpost("/api/outlets",{name:b.dataset.oremove,remove:true});route();}catch(e){alertBox(e.message);}});
// The Status page is full: put it inside a group key, where it shows and a hold on the group fixes it.
constgroups=d.tiles.filter((g)=>g.members);
dialog(`<h3>No free spot on the Status page</h3><p class="small">Put ${esc(conn)} inside a group key: its problems show on that key, and holding the key reconnects it.</p><select id="ngroup">${groups.map((g)=>`<option value="${esc(g.id)}">${esc(g.label)} (${esc(g.members.join(", "))})</option>`).join("")}</select><div class="row"><button class="primary" id="dok">Add</button><button id="dcancel">Cancel</button></div>`,(bx)=>{
for(constbofmain.querySelectorAll("[data-rmcheck]"))b.onclick=()=>confirmBox("Remove this MIDI link from its key?","Remove",()=>{if(!ensureDraft())return;const[tid,j]=b.dataset.rmcheck.split("|");constt=state.draft.config.tiles.find((x)=>x.id===tid);t.checks.splice(+j,1);saveDraft();route();});
constrs=q("#resurvey");if(rs)rs.onclick=async()=>{try{awaitpost("/api/command",{text:"resurvey"});alertBox("Asked the watchdog to look at the switches; the map updates within a minute.");}catch(e){alertBox(e.message);}};
constsplink=q("#splink");if(splink)splink.onchange=()=>{constm=splink.value.match(/(?:playlist[/:])([A-Za-z0-9]{22})/);if(m)setPath("spotify.playlist","spotify:playlist:"+m[1]);elseif(splink.value.trim())alertBox("That is not a playlist link.");};
constib=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{constr=awaitpost("/api/program/install",{});alertBox(r.ok?r.detail:"Not started: "+r.error);setTimeout(()=>location.reload(),25000);}catch(e){alertBox(e.message);}});
constbf=q("#bundlefile");if(bf)bf.onchange=async()=>{constf=bf.files[0];if(!f)return;confirmBox(`Start a new campus from ${f.name}? Your current draft is replaced.`,"Start",async()=>{try{constr=awaitfetch("/api/site/import",{method:"POST",headers:{"X-Edit-Key":state.editing,"X-Editor":state.name,"Content-Type":"application/octet-stream"},body:awaitf.arrayBuffer()}).then((x)=>x.json());if(r.error)returnalertBox(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);}});};
alertBox(`${r.file?r.file+": ":""}${r.product||""}, ${r.audio} audio and ${r.camera} camera values; ${r.diff_count} differ from the current file${diffs?":\n"+diffs:"."}\nApply replaces the file.`);
dialog(`<h3>MIDI link${hint("A network MIDI session on one of the Macs, and the peer that must stay in it. Pick from what the Macs report now, or type the names if that Mac is off.")}</h3>
<input id="jpeer" list="jpeers" placeholder="peer (the other end)"><datalist id="jpeers"></datalist>
<input id="jtag" placeholder="key text when lost (LIGHT PC)" maxlength="10">
<input id="jaddr" placeholder="address, if it must be reconnected by address (optional)">
<select id="jkey">${midiKey?`<option value="${esc(midiKey.id)}">on the ${esc(midiKey.label)} key</option>`:""}<option value="__new">on a new key</option></select>
dialog(`<h3>New computer${hint("It appears on Computers; the Setup tab then shows what to do on it (login key, Remote Login). Fixes and checks for it come after.")}</h3>
<input id="jname" placeholder="name, as people say it (Lobby PC)"><input id="jalias" placeholder="short name – filled from the name">
if(!pcs.length)returnalertBox("No Windows PC is listed yet: add the computer first.");
constcur=(location.hash.split("/")[1]||"");
dialog(`<h3>Program a PC can start${hint("The panel starts programs on a PC through a scheduled task (the Setup tab's script creates it). This lists the task and adds a 'start it if it is not running' fix.")}</h3>
elseif(p.type==="list")input=`<input id="${id}" data-param="${p.name}" value="${esc(Array.isArray(v)?v.join(", "):v||"")}" placeholder="one or more, with commas">`;
inputs+=`<div class="field block"><span class="fl">command${hint("Typed by hand: zsh on a Mac, cmd.exe on a PC. Shown as a command at Apply; never run before Apply.")}</span><textarea data-path="fixes.${esc(id)}.run" rows="3" class="mono">${esc(f.run||"")}</textarea></div>`;
}elseif(f.run)inputs+=`<details><summary>the command it makes${f.kind==="local"?"":" (on "+esc((c.hosts[f.host]||{}).name || f.host) + ")"}</summary><pre class="log small" style="max-height:160px">${esc(f.run)}</pre></details>`;
main.innerHTML=`<h1>Keys${hint("Everything a hold can run. Click a fix to see how it is built.")}</h1><div class="tabs"><button onclick="location.hash='#keys'">Status page</button>${c.restart_page?`<button onclick="location.hash='#keys/restart'">Restart page</button>`:""}${c.ready_page?`<button onclick="location.hash='#keys/ready'">READY page</button>`:""}<button class="on">Fixes</button>${add}</div>
constdel=main.querySelector("#delfix");if(del)del.onclick=()=>confirmBox(`Delete the fix ${id}? No key uses it.`,"Delete",()=>{if(!ensureDraft())return;deletestate.draft.config.fixes[id];constaf2=state.draft.config.auto_fix.fixes||[];state.draft.config.auto_fix.fixes=af2.filter((x)=>x!==id);saveDraft();location.hash="#keys/fixes";});
constaf=main.querySelector("#addfix");if(af)af.onclick=()=>{constnid=main.querySelector("#newfix").value.trim();if(!/^[A-Za-z0-9_]+$/.test(nid)||c.fixes[nid])returnalertBox("A new id: letters, digits and _ only, not in use.");if(!ensureDraft())return;state.draft.config.fixes[nid]={kind:"local",short:nid.toUpperCase().slice(0,12),label:"",run:""};saveDraft();location.hash="#keys/fixes/"+nid;};