From a29a493678235e4d9493af39f68b5500c5863517 Mon Sep 17 00:00:00 2001 From: Carlos Valente Date: Sat, 7 Mar 2026 00:10:19 +0100 Subject: [PATCH] fix: add current day when ticking over midnight --- .../src/stores/__tests__/runtimeState.test.ts | 105 +++++++++++++++++- apps/server/src/stores/runtimeState.ts | 31 ++++-- 2 files changed, 123 insertions(+), 13 deletions(-) diff --git a/apps/server/src/stores/__tests__/runtimeState.test.ts b/apps/server/src/stores/__tests__/runtimeState.test.ts index 4227ca6a5..e7c82a78a 100644 --- a/apps/server/src/stores/__tests__/runtimeState.test.ts +++ b/apps/server/src/stores/__tests__/runtimeState.test.ts @@ -256,8 +256,8 @@ describe('mutation on runtimeState', () => { order: ['event1'], }); - const startEpoch = new Date('2024-01-01T00:00:00Z').getTime(); - vi.setSystemTime(new Date('2024-01-03T00:00:00Z')); + const startEpoch = new Date('jan 1 00:00').getTime() as Instant; + vi.setSystemTime('jan 3 23:59:59'); await initRundown(mockRundown, {}); vi.runAllTimers(); @@ -280,7 +280,8 @@ describe('mutation on runtimeState', () => { expect(newState.rundown.actualStart).toBe(60 * 1000); expect(newState.rundown.currentDay).toBe(2); - vi.setSystemTime(new Date('2024-01-04T00:00:00Z')); + // crossing midnight increments currentDay + vi.setSystemTime('jan 4 00:00:01'); update(); expect(getState().rundown.currentDay).toBe(3); }); @@ -297,6 +298,104 @@ describe('roll mode', () => { vi.useRealTimers(); }); + describe('overnight event sets correct currentDay', () => { + test('roll into overnight event after midnight sets currentDay correctly', async () => { + // Event spans 23:30 to 00:30 (overnight) + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 23 * 60 * 60 * 1000 + 30 * 60 * 1000, // 23:30 + timeEnd: 30 * 60 * 1000, // 00:30 + duration: 60 * 60 * 1000, // 1 hour + dayOffset: 0, + }, + }, + order: ['1'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + // Clock is at 00:10 (morning part of overnight event) + vi.setSystemTime('jan 1 00:10'); + const { rundown, metadata } = rundownCache.get(); + const result = roll(rundown, metadata); + + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(true); + + const state = getState(); + // currentDay should be 1 because we're in the "next day" part of the overnight event + expect(state.rundown.currentDay).toBe(1); + }); + + test('roll into overnight event before midnight sets currentDay correctly', async () => { + // Event spans 23:30 to 00:30 (overnight) + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 23 * 60 * 60 * 1000 + 30 * 60 * 1000, // 23:30 + timeEnd: 30 * 60 * 1000, // 00:30 + duration: 60 * 60 * 1000, // 1 hour + dayOffset: 0, + }, + }, + order: ['1'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + // Clock is at 23:40 (evening part of overnight event) + vi.setSystemTime('jan 1 23:40'); + const { rundown, metadata } = rundownCache.get(); + const result = roll(rundown, metadata); + + expect(result.eventId).toBe('1'); + expect(result.didStart).toBe(true); + + const state = getState(); + // currentDay should be 0 because we're still on the same day as the event start + expect(state.rundown.currentDay).toBe(0); + }); + + test('currentDay increments when crossing midnight during roll', async () => { + const mockRundown = makeRundown({ + entries: { + 1: { + ...mockEvent, + id: '1', + timeStart: 23 * 60 * 60 * 1000 + 30 * 60 * 1000, // 23:30 + timeEnd: 30 * 60 * 1000, // 00:30 + duration: 60 * 60 * 1000, // 1 hour + dayOffset: 0, + }, + }, + order: ['1'], + }); + + await initRundown(mockRundown, {}); + vi.runAllTimers(); + + // Start at 23:40 + vi.setSystemTime('jan 1 23:40'); + const { rundown, metadata } = rundownCache.get(); + roll(rundown, metadata); + + expect(getState().rundown.currentDay).toBe(0); + + // Cross midnight + vi.setSystemTime('jan 2 00:10'); + update(); + + expect(getState().rundown.currentDay).toBe(1); + }); + }); + describe('normal roll', async () => { beforeEach(async () => { vi.useFakeTimers(); diff --git a/apps/server/src/stores/runtimeState.ts b/apps/server/src/stores/runtimeState.ts index f1a3083e8..2349ea3c1 100644 --- a/apps/server/src/stores/runtimeState.ts +++ b/apps/server/src/stores/runtimeState.ts @@ -40,7 +40,11 @@ import { RundownMetadata } from '../api-data/rundown/rundown.types.js'; import { getPlayableIndexFromTimedIndex } from '../api-data/rundown/rundown.utils.js'; import * as timeCore from '../lib/time-core/timeCore.js'; -type ExpectedMetadata = { event: OntimeEvent; accumulatedGap: number; isLinkedToLoaded: boolean } | null; +type ExpectedMetadata = { + event: OntimeEvent; + accumulatedGap: number; + isLinkedToLoaded: boolean; +} | null; export type RuntimeState = { clock: TimeOfDay; @@ -541,15 +545,10 @@ export function update(): UpdateResult { return updateIfIdle(); } - // if we are playing and playback changes. we tick the current runtime day - if (runtimeState._startDayOffset !== null && runtimeState._startEpoch) { - runtimeState.rundown.currentDay = - runtimeState._startDayOffset + Math.floor((epoch - runtimeState._startEpoch) / dayInMs); - } + const hasCrossedMidnight = previousClock > now; // 2. are we waiting to roll? if (runtimeState.timer.playback === Playback.Roll && runtimeState.timer.secondaryTimer !== null) { - const hasCrossedMidnight = previousClock > runtimeState.clock; return updateIfWaitingToRoll(hasCrossedMidnight); } @@ -557,6 +556,11 @@ 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) { @@ -616,7 +620,10 @@ export function update(): UpdateResult { } runtimeState.timer.secondaryTimer = runtimeState._timer.secondaryTarget! - offsetClock; - return { hasTimerFinished: false, hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0 }; + return { + hasTimerFinished: false, + hasSecondaryTimerFinished: runtimeState.timer.secondaryTimer <= 0, + }; } } @@ -683,7 +690,9 @@ export function roll( if (runtimeState.rundown.actualStart === null) { runtimeState.rundown.actualStart = plannedStart; - runtimeState._startDayOffset = 0; + runtimeState._startDayOffset = + findDayOffset(runtimeState.eventNow.timeStart, runtimeState.clock) + runtimeState.eventNow.dayOffset; + runtimeState.rundown.currentDay = runtimeState._startDayOffset; runtimeState._startEpoch = epoch; } } else { @@ -773,7 +782,9 @@ export function roll( } // update metadata - runtimeState._startDayOffset = 0; + runtimeState._startDayOffset = + findDayOffset(runtimeState.eventNow.timeStart, runtimeState.clock) + runtimeState.eventNow.dayOffset; + runtimeState.rundown.currentDay = runtimeState._startDayOffset; runtimeState._startEpoch = epoch; return { eventId: runtimeState.eventNow.id, didStart: true };