From d97d9db2ab988ae7dbfacd4d4c326f99ccc040a2 Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 05:45:27 +0000 Subject: [PATCH 1/2] Fix rundown view crash when tTimers or studio settings are missing Playlists created before the T-timers feature may not have a tTimers field in MongoDB, causing RundownHeaderTimers to crash when calling .filter() on undefined. Similarly, studios that have not run recent migrations may be missing packageContainerSettingsWithOverrides and other ObjectWithOverrides fields, causing server publications to throw when reading .defaults. - Add getRundownTTimers() helper with DEFAULT_RUNDOWN_T_TIMERS fallback - Guard RundownHeaderTimers and getDefaultTTimer against undefined input - Make applyAndValidateOverrides tolerate undefined/null/invalid objects --- .../src/dataModel/RundownPlaylist/TTimers.ts | 12 ++++++++++++ .../__tests__/objectWithOverrides.spec.ts | 10 ++++++++++ .../src/settings/objectWithOverrides.ts | 19 +++++++++++++++---- packages/webui/src/client/lib/tTimerUtils.ts | 7 +++++-- .../RundownHeader/RundownHeaderTimers.tsx | 9 ++++++--- 5 files changed, 48 insertions(+), 9 deletions(-) diff --git a/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts b/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts index ef974b741a5..ed05a3a47a1 100644 --- a/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts +++ b/packages/corelib/src/dataModel/RundownPlaylist/TTimers.ts @@ -164,3 +164,15 @@ export interface RundownTTimer { * display: { ... } // some kind of options for how to display in the ui */ } + +export const DEFAULT_RUNDOWN_T_TIMERS: [RundownTTimer, RundownTTimer, RundownTTimer] = [ + { index: 1, label: '', mode: null, state: null }, + { index: 2, label: '', mode: null, state: null }, + { index: 3, label: '', mode: null, state: null }, +] + +export function getRundownTTimers( + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] | null | undefined +): [RundownTTimer, RundownTTimer, RundownTTimer] { + return tTimers ?? DEFAULT_RUNDOWN_T_TIMERS +} diff --git a/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts b/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts index 55699dbda25..06b9585256c 100644 --- a/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts +++ b/packages/corelib/src/settings/__tests__/objectWithOverrides.spec.ts @@ -22,6 +22,16 @@ interface BasicType { } describe('applyAndValidateOverrides', () => { + test('undefined input', () => { + const res = applyAndValidateOverrides(undefined) + expect(res).toBeTruthy() + + expect(res.obj).toStrictEqual({}) + expect(res.invalid).toHaveLength(0) + expect(res.preserve).toHaveLength(0) + expect(res.unused).toHaveLength(0) + }) + test('no overrides', () => { const inputObj = { abc: 'def', diff --git a/packages/corelib/src/settings/objectWithOverrides.ts b/packages/corelib/src/settings/objectWithOverrides.ts index 59a015d7164..d22dbfdf7e2 100644 --- a/packages/corelib/src/settings/objectWithOverrides.ts +++ b/packages/corelib/src/settings/objectWithOverrides.ts @@ -52,9 +52,11 @@ export function wrapDefaultObject(obj: T): ObjectWithOverrides overrides: [], } } -export function isObjectWithOverrides(o: ObjectWithOverrides | T): o is ObjectWithOverrides { +export function isObjectWithOverrides( + o: ObjectWithOverrides | T | ReadonlyDeep> | undefined | null +): o is ObjectWithOverrides { const oAny = o as any - return typeof oAny.defaults === 'object' && Array.isArray(oAny.overrides) + return oAny != null && typeof oAny.defaults === 'object' && oAny.defaults != null && Array.isArray(oAny.overrides) } /** * In some cases, an ObjectWithOverrides should have no defaults. This is common for when the user owns the object containing the ObjectWithOverrides. @@ -194,10 +196,19 @@ function recursivelyGenerateOverrides( * Note: No validation is done to make sure the type conforms to the typescript definition. It is assumed that the definitions which drive ui ensure that they dont violate the typings, and that any changes will be backwards compatible with old overrides */ export function applyAndValidateOverrides( - obj: ReadonlyDeep> + obj: ReadonlyDeep> | undefined | null ): ApplyOverridesResult { + if (obj == null || !isObjectWithOverrides(obj)) { + return { + obj: {} as T, + preserve: [], + unused: [], + invalid: [], + } + } + const result: ApplyOverridesResult = { - obj: clone(obj.defaults), + obj: clone(obj.defaults) as T, preserve: [], unused: [], invalid: [], diff --git a/packages/webui/src/client/lib/tTimerUtils.ts b/packages/webui/src/client/lib/tTimerUtils.ts index dd81678dbec..a2d3f211162 100644 --- a/packages/webui/src/client/lib/tTimerUtils.ts +++ b/packages/webui/src/client/lib/tTimerUtils.ts @@ -1,5 +1,6 @@ import { type RundownTTimer, + getRundownTTimers, timerStateToDuration, } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' @@ -49,6 +50,8 @@ export function calculateTTimerOverUnder(timer: RundownTTimer, now: number): num return projectedDuration - duration } -export function getDefaultTTimer(tTimers: [RundownTTimer, RundownTTimer, RundownTTimer]): RundownTTimer | undefined { - return tTimers.find((t) => t.mode) +export function getDefaultTTimer( + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] | null | undefined +): RundownTTimer | undefined { + return getRundownTTimers(tTimers).find((t) => t.mode) } diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 176eb08a251..5c78b37897b 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -1,4 +1,7 @@ -import type { RundownTTimer } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' +import { + type RundownTTimer, + getRundownTTimers, +} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { useTiming } from '../RundownTiming/withTiming' import { RundownUtils } from '../../../lib/rundown.js' import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../../lib/tTimerUtils' @@ -8,13 +11,13 @@ import { Countdown } from './Countdown' import { OverUnderChip } from '../../../lib/Components/OverUnderChip' interface IProps { - tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] + tTimers: [RundownTTimer, RundownTTimer, RundownTTimer] | null | undefined } export const RundownHeaderTimers: React.FC = ({ tTimers }) => { useTiming() - const activeTimers = tTimers.filter((t) => t.mode).slice(0, 2) + const activeTimers = getRundownTTimers(tTimers).filter((t) => t.mode).slice(0, 2) if (activeTimers.length == 0) return null return ( From 7cfcb620c3ae0426291eacca3e5a6f2eccb6c7ce Mon Sep 17 00:00:00 2001 From: Cursor Agent Date: Wed, 17 Jun 2026 07:21:14 +0000 Subject: [PATCH 2/2] Fix Prettier formatting in RundownHeaderTimers --- .../ui/RundownView/RundownHeader/RundownHeaderTimers.tsx | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx index 5c78b37897b..73715bf9d26 100644 --- a/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx +++ b/packages/webui/src/client/ui/RundownView/RundownHeader/RundownHeaderTimers.tsx @@ -1,7 +1,4 @@ -import { - type RundownTTimer, - getRundownTTimers, -} from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' +import { type RundownTTimer, getRundownTTimers } from '@sofie-automation/corelib/dist/dataModel/RundownPlaylist/TTimers' import { useTiming } from '../RundownTiming/withTiming' import { RundownUtils } from '../../../lib/rundown.js' import { calculateTTimerDiff, calculateTTimerOverUnder } from '../../../lib/tTimerUtils' @@ -17,7 +14,9 @@ interface IProps { export const RundownHeaderTimers: React.FC = ({ tTimers }) => { useTiming() - const activeTimers = getRundownTTimers(tTimers).filter((t) => t.mode).slice(0, 2) + const activeTimers = getRundownTTimers(tTimers) + .filter((t) => t.mode) + .slice(0, 2) if (activeTimers.length == 0) return null return (