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).`;
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();}
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>`:"";
constlive=isLive?`<div class="notice">Live now: ${esc(o.live.reason)}. Changes to the panel wait.</div>`:"";
<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,cfg(),"status")}
<div class="row" style="margin-top:10px"><button disabled title="arrives with step 4">Change the playlist</button><button disabled title="arrives with step 4">Add or remove a HyperDeck</button><button disabled title="arrives with step 4">Add a MIDI link</button></div></div>
<dt>Companion</dt><dd>${o.companion_ok?"OK":"<b>not answering</b>"}${o.pages&&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>
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");
confirmBox(`Run "${f.label||fixId}" now? It runs through the watchdog exactly like a hold on the ${key.toUpperCase()} key.`,"Run it",async()=>{
try{awaitpost("/api/command",{text:`test ${key}${fixId}${which}`});alertBox("Sent. Watch the key, or Logs › Watchdog (TEST).");}catch(e){alertBox(e.message);}
letpanel=`<p class="muted">Click a key to see what it watches and what holding it does.${editing()?" With Editing on you can change it here.":""}</p>`;
main.innerHTML=`<h1>Keys</h1><p class="what">The deck as people hold it. Click a key; with Editing on, change what it watches and what holding it does (moving keys arrives with step 3).</p>${tabs}
<div class="cols"><div>${deckHtml(o.deck,c,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 id="panel">${panel}</div></div>`;
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"><label>Fix: what holding runs for this check</label><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}">Run this fix now</button>`:"";
constholdAlways=editing()&&plain?`<div class="field"><label>Hold runs even when the key is green</label><select data-path="tiles.${i}.hold_always" class="holdsel"><option value="">no – only what is failing</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>`:"";
constpos=`<details><summary>Advanced</summary><dl class="kv"><dt>id</dt><dd class="mono">${esc(t.id)}</dd></dl>${editing()&&!t.kind?`<div class="field"><label>Position (row, column; blank = inside its group only)</label><input id="pos" value="${esc(t.pos?t.pos.join(", "):"")}" placeholder="row, column"></div>`:""}</details>`;
for(constbofmain.querySelectorAll("[data-removecheck]"))b.onclick=()=>confirmBox("Remove this check from the key?","Remove",()=>{if(!ensureDraft())return;state.draft.config.tiles[i].checks.splice(+b.dataset.removecheck,1);saveDraft();route();});
constpos=main.querySelector("#pos");if(pos)pos.onchange=()=>{if(!ensureDraft())return;constt=state.draft.config.tiles[i];constm=pos.value.match(/^\s*(\d+)\s*,\s*(\d+)\s*$/);if(m)t.pos=[+m[1],+m[2]];elseif(!pos.value.trim())deletet.pos;elsereturnalertBox("Row, column: two numbers, like 2, 5.");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.");}};
<div class="field"><label>Keep flashing HOLD TO FIX while these are down (hold cmd/ctrl for several)</label><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>${esc(r.label)}</h2><p class="muted">row ${r.pos[0]}, column ${r.pos[1]}</p><p><b>HOLDING ${esc(hold)} WILL:</b> ${esc(f.label||r.fix)}, even when the Status page says it is fine.</p>${r.watch&&r.watch.length?`<p class="small muted">Keeps flashing HOLD TO FIX while down: ${esc(r.watch.join(", "))}</p>`:""}${edit}
${canRunNow(r.fix)&&editing()?`<button class="small" data-run="${esc(r.id)}|${esc(r.fix)}|0">Run this fix now</button>`:""}<details><summary>Advanced</summary><dl class="kv"><dt>id</dt><dd class="mono">${esc(r.id)}</dd></dl></details></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}`;
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("");
constedit=editing()?`<details open><summary>Change this computer</summary>${["name","role","notes","os"].concat(c.self?[]:["ssh","mac","wake_broadcast","pingable"]).map((f)=>inputHtml(base+f,F("hosts.*."+f),getPath(base+f))).join("")}
<div class="field"><label>What runs here (one per line, as {"kind": ..., "name": ...})</label><textarea id="runs" rows="${Math.max(4,(getPath(base+"runs")||[]).length+1)}" class="mono">${esc((getPath(base+"runs")||[]).map((r)=>JSON.stringify(r)).join("\n"))}</textarea><div class="help">Kinds: sshd, app (process), launchagent (label), daemon (process), task (name, exe), service (name), web (url), helper_dvs, helper_solo (label), midi_session (name, peer_of), artnet (source), folder (path), disabled (label). "watched": false for things the panel only lists.</div></div></details>`:"";
return`<p class="small muted">Devices that are not computers, from the keys that watch them.${editing()?" Type where each one lives and press Save.":""}</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/i.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>`;
detail=`<div class="card"><h2>${esc(rest[0])}</h2><p class="muted">What is different between that version and now:</p>${d.changes_to_now.length?`<ul>${d.changes_to_now.map((c)=>`<li>${esc(c.text)}</li>`).join("")}</ul>`:"<p>Nothing: it is the same as now.</p>"}
${editing()?`<button class="primary" id="goback">Go back to this version</button> <span class="help">It becomes your draft; Review & Apply puts it live.</span>`:`<p class="help">Switch Editing on to go back to it.</p>`}</div>`;
constun=main.querySelector("#undo");if(un)un.onclick=()=>confirmBox("Undo the last change? The version before it becomes your draft; Review & Apply puts it live.","Undo",async()=>{try{constr=awaitpost("/api/undo");state.draft=r.draft;badge();location.hash="#review";}catch(e){alertBox(e.message);}});
main.innerHTML=`<h1>Review & Apply</h1><p class="what">Nothing to apply: there is no draft. Change something on any screen with Editing on.</p>${st.state==="done"||st.state==="failed"?applyResult(st):""}`;
return;
}
constr=awaitpost("/api/validate",{});
constchanges=r.changes.map((c)=>`<li class="${c.command?"command":""}">${esc(c.text)}${c.command?' <span class="pill">command</span>':""}</li>`).join("")+(r.atem_pending?"<li>A new saved switcher file replaces the current one.</li>":"");
constlive=r.live&&r.live.live?`<div class="notice">Live now: ${esc(r.live.reason)}. Apply waits until it is over; your draft is saved. ${editing()?'<button id="emergency" class="danger">Apply anyway (emergency)</button>':""}</div>`:"";
main.innerHTML=`<h1>Review & Apply</h1><p class="what">Started by ${esc(state.draft.started_by||state.draft.by||"?")}${state.draft.started?" at "+esc(when(state.draft.started)):""}. ${state.draftStale?"<b>The live settings changed since this draft was started: look again before applying.</b>":""}</p>
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);}});
}
functionapplyProgress(st){
main.innerHTML=`<h1>Applying…</h1><p class="what">${esc(st.note||"")}</p>${stepsHtml(st)}<p class="muted">This page updates by itself.</p>`;
["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 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 press Restart the watchdog on the Overview."],
["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>Changing something</h2><ol><li>Switch Editing on (top right): the editing code and your first name.</li><li>Change it where it lives: a key on Keys, a computer on Computers, a fix on Fixes, the rest on Site setup.</li><li>The badge at the top counts your changes; it opens Review & Apply.</li><li>Review reads your changes back in plain words and says what will happen (the deck goes purple for a few seconds; pages re-import only when the layout changed). Press APPLY.</li><li>If the watchdog does not come back, the previous version is put back by itself. History keeps every version; Go back to any of them.</li></ol>
<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>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, then pick it on Site setup › ATEM.</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>`;
letbody=own.length?`<div class="card"><h3>Settings</h3>${plain.map(fieldHtml).join("")||"<p class='muted small'>Only numbers here; they sit under Advanced.</p>"}${adv.length?`<details><summary>Advanced</summary>${adv.map(fieldHtml).join("")}</details>`:""}</div>`:"";
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()?"":" Switch Editing on to change anything."}</p><div class="tabs">${tabs}</div>${body}`;
constfixes=Object.entries(c.fixes).map(([id,f])=>{constcl=closes(f);conston=(c.auto_fix.fixes||[]).includes(id);return`<label class="inline"><input type="checkbox" data-autofix="${esc(id)}" ${on?"checked":""}${editing()&&!cl&&!f.never_auto?"":"disabled"}> ${esc(f.short||id)}${cl?' <span class="muted small">(closes a program: hold only)</span>':f.never_auto?' <span class="muted small">(never by itself)</span>':""}</label>`;}).join(" ");
return`<div class="notice">Alarms are on every day. The ALARMS key silences them until midnight, then they come back by themselves. Changes to the panel wait while a HyperDeck is recording.</div>
<div class="card"><h3>Auto-fix</h3>${windowsHtml("auto_fix.windows",c.auto_fix.windows,"When auto-fix may act by itself","An end earlier than the start runs past midnight.")}<div class="field"><label>Fixes it may run (gentle ones only)</label>${fixes}</div></div>
<div class="card"><h3>Changes wait</h3>${windowsHtml("site.live_services",(c.site||{}).live_services, "Clock windows during which changes to the panel wait", "A recording HyperDeck always counts, whatever the clock says. NA has no clock windows.")}</div>`;
return`<div class="card"><h3>Companion ${cc.ok?"OK":"<b>not answering</b>"}${cc.build?` <span class="pill">${esc(cc.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>
${(cc.missing||[]).length?`<p class="notice">Keys watch connections Companion does not have: ${esc(cc.missing.join(", "))}</p>`:""}
${cc.page_number_buttons?`<p class="small">Page-number buttons coloured on ${cc.page_number_buttons.pages_coloured} pages (${esc(cc.page_number_buttons.recorded_in)}).</p>`:""}
return`<div class="card"><h3>Dante device list (the DANTE key's devices)</h3><div class="tablewrap"><table><tr><th>Dante name</th><th>key text</th><th>address</th><th>switch / port</th><th>PoE</th><th>when it is gone, hold runs</th><th></th></tr>${rows}</table></div>
<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>
${editing()?`<div class="field"><label>Add a device</label><input id="dantenew" placeholder="Dante name (as Dante Controller shows it)" list="dantefound"><datalist id="dantefound">${extra.map((n)=>`<option value="${esc(n)}">`).join("")}</datalist> <input id="dantenewkey" placeholder="key text" size="10"> <button class="small" id="danteadd">Add</button>${extra.length?`<div class="help">Seen on the network but not listed: ${esc(extra.join(", "))}</div>`:""}</div>`:""}</div>`;
}
functionspotifyHtml(c){
consts=c.spotify||{};
return`<div class="card"><h3>The service playlist</h3><div class="field"><label>Playlist link</label><input id="splink" value="${esc(s.playlist||"")}" placeholder="paste the open.spotify.com/playlist/... link" ${editing()?"":"disabled"}> ${editing()?`<button class="small" id="splook">Look it up</button>`:""}<div class="help">The SPOTIFY key, the play fix and the key guide all use it.</div></div>
<div class="field"><label>Name</label><input id="spname" value="${esc(s.name||"")}" ${editing()?"":"disabled"}><div class="help">As the keys say it.</div></div><p id="spinfo" class="help"></p></div>`;
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>`:"";
return`<div class="card"><h3>The saved switcher file</h3><p class="small muted">The panel keeps the switcher's audio and camera settings in line with this file (atem-reference.xml). Save a new one from ATEM Software Control when the show changes.</p>${pending}
${editing()?`<div class="row"><label class="btn">Upload a saved file <input type="file" id="atemfile" accept=".xml" hidden></label><button id="atemnewest" ${folder.mounted&&(folder.files||[]).length?"":"disabled"}>Pick the newest from the folder</button></div>`:""}
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);}};
constlook=q("#splook");if(look)look.onclick=async()=>{constinfo=q("#spinfo");info.textContent="Looking…";try{constr=awaitapi("/api/live/spotify?url="+encodeURIComponent(q("#splink").value));if(!r.ok){info.textContent=r.error;return;}if(!ensureDraft())return;setPath("spotify.playlist",r.uri);if(r.name){setPath("spotify.name",r.name);q("#spname").value=r.name;}q("#splink").value=r.uri;info.textContent=r.name?`${r.name}: ${r.songs} songs. The play fix's words follow the name.`:(r.warning||"");}catch(e){info.textContent=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 or URI.");};
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.`);
if(fl==="run")return`<div class="field"><label>Command (shown as a command at Review; never run before Apply)</label><textarea data-path="fixes.${esc(id)}.run" rows="3" class="mono">${esc(f.run||"")}</textarea><div class="help">zsh on a Mac, cmd.exe on a PC. ${esc(Fd.help)}</div></div>`;
detail=`<div class="card"><h2>${esc(f.short||id)}</h2><p>${esc(f.label||"")}</p>${kindSel}${inputs}${canRunNow(id)&&editing()?`<p class="help">Run it from the key it belongs to (${esc(keys.join(", ")||"none yet")}).</p>`:""}<details ${editing()?"":"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("")}<dt>id</dt><dd class="mono">${esc(id)}</dd></dl></details>${editing()&&!keys.length?`<button class="danger small" id="delfix">Delete this fix</button>`:""}</div>`;
}
constadd=editing()?`<div class="row"><input id="newfix" placeholder="new fix id (letters, digits, _)"><button id="addfix" class="small">+ New fix</button></div>`:"";
main.innerHTML=`<h1>Fixes</h1><p class="what">Everything a hold can run. ${Object.keys(c.fixes).length} today.</p>${add}<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}`;
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="#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="#fixes/"+nid;};