- ON/OFF on every key, check, fix, Restart and READY key, computer, MIDI link, HyperDeck,
Dante device, watched Companion connection, switcher and Spotify check, outlet fix, and one
main switch each for auto-fix, the auto switch and the READY page. Switched off = kept in
the settings, grey OFF on the deck, never checked or run (effective_config in the watchdog).
- Settings › Auto switch: a plain summary, what counts as down (each trigger editable in place:
address, program and computer, Dante device, the button it presses, timings), the hours
folded away, the buttons; + Button, + What counts as down, Remove.
- Pick a Companion button by clicking it on its page, drawn like Companion (read-only API:
/api/companion/pages and /api/companion/page).
- STREAM STB / STREAM FOH Restart keys removed (the Studio B page has its own); the alert names
the switch-back button instead.
- Dante screen: shows the DANTE key's device list again (it picked STB AUDIO's one device).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@@ -89,14 +89,14 @@ The stream's audio comes in on the Dante device STUDIO-A-Resi-AJA-SDI, channels
- **When:** inside the `"failover"` windows (Sunday 06:30–13:30 today), even while ALARMS is muted. Once per fault: it re-arms once the fault has been clear for 10 s (a check that cannot ask, say an SSH hiccup, does not count as clear).
- **When:** inside the `"failover"` windows (Sunday 06:30–13:30 today), even while ALARMS is muted. Once per fault: it re-arms once the fault has been clear for 10 s (a check that cannot ask, say an SSH hiccup, does not count as clear).
- **How fast:** about 5–6 s from a crash to the press (2–3 s to notice, 1 s debounce, 2 s `after_seconds`). A vanished Dante device takes as long as Companion's Dante module needs to notice it.
- **How fast:** about 5–6 s from a crash to the press (2–3 s to notice, 1 s debounce, 2 s `after_seconds`). A vanished Dante device takes as long as Companion's Dante module needs to notice it.
- **Unlike auto-fix:** it does not wait 20 s, does not stop for a network blip, and is not limited to 2 tries per 10 minutes. The one guard: when several Dante devices vanish at once (that is NA Ingest's view of Dante, not Studio B) it holds back, like auto-fix.
- **Unlike auto-fix:** it does not wait 20 s, does not stop for a network blip, and is not limited to 2 tries per 10 minutes. The one guard: when several Dante devices vanish at once (that is NA Ingest's view of Dante, not Studio B) it holds back, like auto-fix.
- **What the keys show:** AUDIO (and the hidden STB AUDIO) carry an extra line **ON FOH BACKUP** while the fault lasts and for 15 minutes after (`note_seconds`), or until STREAM STB is held. Hold AUDIO right after and it says JUST TRIED.
- **What the keys show:** AUDIO (and the hidden STB AUDIO) carry an extra line **ON FOH BACKUP** while the fault lasts and for 15 minutes after (`note_seconds`). Hold AUDIO right after and it says JUST TRIED.
- **Log and alert:** every press goes to the daily auto-fix log as `FAILOVER:` lines, to the watchdog log, and to `alert_webhook` if set.
- **Log and alert:** every press goes to the daily auto-fix log as `FAILOVER:` lines, to the watchdog log, and to `alert_webhook` if set.
- **Switching back is yours.** It never switches back by itself: when Studio B is up again, hold **STREAM STB** on the Restart page for 2 s (the fix's `"undo"`). That presses Studio B on the Audio page and clears ON FOH BACKUP.
- **Switching back is yours.** It never switches back by itself: when Studio B is up again, press **Studio B** on Companion's Studio B page (page 7) or Audio page (page 4). The alert names that button (the fix's `"undo"`).
- **Never:** a press outside the window, a second press for the same fault, or a press back to Studio B.
- **Never:** a press outside the window, a second press for the same fault, or a press back to Studio B.
In `config.json`: the `"failover"` block (`windows`, `after_seconds` 2, `note_seconds` 900); `"failover": true` and `"undo"` on the fix (`stream_foh_backup` undoes with `stream_studio_b`); `"interval_seconds"` on ping checks (default 3) and remote checks (default 10). The remote poll runs each computer's checks in its own thread, so a dead Studio B never delays the checks on another Mac.
In `config.json`: the `"failover"` block (`windows`, `after_seconds` 2, `note_seconds` 900); `"failover": true` and `"undo"` on the fix (`stream_foh_backup` undoes with `stream_studio_b`); `"interval_seconds"` on ping checks (default 3) and remote checks (default 10). The remote poll runs each computer's checks in its own thread, so a dead Studio B never delays the checks on another Mac.
**Another fallback button:** any Companion button can be a fix. On the settings page, Keys › Fixes › + Fix › "Press a Companion button" (page, row, column and what the button says), tick failover under more and pick its undo; Settings › Auto-fix › Failover calendar holds the window and `after_seconds`; Keys › + Key / + check… adds the checks that call it (a ping with interval seconds and failures, a program on a Mac, a Dante device) with fix = the press; Keys › Restart page › + Restart key adds the keys that press it by hand. The Help page "How do I add…" has an entry "A fallback button".
**Another fallback button:** any Companion button can be a fix. On the settings page, Keys › Fixes › + Fix › "Press a Companion button" (page, row, column and what the button says), tick failover under more and pick its undo; **Settings › Auto switch** has it all on one screen: which buttons switch by themselves (tick, page/row/column, the button to switch back with, Press it now), the hours (calendar), `after_seconds` and `note_seconds`, and what counts as down (how often each check looks, how many misses, how long a fault must last). Keys › + Key / + check… adds more checks that call it (a ping, a program on a Mac, a Dante device) with fix = the press. The Help page "How do I add…" has an entry "A fallback button".
## Restart page (page 20)
## Restart page (page 20)
@@ -112,8 +112,6 @@ Tap **RESTART PAGE** on the status page (row 2). Each key restarts one thing **e
| PRODDASH / CAPTIONS / BRIDGE | that service on NA Ingest |
| PRODDASH / CAPTIONS / BRIDGE | that service on NA Ingest |
| TASCAM | reconnects Companion to the recorder (see TASCAM above) |
| TASCAM | reconnects Companion to the recorder (see TASCAM above) |
| SPOTIFY | plays the Christian EDM 2026 playlist on NA Ingest (opens Spotify first if it is closed) |
| SPOTIFY | plays the Christian EDM 2026 playlist on NA Ingest (opens Spotify first if it is closed) |
| STREAM STB | presses Studio B on Companion's Audio page: the stream audio back on Studio B (how you switch back after a failover, see Audio failover) |
| STREAM FOH | presses FOH Backup on Companion's Audio page: the stream audio from the FOH console. Flashes HOLD TO FIX while Studio B is down; once the failover has pressed it, it shows ON FOH BACKUP (green) instead. |
A pressing key says PRESSING, then DONE; a press that finds no button there says NO BUTTON THERE.
A pressing key says PRESSING, then DONE; a press that finds no button there says NO BUTTON THERE.
constLABELS={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"};
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>`;
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>`:"";
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>`:""}
${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>`:""}
<divclass="field"><spanclass="fl">flashwhiledown${hint("Keeps flashing HOLD TO FIX while these are down; hold cmd/ctrl to pick several")}</span><selectmultiplesize="5"class="watchsel"data-path="restart_page.keys.${i}.watch">${Object.keys(c.fixes).map((id)=>`<optionvalue="${esc(id)}"${(r.watch||[]).includes(id)?"selected":""}>fix${esc(id)}</option>`).join("")}${[...conns].sort().map((cn)=>`<optionvalue="conn:${esc(cn)}"${(r.watch||[]).includes("conn:"+cn)?"selected":""}>connection${esc(cn)}</option>`).join("")}</select></div>`:"";
<divclass="field"><spanclass="fl">flashwhiledown${hint("Keeps flashing HOLD TO FIX while these are down; hold cmd/ctrl to pick several")}</span><selectmultiplesize="5"class="watchsel"data-path="restart_page.keys.${i}.watch">${Object.keys(c.fixes).map((id)=>`<optionvalue="${esc(id)}"${(r.watch||[]).includes(id)?"selected":""}>fix${esc(id)}</option>`).join("")}${[...conns].sort().map((cn)=>`<optionvalue="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">${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}
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}
${canRunNow(r.fix)&&editing()?`<button class="small" data-run="${esc(r.id)}|${esc(r.fix)}|0">Run the fix</button>`:""}<details><summary>more</summary><dlclass="kv"><dt>id</dt><ddclass="mono">${esc(r.id)}</dd>${r.watch&&r.watch.length?`<dt>flasheswhiledown</dt><dd>${esc(r.watch.join(","))}</dd>`:""}</dl></details>${editing()?`<pstyle="margin-top:12px"><buttonclass="smalldanger"id="delrkey">Deletekey</button></p>`:""}</div>`;
${canRunNow(r.fix)&&editing()?`<button class="small" data-run="${esc(r.id)}|${esc(r.fix)}|0">Run the fix</button>`:""}<details><summary>more</summary><dlclass="kv"><dt>id</dt><ddclass="mono">${esc(r.id)}</dd>${r.watch&&r.watch.length?`<dt>flasheswhiledown</dt><dd>${esc(r.watch.join(","))}</dd>`:""}</dl></details>${editing()?`<pstyle="margin-top:12px"><buttonclass="smalldanger"id="delrkey">Deletekey</button></p>`:""}</div>`;
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>
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>
${c.os==="windows"?`<button class="small" data-job="pcprogram">+ Program this PC can start</button>`:""}</details>`:"";
${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>`:"";
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>`:"";
<pstyle="margin-top:12px"><aclass="btn"href="/key-guide.html"target="_blank"data-hint="One entry per key: what it checks, what holding does, every word it can show">Whateachkeydoes</a><aclass="btn"href="/cheat.html"target="_blank"data-hint="Oneprintablepage:bothdecks,thecolours,press=acknowledge,hold=fix">Cheatsheet</a><aclass="btn"href="/where.html"target="_blank"data-hint="Everyfile,folder,logandportpercomputer">Wherethingsare</a></p>
<pstyle="margin-top:12px"><aclass="btn"href="/key-guide.html"target="_blank"data-hint="One entry per key: what it checks, what holding does, every word it can show">Whateachkeydoes</a><aclass="btn"href="/cheat.html"target="_blank"data-hint="Oneprintablepage:bothdecks,thecolours,press=acknowledge,hold=fix">Cheatsheet</a><aclass="btn"href="/where.html"target="_blank"data-hint="Everyfile,folder,logandportpercomputer">Wherethingsare</a></p>
["playlist","Playlist","The Spotify playlist the SPOTIFY key and the play fix use"],
["playlist","Playlist","The Spotify playlist the SPOTIFY key and the play fix use"],
["autofix","Auto-fix","When the panel may fix things by itself, and which fixes"],
["autofix","Auto-fix","When the panel may fix things by itself, and which fixes"],
["failover","Auto switch","Press a Companion button by itself when something dies (Studio B -> FOH Backup): which button, when, and what counts as down"],
["ready","READY page","Where the weekly checklist looks: Planning Center, ProPresenter, the Tracks Mac, the lineup TV, Resolume, the bundles"],
["ready","READY page","Where the weekly checklist looks: Planning Center, ProPresenter, the Tracks Mac, the lineup TV, Resolume, the bundles"],
["wait","Changes wait","When changes to the panel must wait (a recording deck always counts)"],
["wait","Changes wait","When changes to the panel must wait (a recording deck always counts)"],
["hyperdecks","HyperDecks","Which decks are in use"],
["hyperdecks","HyperDecks","Which decks are in use"],
return`<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>`
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>`
+fieldsHtml("Ready")
+fieldsHtml("Ready")
+`<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>
+`<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>
return`<div class="card"><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>
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>
<divclass="field"><spanclass="fl">name${hint("As the keys say it")}</span><inputid="spname"value="${esc(s.name||"")}"${editing()?"":"disabled"}></div><pid="spinfo"class="help"></p></div>`+fieldsHtml("Spotify");
<divclass="field"><spanclass="fl">name${hint("As the keys say it")}</span><inputid="spname"value="${esc(s.name||"")}"${editing()?"":"disabled"}></div><pid="spinfo"class="help"></p></div>`+fieldsHtml("Spotify");
}
}
functionautofixHtml(c){
functionautofixHtml(c){
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":""}"><input type="checkbox" data-autofix="${esc(id)}" ${on?"checked":""}${editing()?"":"disabled"}> ${esc(f.short||id)}${cl||f.restart_ok?" ⚠":""}</label>`;}).join(" ");
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":""}"><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>
<divclass="card"><h3style="margin-top:0">Fixesitmayrun${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><divclass="chips">${fixes}</div></div>`+failoverHtml(c)+fieldsHtml("Schedules");
<divclass="card"><h3style="margin-top:0">Fixesitmayrun${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><divclass="chips">${fixes}</div></div><pclass="help">PressingaCompanionbuttonbyitselfwhensomethingdiesisunderSettings›<ahref="#settings/failover">Autoswitch</a>.</p>`+fieldsHtml("Schedules");
}
}
// Which keys name each fix (on a check, as hold-also-when-green, or on a restart key).
// Which keys name each fix (on a check, as hold-also-when-green, or on a restart key).
<divclass="field"id="tnewkey"hidden><inputid="tklabel"placeholder="the new key's name (STA AUDIO)"><selectid="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}});
return`<div class="card"><h3 style="margin-top:0">Failover${hint("A fix marked failover runs by itself within seconds when its check fails, inside these hours, even while ALARMS is muted, once per fault; it never switches back by itself.")}</h3>
<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>
<divclass="card"><h3style="margin-top:0">Whatcountsasdown${hint("Any one of these switches. Faster checks switch sooner: ping every 1 s with 2 misses, a program every 2 s.")}</h3>
<divclass="card"><h3style="margin-top:0">Thebuttons${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>"}
constrows=decks.map((d)=>{constk=keyFor(d.label);constchk=k&&k.checks.find((ch)=>ch.conn===d.label);return`<tr><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("");
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>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">${!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>
${editing()&&rows?`<div class="row" style="margin-top:8px"><button class="primary small" id="decksave">Use these decks</button></div>`:""}</div>`;
${editing()&&rows?`<div class="row" style="margin-top:8px"><button class="primary small" id="decksave">Use these decks</button></div>`:""}</div>`;
return`<div class="card"><div class="tablewrap"><table><tr><th>computer</th><th>kind</th><th>log-in</th><th>does</th><th>programs it can start</th></tr>${rows}</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>`;
${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>`;
}
}
asyncfunctionprogramHtml(){
asyncfunctionprogramHtml(){
@@ -1007,7 +1240,7 @@
consto=awaitapi("/api/outlets");
consto=awaitapi("/api/outlets");
constdis=editing()?"":"disabled";
constdis=editing()?"":"disabled";
constrows=Object.entries(o.outlets||{}).map(([n,v])=>`<tr><td><b>${esc(n)}</b></td><td class="small">${esc(v.name||"")}</td><td class="small" id="ost-${esc(n)}"><span class="muted">…</span></td><td>${editing()?`<button class="small" data-oswitch="${esc(n)}|1" data-hint="Switch the outlet on">On</button> <button class="small danger" data-oswitch="${esc(n)}|0" data-hint="Switch the outlet off: whatever is on it loses power">Off</button> <button class="small" data-oremove="${esc(n)}">Remove</button>`:""}</td></tr>`).join("");
constrows=Object.entries(o.outlets||{}).map(([n,v])=>`<tr><td><b>${esc(n)}</b></td><td class="small">${esc(v.name||"")}</td><td class="small" id="ost-${esc(n)}"><span class="muted">…</span></td><td>${editing()?`<button class="small" data-oswitch="${esc(n)}|1" data-hint="Switch the outlet on">On</button> <button class="small danger" data-oswitch="${esc(n)}|0" data-hint="Switch the outlet off: whatever is on it loses power">Off</button> <button class="small" data-oremove="${esc(n)}">Remove</button>`:""}</td></tr>`).join("");
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>
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>
${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>
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("");
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>`:"";
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">${pending}
constatemChecks=[];c.tiles.forEach((t,ti)=>(t.checks||[]).forEach((ch,j)=>{if(ch.type==="atem"||(ch.type==="pref"&&/atem/i.test(ch.domain||"")))atemChecks.push(`<label class="inline small" style="margin-right:14px">${onOff(`tiles.${ti}.checks.${j}`,ch,"this switcher check")}${esc(ch.type==="atem"?"switcher "+ch.part+" matches the file":ch.msg||"ATEM Software Control setting")}</label>`);}));
${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>`:""}<pid="ateminfo"class="help"></p></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>`:""}<pid="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);}};
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();}};
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();}};
@@ -1256,7 +1595,7 @@
asyncfunctionfixes(rest){
asyncfunctionfixes(rest){
constc=cfg(),auto=newSet(c.auto_fix.fixes||[]);
constc=cfg(),auto=newSet(c.auto_fix.fixes||[]);
constused=fixUsers(c);
constused=fixUsers(c);
constrows=Object.entries(c.fixes).map(([id,f])=>`<tr class="click${rest[0]===id?" sel":""}" onclick="location.hash='#keys/fixes/${esc(id)}'"><td><b>${esc(f.short||id)}</b></td><td class="small">${esc(f.label||"")}</td><td class="small">${esc(f.kind||"ssh")}${f.host?" on "+esc((c.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">${f.failover?"by itself (failover)":auto.has(id)?"by itself":f.never_auto?"never by itself":"hold only"}</td></tr>`).join("");
constrows=Object.entries(c.fixes).map(([id,f])=>`<tr class="click${rest[0]===id?" sel":""}${f.disabled?" off":""}" onclick="location.hash='#keys/fixes/${esc(id)}'"><td>${onOff(`fixes.${id}`,f,"this fix")}</td><td><b>${esc(f.short||id)}</b></td><td class="small">${esc(f.label||"")}</td><td class="small">${esc(f.kind||"ssh")}${f.host?" on "+esc((c.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">${f.failover?"by itself (failover)":auto.has(id)?"by itself":f.never_auto?"never by itself":"hold only"}</td></tr>`).join("");
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>
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>
self.assertEqual(tiles["audio"]["x"],"AUDIO\nSTUDIO B NO PING")
self.assertEqual(tiles["audio"]["x"],"AUDIO\nSTUDIO B NO PING")
Reference in New Issue
Block a user
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.