From efd19353e9ed15591b80d916b2914554f047235c Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 7 Mar 2026 01:28:02 +0100 Subject: [PATCH] fix: roll resets day --- .../src/stores/__tests__/runtimeState.test.ts | 186 ++++++++++++++++++ apps/server/src/stores/runtimeState.ts | 7 + 2 files changed, 193 insertions(+) diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index e7c82a78a..c4acd1053 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -1,4 +1,5 @@ import { 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'; import { initRundown } from '../../api-data/rundown/rundown.service.js'; @@ -394,6 +395,191 @@ describe('roll mode', () => { expect(getState().rundown.currentDay).toBe(1); }); + + test('pending roll crossing midnight keeps currentDay null', async () => { + // Event starts at 01:00 (future event, will be pending) + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 1 * MILLIS_PER_HOUR, // 01:00 + timeEnd: 2 * MILLIS_PER_HOUR, // 02:00 + duration: 1 * MILLIS_PER_HOUR, // 1 hour + dayOffset: 0, + }, + }, + order: ['1'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + // Clock is at 23:50 (before midnight, event is pending for tomorrow 01:00) + vi.setSystemTime('jan 1 23:50'); + const { rundown, metadata } = rundownCache.get(); + const result = roll(rundown, metadata); + + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(false); + + const stateBeforeMidnight = getState(); + expect(stateBeforeMidnight.timer.secondaryTimer).not.toBeNull(); + // currentDay is null when pending (rundown hasn't started) + expect(stateBeforeMidnight.rundown.currentDay).toBeNull(); + + // Cross midnight while still pending + vi.setSystemTime('jan 2 00:10'); + update(); + + const stateAfterMidnight = getState(); + // currentDay stays null while pending (rundown hasn't started yet) + expect(stateAfterMidnight.rundown.currentDay).toBeNull(); + // should still be pending (event starts at 01:00) + expect(stateAfterMidnight.timer.secondaryTimer).not.toBeNull(); + }); + + test('pending roll sets currentDay to 0 when event starts (rundown starts fresh)', async () => { + // Event starts at 01:00 + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 1 * MILLIS_PER_HOUR, // 01:00 + timeEnd: 2 * MILLIS_PER_HOUR, // 02:00 + duration: 1 * MILLIS_PER_HOUR, // 1 hour + dayOffset: 0, + }, + }, + order: ['1'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + // Start pending at 23:50 + vi.setSystemTime('jan 1 23:50'); + const { rundown, metadata } = rundownCache.get(); + roll(rundown, metadata); + + // Cross midnight + vi.setSystemTime('jan 2 00:10'); + update(); + + // Now the event starts at 01:05 - call roll again to simulate runtime service + vi.setSystemTime('jan 2 01:05'); + const result = roll(rundown, metadata); + + expect(result.didStart).toBe(true); + + const state = getState(); + // currentDay is 0 because the rundown just started (this is day 0) + // the pending time before midnight doesn't count as rundown time + expect(state.rundown.currentDay).toBe(0); + }); + + test('pending roll crossing midnight updates secondaryTimer correctly', async () => { + // Event starts at 01:00 + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 1 * MILLIS_PER_HOUR, // 01:00 + timeEnd: 2 * MILLIS_PER_HOUR, // 02:00 + duration: 1 * MILLIS_PER_HOUR, // 1 hour + dayOffset: 0, + }, + }, + order: ['1'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + // Clock is at 23:50 + vi.setSystemTime('jan 1 23:50'); + const { rundown, metadata } = rundownCache.get(); + roll(rundown, metadata); + + const stateBeforeMidnight = getState(); + // secondaryTimer should be time until 01:00 (1h 10min = 70 min) + expect(stateBeforeMidnight.timer.secondaryTimer).toBe(70 * MILLIS_PER_MINUTE); + + // Cross midnight to 00:10 + vi.setSystemTime('jan 2 00:10'); + update(); + + const stateAfterMidnight = getState(); + // secondaryTimer should now be 50 minutes until 01:00 + expect(stateAfterMidnight.timer.secondaryTimer).toBe(50 * MILLIS_PER_MINUTE); + }); + + test('rundown loops back after finishing and resets currentDay when first event starts again', async () => { + // Rundown: 09:00-23:00, then overnight event 23:00-01:00 + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 9 * MILLIS_PER_HOUR, // 09:00 + timeEnd: 23 * MILLIS_PER_HOUR, // 23:00 + duration: 14 * MILLIS_PER_HOUR, // 14 hours + dayOffset: 0, + }, + 2: { + ...mockEvent, + id: '2', + timeStart: 23 * MILLIS_PER_HOUR, // 23:00 + timeEnd: 1 * MILLIS_PER_HOUR, // 01:00 (next day) + duration: 2 * MILLIS_PER_HOUR, // 2 hours + dayOffset: 0, + }, + }, + order: ['1', '2'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + const { rundown, metadata } = rundownCache.get(); + + // Day 1: Start at 09:05 - first event is playing + vi.setSystemTime('jan 1 09:05'); + let result = roll(rundown, metadata); + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(true); + expect(getState().rundown.currentDay).toBe(0); + + // Day 1: Move to 23:05 - second event (overnight) is playing + vi.setSystemTime('jan 1 23:05'); + result = roll(rundown, metadata); + expect(result.eventId).toBe('2'); + expect(result.didStart).toBe(true); + + // Cross midnight + vi.setSystemTime('jan 2 00:30'); + update(); + expect(getState().rundown.currentDay).toBe(1); + + // Day 2: 01:05 - rundown finishes, loops back to first event (pending for 09:00) + vi.setSystemTime('jan 2 01:05'); + result = roll(rundown, metadata); + // First event is pending (09:00 is in the future) + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(false); + // currentDay is null while pending (rundown hasn't "restarted" yet) + expect(getState().rundown.currentDay).toBeNull(); + + // Day 2: 09:05 - first event starts again (fresh rundown cycle) + vi.setSystemTime('jan 2 09:05'); + result = roll(rundown, metadata); + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(true); + // currentDay resets to 0 for the new cycle + expect(getState().rundown.currentDay).toBe(0); + }); }); describe('normal roll', async () => { diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index 2349ea3c1..a925ca866 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -751,6 +751,13 @@ export function roll( // preload timer properties runtimeState.timer.duration = calculateDuration(runtimeState.eventNow.timeStart, runtimeState.eventNow.timeEnd); runtimeState.timer.current = runtimeState.timer.duration; + + // reset rundown state - the rundown will start fresh when the event begins + runtimeState.rundown.actualStart = null; + runtimeState.rundown.currentDay = null; + runtimeState._startDayOffset = null; + runtimeState._startEpoch = null; + return { eventId: runtimeState.eventNow.id, didStart: false }; }