READY page: the weekly worship checklist, checked against Planning Center
- readiness.py looks every 2 minutes (never while a HyperDeck records) at this weekend's PCO plan and what is in place for it: the Ableton set (songs, mix, routing, lighting cues), ProPresenter (every PCO item, MC announcements, baptism names, timelines, backgrounds), the LTC note of every song against the timecode the tracks play, the Band Lineup TV, Resolume's LED cross, and the tracks and ProP bundles for the locations. - READY key on the Status page (row 2, col 7; SPOTIFY now shows inside SHOW) opens page 24. Amber until done, blinking red from Saturday noon; not part of ALL. Tap keys for BOH mix, mics and stage reset every Monday. - Holds fix what can be fixed: re-sync the lineup, put the cross back up, write LTC into PCO, copy tracks to locations, look again. Hold READY fixes everything. - Settings page: a READY page tab (live keys, drag to move, why each key is amber), and Apply re-imports page 24. - Auto-fix: "while_muted" fixes keep running with ALARMS muted (Spotify playlist). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+55
-12
@@ -262,6 +262,7 @@
|
||||
function canRunNow(fixId) {
|
||||
const f = cfg().fixes[fixId]; if (!f) return false;
|
||||
if (["reconnect", "hyperdeck", "midi_connect", "spotify"].includes(f.kind)) return true;
|
||||
if (f.kind === "ready") return !f.never_auto; // the READY page's fixes (not the tracks copy)
|
||||
if ((f.kind || "ssh") === "ssh" || f.kind === "local") return !closes(f) && !f.never_auto;
|
||||
return false;
|
||||
}
|
||||
@@ -274,21 +275,24 @@
|
||||
|
||||
function deckHtml(deck, c, which, selected, draggable) {
|
||||
const cells = {};
|
||||
const rp = c.restart_page;
|
||||
const rp = c.restart_page, dp = c.ready_page;
|
||||
if (which === "restart" && rp) { for (const k of rp.keys) cells[k.pos] = { id: k.id, label: k.label }; cells[rp.back_pos] = { nav: "STATUS\nPAGE" }; }
|
||||
else if (which === "ready" && dp) { for (const k of dp.keys) cells[k.pos] = { id: k.id, label: k.label }; cells[dp.back_pos] = { nav: "STATUS\nPAGE", back: "dback:" }; }
|
||||
else { for (const t of c.tiles) if (t.pos) cells[t.pos] = { id: t.id, label: t.label }; if (rp) cells[rp.jump_pos] = { nav: "RESTART\nPAGE" }; }
|
||||
const nav = { "0,0": "UP", "1,0": String(which === "restart" && rp ? rp.page : c.page), "2,0": "DOWN" };
|
||||
const nav = { "0,0": "UP", "1,0": String(which === "restart" && rp ? rp.page : which === "ready" && dp ? dp.page : c.page), "2,0": "DOWN" };
|
||||
const prefix = { restart: "rkey:", ready: "dkey:" }[which] || "tile:";
|
||||
const route_ = { restart: "restart/", ready: "ready/" }[which] || "";
|
||||
const drag = (what) => editing() && draggable ? ` draggable="true" data-drag="${esc(what)}"` : "";
|
||||
let out = `<div class="deck">`;
|
||||
for (let r = 0; r < 4; r++) for (let col = 0; col < 8; col++) {
|
||||
const k = `${r},${col}`, cell = cells[k];
|
||||
if (nav[k]) out += `<div class="key nav locked">${esc(nav[k])}</div>`;
|
||||
else if (cell && cell.nav) out += `<div class="key nav"${drag(cell.nav.startsWith("RESTART") ? "jump:" : "back:")}>${esc(cell.nav)}</div>`;
|
||||
else if (cell && cell.nav) out += `<div class="key nav"${drag(cell.back || (cell.nav.startsWith("RESTART") ? "jump:" : "back:"))}>${esc(cell.nav)}</div>`;
|
||||
else if (cell) {
|
||||
const s = deck && deck[cell.id];
|
||||
const style = s ? `--b1:${hex(s.b)};--f1:${hex(s.f)};--b2:${hex(s.b2)};--f2:${hex(s.f2)};background:${hex(s.b)};color:${hex(s.f)}` : "background:#444;color:#ddd";
|
||||
const blink = s && s.b !== s.b2 ? " blink" : "";
|
||||
out += `<div class="key${blink}${selected === cell.id ? " sel" : ""}" style="${style}"${drag((which === "restart" ? "rkey:" : "tile:") + cell.id)} onclick="location.hash='#keys/${which === "restart" ? "restart/" : ""}${esc(cell.id)}'">${esc(s ? s.x : cell.label)}</div>`;
|
||||
out += `<div class="key${blink}${selected === cell.id ? " sel" : ""}" style="${style}"${drag(prefix + cell.id)} onclick="location.hash='#keys/${route_}${esc(cell.id)}'">${esc(s ? s.x : cell.label)}</div>`;
|
||||
} else out += `<div class="key empty"${editing() && draggable ? ` data-drop="${r},${col}"` : ""}></div>`;
|
||||
}
|
||||
return out + `</div>`;
|
||||
@@ -300,29 +304,32 @@
|
||||
if (rest[0] === "fixes") return fixes(rest.slice(1));
|
||||
const o = await api("/api/overview").catch(() => ({ deck: null }));
|
||||
const c = cfg();
|
||||
const ready = rest[0] === "ready";
|
||||
const restart = rest[0] === "restart";
|
||||
const sel = restart ? rest[1] : rest[0];
|
||||
const sel = restart || ready ? rest[1] : rest[0];
|
||||
const hold = ((c.page_hold_ms || 2000) / 1000) + " s";
|
||||
const tabs = `<div class="tabs"><button class="${restart ? "" : "on"}" onclick="location.hash='#keys'">Status page</button>${c.restart_page ? `<button class="${restart ? "on" : ""}" onclick="location.hash='#keys/restart'">Restart page</button>` : ""}<button onclick="location.hash='#keys/fixes'">Fixes</button>
|
||||
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 (sel && !restart) { const i = c.tiles.findIndex((x) => x.id === sel); if (i >= 0) panel = await keyPanel(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); }
|
||||
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").map((p) => p[0].toUpperCase() + p.slice(1)).join(" and ")} page.</div>`; } catch (e) {} }
|
||||
const adders = editing() ? (restart ? `<button id="newrkey" class="small">+ Restart key</button>` : `<button id="newkey" class="small">+ Key</button>`) : "";
|
||||
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>`) : "";
|
||||
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>" : deckHtml(o.deck, c, restart ? "restart" : "status", sel, true)}
|
||||
${!restart ? `<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="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);
|
||||
wireKeyPanel(sel, restart || ready);
|
||||
wireLayout(restart);
|
||||
}
|
||||
function takenCells(c, which) {
|
||||
const taken = new Set(["0,0", "1,0", "2,0"]);
|
||||
if (which === "restart") { for (const k of c.restart_page.keys) taken.add(String(k.pos)); taken.add(String(c.restart_page.back_pos)); }
|
||||
else if (which === "ready") { for (const k of c.ready_page.keys) taken.add(String(k.pos)); taken.add(String(c.ready_page.back_pos)); }
|
||||
else { for (const t of c.tiles) if (t.pos) taken.add(String(t.pos)); if (c.restart_page) taken.add(String(c.restart_page.jump_pos)); }
|
||||
return taken;
|
||||
}
|
||||
@@ -347,6 +354,8 @@
|
||||
else if (kind === "rkey") { const k = c.restart_page.keys.find((x) => x.id === id); if (k && pos) k.pos = pos; }
|
||||
else if (kind === "jump" && pos) c.restart_page.jump_pos = pos;
|
||||
else if (kind === "back" && pos) c.restart_page.back_pos = pos;
|
||||
else if (kind === "dkey") { const k = c.ready_page.keys.find((x) => x.id === id); if (k && pos) k.pos = pos; }
|
||||
else if (kind === "dback" && pos) c.ready_page.back_pos = pos;
|
||||
saveDraft(); route();
|
||||
};
|
||||
let dragging = null;
|
||||
@@ -479,6 +488,40 @@
|
||||
for (const s of main.querySelectorAll("select.holdsel")) s.onchange = () => { setPath(s.dataset.path, s.value || undefined); route(); };
|
||||
for (const s of main.querySelectorAll("select.watchsel")) s.onchange = () => setPath(s.dataset.path, [...s.selectedOptions].map((o) => o.value));
|
||||
}
|
||||
// ---- The READY page: what each checklist key looks at, and why it is amber or red right now ----
|
||||
const READY_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.",
|
||||
led_cross: "Resolume is on the Service deck and a clip is playing on the cross.",
|
||||
lineup: "The Band Lineup TV shows this plan, nobody who is not on it or declined, and everyone on the band and communication teams.",
|
||||
track_bundle: "Every location has this weekend's Ableton project folder on the shared drive.",
|
||||
prop_bundle: "A ProPresenter bundle was saved for the locations since Monday.",
|
||||
};
|
||||
async function readyKeyPanel(i, hold) {
|
||||
const c = cfg();
|
||||
const snap = await api("/api/snapshot").catch(() => ({}));
|
||||
const r = snap.ready || {};
|
||||
const plan = r.plan ? `<p class="small muted">Plan: ${esc(r.plan.title || "")} ${esc(r.plan.dates || "")}${r.time ? ` · looked ${esc(new Date(r.time * 1000).toLocaleTimeString())}` : ""}</p>` : `<p class="small muted">Not looked yet.</p>`;
|
||||
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(", "))}). Tap keys (BOH MIX, MICS, STAGE) are 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>`;
|
||||
const k = c.ready_page.keys[i], chk = (k.checks || [])[0] || {};
|
||||
const item = r.items && chk.item ? r.items[chk.item] : null;
|
||||
const fix = chk.fix && c.fixes[chk.fix];
|
||||
let what;
|
||||
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.`;
|
||||
else if (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")}.`;
|
||||
else if (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")}.`;
|
||||
else what = `Watches ${esc((k.checks || []).map((x) => x.tag || x.msg || x.type).join(", "))}. <b>Hold ${esc(hold)}</b>: fix the one that dropped.`;
|
||||
const faults = item && item.faults && item.faults.length
|
||||
? `<ul class="small">${item.details.map((d) => `<li>${esc(d)}</li>`).join("")}</ul>`
|
||||
: item ? `<p class="small">Nothing to do${item.info ? ` (${esc(item.info)})` : ""}.</p>` : "";
|
||||
const edit = editing() ? inputHtml(`ready_page.keys.${i}.label`, F("tiles[].label"), k.label) : "";
|
||||
return `<div class="card"><h2 style="margin-top:4px">${esc(k.label)} <span class="muted small">row ${k.pos[0]}, col ${k.pos[1]}</span></h2>${plan}<p class="small">${what}</p>${faults}${edit}
|
||||
${chk.fix && canRunNow(chk.fix) && editing() ? `<button class="small" data-run="${esc(k.id)}|${esc(chk.fix)}|0">Run the fix</button>` : ""}<details><summary>more</summary><dl class="kv"><dt>id</dt><dd class="mono">${esc(k.id)}</dd>${chk.item ? `<dt>checklist line</dt><dd class="mono">${esc(chk.item)}</dd>` : ""}</dl></details></div>`;
|
||||
}
|
||||
function restartKeyPanel(i, hold) {
|
||||
const c = cfg(), r = c.restart_page.keys[i];
|
||||
const f = c.fixes[r.fix] || {};
|
||||
|
||||
Reference in New Issue
Block a user