pco-plan: per-service timing, ProPresenter cues, timers for the Timers module
The service clock now belongs to one service at a time: a plan's services take turns by window (the next one takes over pre-roll before its start, once the previous one has ended or was due to end), and moving into the next window resets the service elapsed clock, the item timing and the countdown. A service starts at its scheduled time or, under the new "Service timing" admin group, when a chosen ProPresenter document goes live (scheduled time as the failsafe); it ends when the chosen end document goes live, or at the next service / plan end + 60 min. The pure logic lives in service-timing.js and matching.js (cue documents are matched like plan items, with a stricter 0.75 bar so the pre-service loop never ends a service) with service-timing.test.js covering windows, precedence and failsafes. GET /timers and /timers/stream offer the service, current-item and countdown timers per the module guide; GET /documents lists the documents the Now/Next feed has shown or listed for the new selects. Loopback calls use shell.port, with the old proddash.json guess only when the shell hands no port. Manifest: 1.4.0, proddash >=1.4.0, provides ["timers"]; Client ID wording, shorter labels, group help tightened, field help trimmed. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Fable 5.1
parent
5d55609fbe
commit
48526934bb
+35
-16
@@ -209,36 +209,55 @@ export default function create({ root, moduleApi }) {
|
||||
renderClock();
|
||||
}
|
||||
|
||||
/**
|
||||
* The header clock follows the service the server says owns the clock
|
||||
* (state.service: idle → running → stopped, by schedule or by the admin's
|
||||
* ProPresenter document cues): a countdown before it, the running time
|
||||
* while it runs (with ahead/behind when following ProPresenter), the final
|
||||
* time once it has ended.
|
||||
*/
|
||||
function renderClock() {
|
||||
const plan = state?.plan;
|
||||
const live = state?.live || {};
|
||||
const svc = state?.service || {};
|
||||
clockLabelEl.textContent = '';
|
||||
clockValueEl.textContent = '';
|
||||
clockValueEl.className = 'pp-clock-value';
|
||||
if (!plan || headEl.hidden) return;
|
||||
const now = serverNow();
|
||||
if (on('showRunningClock') && live.following && live.serviceStartedAt) {
|
||||
const startedAt = svc.startedAt || live.serviceStartedAt || 0;
|
||||
if (on('showRunningClock') && svc.phase === 'running' && startedAt) {
|
||||
clockLabelEl.textContent = 'Running';
|
||||
clockValueEl.textContent = fmtDur((now - live.serviceStartedAt) / 1000);
|
||||
// Ahead of / behind the plan: elapsed minus what the completed items were planned to take.
|
||||
const done = state.items.filter((it) => live.history?.[it.id]?.endedAt);
|
||||
const planned = done.reduce((sum, it) => sum + (it.length || 0), 0);
|
||||
const actual = done.reduce((sum, it) => sum + (live.history[it.id].endedAt - live.history[it.id].startedAt) / 1000, 0);
|
||||
const drift = actual - planned;
|
||||
if (planned && Math.abs(drift) >= 30) {
|
||||
clockLabelEl.textContent = `Running · ${drift > 0 ? 'behind' : 'ahead'} ${fmtDur(Math.abs(drift))}`;
|
||||
clockValueEl.classList.add(drift > 0 ? 'is-behind' : 'is-ahead');
|
||||
clockValueEl.textContent = fmtDur((now - startedAt) / 1000);
|
||||
if (live.following) {
|
||||
// Ahead of / behind the plan: elapsed minus what the completed items were planned to take.
|
||||
const done = state.items.filter((it) => live.history?.[it.id]?.endedAt);
|
||||
const planned = done.reduce((sum, it) => sum + (it.length || 0), 0);
|
||||
const actual = done.reduce((sum, it) => sum + (live.history[it.id].endedAt - live.history[it.id].startedAt) / 1000, 0);
|
||||
const drift = actual - planned;
|
||||
if (planned && Math.abs(drift) >= 30) {
|
||||
clockLabelEl.textContent = `Running · ${drift > 0 ? 'behind' : 'ahead'} ${fmtDur(Math.abs(drift))}`;
|
||||
clockValueEl.classList.add(drift > 0 ? 'is-behind' : 'is-ahead');
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (on('showCountdown') && plan.serviceStartsAt && plan.serviceStartsAt > now) {
|
||||
clockLabelEl.textContent = 'Starts in';
|
||||
clockValueEl.textContent = fmtDur((plan.serviceStartsAt - now) / 1000);
|
||||
if (on('showRunningClock') && svc.phase === 'stopped' && startedAt && svc.endedAt) {
|
||||
clockLabelEl.textContent = 'Ended';
|
||||
clockValueEl.textContent = fmtDur((svc.endedAt - startedAt) / 1000);
|
||||
return;
|
||||
}
|
||||
if (on('showRunningClock') && plan.serviceStartsAt) {
|
||||
clockLabelEl.textContent = 'Since start';
|
||||
clockValueEl.textContent = fmtDur((now - plan.serviceStartsAt) / 1000);
|
||||
const startsAt = svc.startsAt || plan.serviceStartsAt || 0;
|
||||
if (on('showCountdown') && startsAt > now) {
|
||||
clockLabelEl.textContent = 'Starts in';
|
||||
clockValueEl.textContent = fmtDur((startsAt - now) / 1000);
|
||||
return;
|
||||
}
|
||||
if (on('showRunningClock') && startsAt) {
|
||||
// Past the scheduled start and not started: waiting for the admin's
|
||||
// start document to go live in ProPresenter (or its failsafe).
|
||||
clockLabelEl.textContent = 'Scheduled';
|
||||
clockValueEl.textContent = fmtClock(startsAt);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user