Settings page: add and edit everything by hand, READY page included
- Keys › READY page: + READY key (tap to confirm, a checklist line the page checks itself, or any check), and the same check/fix editor the Status page keys have; delete a key. - Settings › READY page: when gaps turn red (calendar), every address and folder it looks at, and the lists the slides must match. - Help: "How do I add…", step by step: a Companion connection, a ping, a program, a fix, auto-fix, a checklist line, where READY looks, a computer, undo. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+71
-21
@@ -169,7 +169,7 @@
|
||||
function fieldFor(path) {
|
||||
const F = state.schema.fields;
|
||||
if (F[path]) return F[path];
|
||||
const generic = path.replace(/^tiles\.\d+\.checks\.\d+\./, "tiles[].checks[].").replace(/^tiles\.\d+\./, "tiles[].")
|
||||
const generic = path.replace(/^(tiles|ready_page\.keys)\.\d+\.checks\.\d+\./, "tiles[].checks[].").replace(/^(tiles|ready_page\.keys)\.\d+\./, "tiles[].")
|
||||
.replace(/^restart_page\.keys\.\d+\./, "restart_page.keys[].").replace(/^fixes\.[^.]+\./, "fixes.*.").replace(/^hosts\.[^.]+\./, "hosts.*.")
|
||||
.replace(/^gear\.[^.]+\./, "gear.*.").replace(/^(auto_fix\.windows|site\.live_services|service_windows)\.\d+\./, "$1[].");
|
||||
return F[generic] || null;
|
||||
@@ -311,20 +311,48 @@
|
||||
const tabs = `<div class="tabs"><button class="${restart || ready ? "" : "on"}" onclick="location.hash='#keys'">Status page</button>${c.restart_page ? `<button class="${restart ? "on" : ""}" onclick="location.hash='#keys/restart'">Restart page</button>` : ""}${c.ready_page ? `<button class="${ready ? "on" : ""}" onclick="location.hash='#keys/ready'" data-hint="The weekly worship checklist, checked against Planning Center">READY page</button>` : ""}<button onclick="location.hash='#keys/fixes'">Fixes</button>
|
||||
${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>`;
|
||||
let panel = `<p class="muted small">Click a key.${editing() ? " Drag to move; drop on the tray to hide." : ""}</p>`;
|
||||
if (ready) { const i = sel && c.ready_page ? c.ready_page.keys.findIndex((x) => x.id === sel) : -1; panel = await readyKeyPanel(i, hold); }
|
||||
if (ready) {
|
||||
const i = sel && c.ready_page ? c.ready_page.keys.findIndex((x) => x.id === sel) : -1;
|
||||
panel = await readyKeyPanel(i, hold);
|
||||
if (i >= 0 && editing() && c.ready_page.keys[i].kind !== "ready") panel += await keyPanel(i, hold, "ready_page.keys");
|
||||
}
|
||||
else if (sel && !restart) { const i = c.tiles.findIndex((x) => x.id === sel); if (i >= 0) panel = await keyPanel(i, hold); }
|
||||
else if (sel && restart && c.restart_page) { const i = c.restart_page.keys.findIndex((x) => x.id === sel); if (i >= 0) panel = restartKeyPanel(i, hold); }
|
||||
const trayKeys = c.tiles.filter((t) => !t.pos && t.kind !== "master");
|
||||
const tray = trayKeys.map((t) => `<button class="small"${editing() ? ` draggable="true" data-drag="tile:${esc(t.id)}"` : ""} onclick="location.hash='#keys/${esc(t.id)}'">${esc(t.label)}</button>`).join(" ");
|
||||
let note = "";
|
||||
if (state.draft) { try { const v = await post("/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) {} }
|
||||
const adders = editing() && !ready ? (restart ? `<button id="newrkey" class="small">+ Restart key</button>` : `<button id="newkey" class="small">+ Key</button>`) : "";
|
||||
const adders = editing() ? (ready ? (c.ready_page ? `<button id="newdkey" class="small">+ READY key</button>` : "") : restart ? `<button id="newrkey" class="small">+ Restart key</button>` : `<button id="newkey" class="small">+ Key</button>`) : "";
|
||||
main.innerHTML = `<h1>Keys${hint("The deck as people hold it. Click a key to see what it watches and what holding it does.")}</h1>${tabs}${note}
|
||||
<div class="cols keycols"><div>${restart && !c.restart_page ? "<p class='muted'>No Restart page.</p>" : ready && !c.ready_page ? "<p class='muted'>No READY page.</p>" : deckHtml(o.deck, c, restart ? "restart" : ready ? "ready" : "status", sel, true)}
|
||||
${!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>` : ""}
|
||||
<div class="row" style="margin-top:8px">${adders}</div></div><div id="panel">${panel}</div></div>`;
|
||||
wireKeyPanel(sel, restart || ready);
|
||||
wireKeyPanel(sel, restart, ready ? "ready_page.keys" : "tiles");
|
||||
wireLayout(restart);
|
||||
const nd = main.querySelector("#newdkey"); if (nd) nd.onclick = () => newReadyKey();
|
||||
}
|
||||
// A new READY page key: a checklist line the page checks itself, a tap-to-confirm line, or any check.
|
||||
function newReadyKey() {
|
||||
const c = 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>
|
||||
<select id="nitem" hidden>${Object.entries(READY_WORDS).map(([k, w]) => `<option value="${k}">${esc(k)} – ${esc(w.slice(0, 60))}</option>`).join("")}</select>
|
||||
<input id="npos" placeholder="row, column" value="${(firstEmpty(c, "ready") || []).join(", ")}"><div class="row"><button class="primary" id="dok">Add</button><button id="dcancel">Cancel</button></div>`, (b) => {
|
||||
const kind = b.querySelector("#nkind"), item = b.querySelector("#nitem");
|
||||
kind.onchange = () => { item.hidden = kind.value !== "ready"; };
|
||||
b.querySelector("#dcancel").onclick = closeDialog;
|
||||
b.querySelector("#dok").onclick = () => {
|
||||
const label = b.querySelector("#nlabel").value.trim().toUpperCase();
|
||||
if (!label) return alertBox("Give it a label.");
|
||||
const pos = parsePos(b.querySelector("#npos").value, cfg(), "ready", false); if (!pos) return;
|
||||
if (!ensureDraft()) return;
|
||||
const keys = state.draft.config.ready_page.keys;
|
||||
let id = "rd_" + (slug(label) || "key"), n = 2; while (keys.some((k) => k.id === id) || state.draft.config.tiles.some((t) => t.id === id)) id = "rd_" + slug(label) + "_" + n++;
|
||||
const checks = kind.value === "manual" ? [{ type: "ready_manual", item: slug(label) || id }]
|
||||
: kind.value === "ready" ? [{ type: "ready", item: item.value, fix: "ready_recheck" }] : [];
|
||||
keys.push({ id, label, pos, checks }); saveDraft(); closeDialog(); location.hash = "#keys/ready/" + id;
|
||||
};
|
||||
});
|
||||
}
|
||||
function takenCells(c, which) {
|
||||
const taken = new Set(["0,0", "1,0", "2,0"]);
|
||||
@@ -416,8 +444,11 @@
|
||||
}); };
|
||||
}
|
||||
|
||||
async function keyPanel(i, hold) {
|
||||
const c = cfg(), t = c.tiles[i];
|
||||
// A Status page tile ("tiles") or a READY page key ("ready_page.keys"): the same editor for both.
|
||||
const keyList = (c, list) => (list === "ready_page.keys" ? (c.ready_page || { keys: [] }).keys : c.tiles);
|
||||
const READY_ONLY = ["ready", "ready_manual"];
|
||||
async function keyPanel(i, hold, list = "tiles") {
|
||||
const c = cfg(), t = keyList(c, list)[i], onReady = list !== "tiles";
|
||||
const k = await api("/api/keys");
|
||||
const kt = k.tiles.find((x) => x.id === t.id) || { says: [], holding: "", checks: [] };
|
||||
// A Companion connection is picked from the ones Companion has (typing still works).
|
||||
@@ -434,9 +465,13 @@
|
||||
const header = q !== lastQ ? `<div class="chkgroup">${esc(q)}</div>` : ""; lastQ = q;
|
||||
const summary = `<summary><b>${what}</b>${hint(sentence)}</summary>`;
|
||||
if (!editing()) return `${header}<details class="chk">${summary}<p class="small">${esc(sentence)}</p></details>`;
|
||||
const base = `tiles.${i}.checks.${j}.`;
|
||||
const base = `${list}.${i}.checks.${j}.`;
|
||||
let fields = (spec.fields || []).filter((f) => !["fix", "type", "template", "params"].includes(f)).map((f) => f === "conn"
|
||||
? inputHtml(base + f, F("tiles[].checks[]." + f), chk[f], connList).replace("<input ", '<input list="connlist" ')
|
||||
: f === "item" && chk.type === "ready"
|
||||
? `<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])
|
||||
: inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("");
|
||||
if (chk.type === "remote") {
|
||||
const ctpls = state.builders.checks.concat([{ name: "custom", words: "Custom command (typed by hand)", params: [] }]);
|
||||
@@ -451,13 +486,14 @@
|
||||
<details><summary>more</summary>${["tag", "service_only", "debounce_seconds"].filter((f) => !(spec.fields || []).includes(f)).map((f) => inputHtml(base + f, F("tiles[].checks[]." + f), chk[f])).join("")}<p class="mono small">type ${esc(chk.type)}</p></details>
|
||||
<div class="row">${run}<button class="small danger" data-removecheck="${j}">Remove</button></div></div></details>`;
|
||||
}).join("");
|
||||
const plain = !t.members && t.kind !== "master";
|
||||
const members = t.members ? (editing() ? `<div class="field"><span class="fl">group members</span><span>${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => `<label class="inline"><input type="checkbox" data-member="${esc(x.id)}" ${t.members.includes(x.id) ? "checked" : ""}> ${esc(x.label)}</label>`).join(" ")}</span></div>` : `<p class="small"><b>Sums up</b> ${t.members.map((m) => `<a href="#keys/${esc(m)}">${esc((c.tiles.find((x) => x.id === m) || {}).label || m)}</a>`).join(", ")}</p>`) : "";
|
||||
const addMenu = editing() && plain ? `<select id="addcheck" class="small" style="margin-top:8px"><option value="">+ check…</option>${Object.entries(groupBy(state.schema.checks, (s) => s.question || "Other")).map(([q, types]) => `<optgroup label="${esc(q)}">${types.map((ty) => `<option value="${esc(ty)}">${esc(state.schema.checks[ty].words || ty)}</option>`).join("")}</optgroup>`).join("")}</select>` : "";
|
||||
const label = editing() ? inputHtml(`tiles.${i}.label`, F("tiles[].label"), t.label) : "";
|
||||
const holdAlways = editing() && plain ? `<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>` : "";
|
||||
const plain = !t.members && t.kind !== "master" && t.kind !== "ready";
|
||||
const members = t.members && !onReady ? (editing() ? `<div class="field"><span class="fl">group members</span><span>${c.tiles.filter((x) => x.id !== t.id && !x.members && x.kind !== "master").map((x) => `<label class="inline"><input type="checkbox" data-member="${esc(x.id)}" ${t.members.includes(x.id) ? "checked" : ""}> ${esc(x.label)}</label>`).join(" ")}</span></div>` : `<p class="small"><b>Sums up</b> ${t.members.map((m) => `<a href="#keys/${esc(m)}">${esc((c.tiles.find((x) => x.id === m) || {}).label || m)}</a>`).join(", ")}</p>`) : "";
|
||||
const addMenu = editing() && plain ? `<select id="addcheck" class="small" style="margin-top:8px"><option value="">+ check…</option>${Object.entries(groupBy(state.schema.checks, (s) => s.question || "Other")).map(([q, types]) => `<optgroup label="${esc(q)}">${types.filter((ty) => onReady || !READY_ONLY.includes(ty)).map((ty) => `<option value="${esc(ty)}">${esc(state.schema.checks[ty].words || ty)}</option>`).join("")}</optgroup>`).join("")}</select>` : "";
|
||||
const label = editing() && !onReady ? inputHtml(`tiles.${i}.label`, F("tiles[].label"), t.label) : "";
|
||||
const holdAlways = 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>` : "";
|
||||
const holdList = (kt.holding || "").split(/;\s*/).filter(Boolean);
|
||||
const more = `<details><summary>more</summary><dl class="kv small"><dt>id</dt><dd class="mono">${esc(t.id)}</dd>${t.kind ? `<dt>kind</dt><dd>${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}</dd>` : ""}</dl>${editing() && !t.kind ? `<div class="field"><span class="fl">position${hint("row, column; blank = inside its group only")}</span><input id="pos" value="${esc(t.pos ? t.pos.join(", ") : "")}" placeholder="row, column"></div>` : ""}${editing() && (!t.kind || t.kind === "network") ? `<p><button class="small danger" id="delkey">Delete key</button></p>` : ""}</details>`;
|
||||
const more = `<details><summary>more</summary><dl class="kv small"><dt>id</dt><dd class="mono">${esc(t.id)}</dd>${t.kind ? `<dt>kind</dt><dd>${esc({ master: "sums up every key", service: "the ALARMS switch", network: "the network" }[t.kind] || t.kind)}</dd>` : ""}</dl>${editing() && !t.kind && !onReady ? `<div class="field"><span class="fl">position${hint("row, column; blank = inside its group only")}</span><input id="pos" value="${esc(t.pos ? t.pos.join(", ") : "")}" placeholder="row, column"></div>` : ""}${editing() && (!t.kind || t.kind === "network") ? `<p><button class="small danger" id="delkey">Delete key</button></p>` : ""}</details>`;
|
||||
if (onReady) return `<div class="card keycard"><h3 style="margin:4px 0 8px">Edit ${esc(t.label)}</h3>${checks ? `<div class="checks">${checks}</div>` : ""}${addMenu}${more}</div>`;
|
||||
return `<div class="card keycard"><h2 style="margin:4px 0 8px">${esc(t.label)} <span class="muted small">${t.pos ? `row ${t.pos[0]}, col ${t.pos[1]}` : "in a group"}</span></h2>
|
||||
${label}${members}${holdAlways}
|
||||
${holdList.length ? `<details class="fold"><summary>Hold ${esc(hold)}: ${esc(short(holdList[0], 48))}${holdList.length > 1 ? ` <span class="muted">+${holdList.length - 1}</span>` : ""}${t.hold_always ? " <span class='pill'>also when green</span>" : ""}</summary><ol class="small">${holdList.map((h) => `<li>${esc(h)}</li>`).join("")}</ol></details>` : ""}
|
||||
@@ -465,19 +501,20 @@
|
||||
${checks ? `<div class="checks">${checks}</div>` : ""}${addMenu}${more}</div>`;
|
||||
}
|
||||
function groupBy(obj, fn) { const out = {}; for (const [k, v] of Object.entries(obj)) (out[fn(v)] = out[fn(v)] || []).push(k); return out; }
|
||||
function wireKeyPanel(sel, restart) {
|
||||
function wireKeyPanel(sel, restart, list = "tiles") {
|
||||
const c = cfg();
|
||||
const i = restart ? -1 : c.tiles.findIndex((x) => x.id === sel);
|
||||
const i = restart ? -1 : keyList(c, list).findIndex((x) => x.id === sel);
|
||||
const mine = () => keyList(state.draft.config, list)[i];
|
||||
for (const b of main.querySelectorAll("[data-run]")) b.onclick = () => runNow(...b.dataset.run.split("|"));
|
||||
const dk = main.querySelector("#delkey"); if (dk) dk.onclick = () => confirmBox(`Delete the key ${c.tiles[i].label}? Its checks go with it.`, "Delete", () => { if (!ensureDraft()) return; const cc = state.draft.config; const id = cc.tiles[i].id; cc.tiles.splice(i, 1); for (const t of cc.tiles) if (t.members) t.members = t.members.filter((m) => m !== id); saveDraft(); location.hash = "#keys"; });
|
||||
const dk = 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; const cc = state.draft.config; const keys = keyList(cc, list); const id = keys[i].id; keys.splice(i, 1); for (const t of cc.tiles) if (t.members) t.members = t.members.filter((m) => m !== id); saveDraft(); location.hash = list === "tiles" ? "#keys" : "#keys/ready"; });
|
||||
const dr = main.querySelector("#delrkey"); if (dr) dr.onclick = () => confirmBox("Delete this restart key?", "Delete", () => { if (!ensureDraft()) return; const ks = state.draft.config.restart_page.keys; ks.splice(ks.findIndex((x) => x.id === sel), 1); saveDraft(); location.hash = "#keys/restart"; });
|
||||
for (const b of main.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(); });
|
||||
for (const b of main.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 (const cb of main.querySelectorAll("[data-member]")) cb.onchange = () => { if (!ensureDraft()) return; const t = state.draft.config.tiles[i]; const set = new Set(t.members); cb.checked ? set.add(cb.dataset.member) : set.delete(cb.dataset.member); t.members = [...set]; saveDraft(); };
|
||||
const add = main.querySelector("#addcheck"); if (add) add.onchange = () => { if (!add.value || !ensureDraft()) return; const t = state.draft.config.tiles[i]; t.checks = t.checks || []; t.checks.push({ type: add.value }); saveDraft(); route(); };
|
||||
const add = main.querySelector("#addcheck"); if (add) add.onchange = () => { if (!add.value || !ensureDraft()) return; const t = mine(); t.checks = t.checks || []; t.checks.push(add.value === "ready" ? { type: "ready", fix: "ready_recheck" } : { type: add.value }); saveDraft(); route(); };
|
||||
const pos = main.querySelector("#pos"); if (pos) pos.onchange = () => { const t0 = cfg().tiles[i]; const was = t0.pos; delete t0.pos; const p2 = parsePos(pos.value, cfg(), "status", true); if (was) t0.pos = was; if (p2 === false) return; if (!ensureDraft()) return; const t = state.draft.config.tiles[i]; if (p2) t.pos = p2; else delete t.pos; saveDraft(); route(); };
|
||||
const rebuildCheck = async (j) => {
|
||||
if (!ensureDraft()) return;
|
||||
const t = state.draft.config.tiles[i], chk = t.checks[j];
|
||||
const t = mine(), chk = t.checks[j];
|
||||
const tplName = main.querySelector(`select.ctplsel[data-check="${j}"]`).value;
|
||||
if (tplName === "custom") { delete chk.template; delete chk.params; saveDraft(); route(); return; }
|
||||
const ct = state.builders.checks.find((x) => x.name === tplName);
|
||||
@@ -765,6 +802,7 @@
|
||||
const glossary = Object.entries(gl).filter(([g]) => !/while you hold/.test(g)).map(([g, words]) => `<details><summary><b>${esc(g)}</b></summary><p class="mono small">${esc(words.join(" · "))}</p></details>`).join("");
|
||||
main.innerHTML = `<h1>Help</h1><div class="cards">${cards}</div>
|
||||
<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>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>
|
||||
@@ -782,10 +820,12 @@
|
||||
}
|
||||
|
||||
// ---- Settings: one page, every section ------------------------------------------
|
||||
const HAND_MADE = ["tiles", "fixes", "hosts", "gear", "auto_fix.windows", "service_windows", "site.live_services", "auto_fix.fixes", "atem.reference", "spotify.playlist", "spotify.name", "restart_page.keys", "site.editing_code"];
|
||||
const HAND_MADE = ["tiles", "fixes", "hosts", "gear", "auto_fix.windows", "service_windows", "site.live_services", "auto_fix.fixes", "atem.reference", "spotify.playlist", "spotify.name", "restart_page.keys", "site.editing_code",
|
||||
"ready_page.keys", "ready_page.back_pos", "ready.prop_lists"];
|
||||
const SECTIONS = [
|
||||
["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"],
|
||||
["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)"],
|
||||
["hyperdecks", "HyperDecks", "Which decks are in use"],
|
||||
["midi", "MIDI links", "Network MIDI sessions and the peers that must stay in them"],
|
||||
@@ -809,6 +849,7 @@
|
||||
let body = "";
|
||||
if (which === "playlist") body = spotifyHtml(c);
|
||||
else if (which === "autofix") body = autofixHtml(c);
|
||||
else if (which === "ready") body = readySettingsHtml(c);
|
||||
else if (which === "wait") body = `<div class="card">${calendarHtml("site.live_services", (c.site || {}).live_services)}<p class="help">A recording HyperDeck always counts, whatever the clock says.</p></div>`;
|
||||
else if (which === "hyperdecks") body = await hyperdecksHtml(c);
|
||||
else if (which === "midi") body = midiHtml(c);
|
||||
@@ -825,6 +866,14 @@
|
||||
main.querySelector("#subsel").onchange = (e) => { location.hash = "#settings/" + e.target.value; };
|
||||
wireSettings(which);
|
||||
}
|
||||
function readySettingsHtml(c) {
|
||||
if (!c.ready_page) return "<p class='muted'>This campus has no READY page.</p>";
|
||||
const lists = JSON.stringify((c.ready || {}).prop_lists || [], null, 1);
|
||||
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>`
|
||||
+ 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>
|
||||
<p class="help">The keys themselves (add, move, delete, what each checks) are on Keys › READY page.</p></div>`;
|
||||
}
|
||||
function fieldsHtml(screen, includeAdvanced) {
|
||||
const own = Object.entries(state.schema.fields).filter(([p, f]) => f.screen === screen && !p.includes("[]") && !p.includes("*") && !HAND_MADE.includes(p) && !HAND_MADE.some((h) => h.startsWith(p + ".")) && !Object.keys(state.schema.fields).some((k) => k.startsWith(p + ".") || k.startsWith(p + "[]")));
|
||||
const plain = own.filter(([, f]) => !f.advanced), adv = own.filter(([, f]) => f.advanced);
|
||||
@@ -1011,6 +1060,7 @@
|
||||
function wireSettings(which) {
|
||||
const q = (s) => main.querySelector(s);
|
||||
wireCalendars();
|
||||
for (const ta of main.querySelectorAll("[data-jsonpath]")) ta.onchange = () => { try { setPath(ta.dataset.jsonpath, JSON.parse(ta.value)); } catch (e) { alertBox("That is not valid: " + e.message); } };
|
||||
for (const b of main.querySelectorAll("[data-job]")) b.onclick = () => job(b.dataset.job);
|
||||
const ok = q("#okeys"); if (ok) ok.onclick = async () => { ok.disabled = true; ok.textContent = "Testing…"; try { const r = await post("/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(); } };
|
||||
const oa = q("#oadd"); if (oa) oa.onclick = outletDialog;
|
||||
@@ -1214,7 +1264,7 @@
|
||||
detail = `<div class="card"><h3 style="margin-top:0">${esc(f.short || id)} <span class="muted small">${esc(f.label || "")}</span></h3>${inputs}${keys.length ? `<p class="small muted">Used by ${esc(keys.join(", "))}.</p>` : ""}<details><summary>everything stored</summary><dl class="kv">${Object.entries(f).map(([k, v]) => `<dt>${esc(k)}</dt><dd class="mono small">${esc(typeof v === "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 fix</button>` : ""}</div>`;
|
||||
}
|
||||
const add = editing() ? `<span class="row" style="margin-left:auto"><input id="newfix" placeholder="new fix id" style="width:10em;padding:5px 8px;border:1px solid var(--line);border-radius:8px;background:var(--bg);color:var(--fg);font:inherit"><button id="addfix" class="small">+ Fix</button></span>` : "";
|
||||
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>` : ""}<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>
|
||||
<div class="cols"><div class="tablewrap"><table><tr><th>key text</th><th>does</th><th>built as</th><th>used by</th><th>runs</th></tr>${rows}</table></div><div>${detail}</div></div>`;
|
||||
const ks = main.querySelector("select.kindsel"); if (ks) ks.onchange = () => { setPath(ks.dataset.path, ks.value === "another computer" ? "ssh" : ks.value === "this Mac" ? "local" : ks.value); route(); };
|
||||
const rebuild = async () => {
|
||||
|
||||
Reference in New Issue
Block a user