From f1a1c358bce35625011faa36b66c2fd385cc7650 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 7 Mar 2026 01:31:39 +0100 Subject: [PATCH] fix: infer day offset from epoch --- .../src/stores/__tests__/runtimeState.test.ts | 61 ++++++++++++++++++- apps/server/src/stores/runtimeState.ts | 12 ++-- 2 files changed, 66 insertions(+), 7 deletions(-) diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index c4acd1053..4c55c8e7c 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -1,4 +1,4 @@ -import { PlayableEvent, Playback, TimerPhase } from 'ontime-types'; +import { Instant, PlayableEvent, Playback, TimerPhase } from 'ontime-types'; import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE } from 'ontime-utils'; import { makeOntimeGroup, makeOntimeEvent, makeRundown } from '../../api-data/rundown/__mocks__/rundown.mocks.js'; @@ -439,6 +439,65 @@ describe('roll mode', () => { expect(stateAfterMidnight.timer.secondaryTimer).not.toBeNull(); }); + test('waiting between events crossing midnight increments currentDay', async () => { + // Event 1: 23:00-23:30, Event 2: 00:30-01:00 (gap over midnight) + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 23 * MILLIS_PER_HOUR, // 23:00 + timeEnd: 23 * MILLIS_PER_HOUR + 30 * MILLIS_PER_MINUTE, // 23:30 + duration: 30 * MILLIS_PER_MINUTE, + dayOffset: 0, + }, + 2: { + ...mockEvent, + id: '2', + timeStart: 30 * MILLIS_PER_MINUTE, // 00:30 + timeEnd: 1 * MILLIS_PER_HOUR, // 01:00 + duration: 30 * MILLIS_PER_MINUTE, + dayOffset: 0, + }, + }, + order: ['1', '2'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + const { rundown, metadata } = rundownCache.get(); + + // Start event 1 at 23:05 + vi.setSystemTime('jan 1 23:05'); + const startEpoch = Date.now() as Instant; + let result = roll(rundown, metadata); + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(true); + expect(getState().rundown.currentDay).toBe(0); + + // Simulate event 1 finishing and loading event 2 (what runtime service does) + // Load event 2 and call roll to put it in pending state + vi.setSystemTime('jan 1 23:35'); + load(rundown.entries['2'] as PlayableEvent, rundown, metadata, { + firstStart: getState().rundown.actualStart, + startEpoch, + }); + result = roll(rundown, metadata); + expect(result.eventId).toBe('2'); + expect(result.didStart).toBe(false); // pending for 00:30 + expect(getState().timer.secondaryTimer).not.toBeNull(); + expect(getState().rundown.currentDay).toBe(0); + + // Cross midnight while waiting for event 2 + vi.setSystemTime('jan 2 00:10'); + update(); + + // currentDay should increment even while waiting between events + expect(getState().rundown.currentDay).toBe(1); + expect(getState().timer.secondaryTimer).not.toBeNull(); // still waiting + }); + test('pending roll sets currentDay to 0 when event starts (rundown starts fresh)', async () => { // Event starts at 01:00 const mockRundown = makeRundown({ diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index a925ca866..a7c77fdb1 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -545,10 +545,15 @@ export function update(): UpdateResult { return updateIfIdle(); } - const hasCrossedMidnight = previousClock > now; + // calculate currentDay from epoch (days elapsed since playback was started) + if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) { + const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, epoch); + runtimeState.rundown.currentDay = runtimeState._startDayOffset + daysSinceStart; + } // 2. are we waiting to roll? if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) { + const hasCrossedMidnight = previousClock > now; return updateIfWaitingToRoll(hasCrossedMidnight); } @@ -556,11 +561,6 @@ export function update(): UpdateResult { // reset data runtimeState.timer.secondaryTimer = null; - // increment day if we crossed midnight - if (hasCrossedMidnight && runtimeState.rundown.currentDay !== null) { - runtimeState.rundown.currentDay++; - } - // eslint-disable-next-line no-unused-labels -- dev code path DEV: { if (runtimeState.timer.duration === null) {