Compare commits

..

9 Commits

Author SHA1 Message Date
Carlos Valente b3201bbc45 style: bump oxfmt to 0.63 and reformat
oxfmt is a 0.x, so the ^0.42.0 caret never crossed a minor and it sat 21
minors behind. The reformat turned out to be tiny — 2 files:

- mcp.server.ts: a call collapses onto fewer lines
- rundown.utils.ts: quoted numeric-enum members lose their quotes.
  `enum X { 'id' }` and `enum X { id }` compile to the same object, so
  the `key in RegenerateWhitelist` check is unaffected.

Kept separate from the dependency commits so it can be ignored in blame.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWs1vf42msmoJ21ABvQMpm
2026-08-15 14:52:36 +00:00
Carlos Valente 2f9db37696 chore(deps): upgrade google sheets client libraries
@googleapis/sheets ^5.0.5 -> ^14.0.0 and google-auth-library ^9.4.2 ->
^10.5.0. No source changes: the sheets_v4 shapes used in sheets.utils.ts
and the OAuth2Client/Credentials usage in sheets.service.ts are unchanged.

google-auth-library lands on 10 rather than the latest 11 on purpose.
@googleapis/sheets@14 reaches google-auth-library through
googleapis-common@8, which pins exactly 10.5.0; asking for ^11 installs
both copies and OAuth2Client stops being assignable to itself
("separate declarations of a private property 'redirectUri'").
googleapis-common@9 moves to ^11, so this can go to 11 once a
@googleapis/sheets release depends on it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWs1vf42msmoJ21ABvQMpm
2026-08-15 14:50:47 +00:00
Carlos Valente 3f4a919483 chore(deps): upgrade @mantine/hooks to v9
v9 splits useFullscreen into useFullscreenElement (ref-based) and
useFullscreenDocument. NavigationMenu never attached the ref, and v8's
useFullscreen fell back to document.documentElement in that case, so
useFullscreenDocument is a behaviourally identical swap.

Every other hook in use is unaffected. The one other breaking change —
useLocalStorage/useSessionStorage now return `T | undefined` when no
defaultValue is given — does not apply: all nine call sites pass one.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWs1vf42msmoJ21ABvQMpm
2026-08-15 14:49:01 +00:00
Carlos Valente e3f6c8295e chore(deps): update pinned dependencies
Bumps the exact-pinned deps, which caret ranges never pick up:
turbo, @playwright/test, @sentry/vite-plugin, vite-plugin-compression2,
react-icons, @base-ui/react, electron-builder, and the ts-essentials /
rimraf catalog entries. Plus dotenv ^16 -> ^17 (one consumer, `import
'dotenv/config'`) and wait-on ^7 -> ^9 (devDep, one CLI call).

No CI change needed for playwright: the workflow already derives its
browser cache key from `pnpm ls @playwright/test`.

Deliberately does NOT run a blanket `pnpm update -r`. Refreshing caret
drift pulls @types/express-serve-static-core 5.0.6 -> 5.1.3, which widens
`req.params` values to `string | string[]` and fails typecheck in 39
places across 8 routers. The widening is correct — express 5 can produce
array params — but adopting it is a source change of its own, and pnpm 11
cannot pin around it (overrides in package.json are silently ignored, and
the pnpm-workspace.yaml equivalent does not re-resolve).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWs1vf42msmoJ21ABvQMpm
2026-08-15 14:45:51 +00:00
Carlos Valente c67dfb17e7 chore(deps): bump oxlint with matching tsgolint peer
oxlint was ranged ^1.57.0, so a routine `pnpm update` would have resolved
1.78.0 on its own — which declares `peerDependencies: { oxlint-tsgolint:
">=7.0.2001" }` against a root pin of ^0.17.4, breaking the --type-aware
lint every workspace runs. Move both together.

oxlint-tsgolint jumped from 0.25.0 to 7.0.2xxx to track the TypeScript-Go
version; 7.0.2001 lines up with the catalog's typescript 7.0.2.

Lint still passes with 0 errors. Warning counts rise (server 64 -> 129)
because the newer oxlint ships more rules; 64 of the 65 new server
warnings are vitest(require-mock-type-parameters) in test files.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWs1vf42msmoJ21ABvQMpm
2026-08-15 14:36:41 +00:00
Carlos Valente 8b4fd32048 chore(deps): dedupe vite and esbuild across the monorepo
vitest 4.0.17 declared `vite: ^6 || ^7`, so it pulled a second vite@7.3.1
into the install alongside the vite@8.0.1 the client builds with, meaning
tests transformed through a different vite than the build. vitest 4.1
widened the range to include ^8, collapsing both to a single vite@8.2.1.

Likewise the tree carried three esbuild copies (0.24.2 from the server,
0.25.5 via tsx, 0.27.4 via vite). vite 8 takes esbuild as a peer and
tsx 4.23 deps ~0.28, so aligning the server on ^0.28 leaves one copy.

Also pin @types/node to the catalog in the client and utils workspaces.
Neither declared it, so transitive @types/* packages resolved it to
25.4.0 and the client was type-checked against Node 25 typings while the
app ships on Node 22 (engines.node ~22).

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NWs1vf42msmoJ21ABvQMpm
2026-08-15 14:34:36 +00:00
Carlos Valente c6eccec30e refactor(settings): show new app indicator 2026-08-09 16:48:20 +02:00
Carlos Valente 5220c2c374 fix(settings): prevent loader overflow 2026-08-09 16:48:20 +02:00
Carlos Valente 4eeeb294f7 chore: update electron navigation 2026-08-09 16:48:20 +02:00
20 changed files with 1409 additions and 2290 deletions
+7 -6
View File
@@ -4,7 +4,7 @@
"private": true,
"type": "module",
"dependencies": {
"@base-ui/react": "1.6.0",
"@base-ui/react": "1.7.0",
"@codemirror/commands": "^6.0.0",
"@codemirror/lang-css": "^6.0.0",
"@codemirror/state": "^6.0.0",
@@ -13,7 +13,7 @@
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@fontsource/open-sans": "^5.2.7",
"@mantine/hooks": "^8.3.7",
"@mantine/hooks": "^9.5.1",
"@sentry/react": "^10.59.0",
"@table-nav/react": "^0.0.7",
"@tanstack/react-query": "^5.101.0",
@@ -29,7 +29,7 @@
"react-dom": "^19.2.7",
"react-fast-compare": "^3.2.2",
"react-hook-form": "^7.80.0",
"react-icons": "5.6.0",
"react-icons": "5.7.0",
"react-router": "^8.0.1",
"react-virtuoso": "^4.18.7",
"zustand": "^5.0.14"
@@ -60,7 +60,8 @@
]
},
"devDependencies": {
"@sentry/vite-plugin": "5.1.1",
"@sentry/vite-plugin": "5.4.0",
"@types/node": "catalog:",
"@types/qrcode": "^1.5.6",
"@types/react": "^19.1.12",
"@types/react-dom": "^19.1.9",
@@ -72,8 +73,8 @@
"ontime-utils": "workspace:*",
"sass": "^1.57.1",
"typescript": "catalog:",
"vite": "8.0.1",
"vite-plugin-compression2": "2.5.1",
"vite": "8.2.1",
"vite-plugin-compression2": "2.5.3",
"vite-plugin-svgr": "4.5.0",
"vitest": "catalog:"
}
@@ -1,5 +1,5 @@
import { Dialog } from '@base-ui/react/dialog';
import { useDisclosure, useFullscreen } from '@mantine/hooks';
import { useDisclosure, useFullscreenDocument } from '@mantine/hooks';
import { memo } from 'react';
import { IoClose, IoContract, IoExpand, IoLockClosedOutline, IoSwapVertical } from 'react-icons/io5';
import { LuCoffee } from 'react-icons/lu';
@@ -33,7 +33,7 @@ function NavigationMenu({ isOpen, onClose }: NavigationMenuProps) {
const isSmallScreen = useIsSmallScreen();
const [isRenameOpen, handlers] = useDisclosure(false);
const { fullscreen, toggle } = useFullscreen();
const { fullscreen, toggle } = useFullscreenDocument();
const { mirror, toggleMirror } = useViewOptionsStore();
const { keepAwake, toggleKeepAwake } = useKeepAwakeOptions();
const location = useLocation();
@@ -200,8 +200,7 @@ $card-padding: 2rem;
.overlay {
position: absolute;
z-index: $zindex-backdrop;
width: 100%;
height: 100%;
inset: 0;
backdrop-filter: blur(2px);
display: grid;
place-content: center;
@@ -0,0 +1,7 @@
.updateIndicator {
width: 0.5em;
height: 0.5em;
flex: 0 0 auto;
border-radius: 99px;
background-color: $red-400;
}
@@ -3,6 +3,8 @@ import useAppVersion from '../../../../common/hooks-query/useAppVersion';
import { appVersion, isOntimeCloud, websiteUrl } from '../../../../externals';
import * as Panel from '../../panel-utils/PanelUtils';
import style from './AppVersion.module.scss';
export default function AppVersion() {
const { data, isError } = useAppVersion();
@@ -18,7 +20,12 @@ export default function AppVersion() {
return (
<Panel.ListItem>
<Panel.Field
title={`Ontime ${appVersion}`}
title={
<>
<span className={style.updateIndicator} aria-hidden='true' />
{`Ontime ${appVersion}`}
</>
}
description={
isOntimeCloud
? `Version ${data.version} is available. Restart your stage to update.`
@@ -26,7 +33,7 @@ export default function AppVersion() {
}
/>
{!isOntimeCloud && (
<ExternalLink href={websiteUrl}>Visit Ontime's page to download the latest version.</ExternalLink>
<ExternalLink href={websiteUrl}>Download the latest version from Ontime's page</ExternalLink>
)}
</Panel.ListItem>
);
@@ -85,10 +85,10 @@ export default function ServerPortSettings() {
</Button>
</Panel.InlineElements>
</Panel.SubHeader>
<Panel.Loader isLoading={status === 'pending'} />
{rootError && <Panel.Error>{rootError}</Panel.Error>}
<Panel.Divider />
<Panel.Section>
<Panel.Loader isLoading={status === 'pending'} />
{data.pendingRestart && (
<Info type='warning'>A port change is pending and will happen on the next restart.</Info>
)}
+2 -2
View File
@@ -14,8 +14,8 @@
"main": "src/main.js",
"devDependencies": {
"electron": "38.2.1",
"electron-builder": "26.9.1",
"wait-on": "^7.2.0"
"electron-builder": "26.15.3",
"wait-on": "^9.0.0"
},
"scripts": {
"dev:electron": "wait-on http://localhost:3000 && cross-env NODE_ENV=development electron .",
+13 -1
View File
@@ -100,7 +100,7 @@ function makeFileMenu(askToQuit, serverUrl, redirectWindow, showDialog, download
submenu: [
{
label: 'New project...',
click: () => redirectWindow('/editor?settings=project__manage&new=true'),
click: () => redirectWindow('/editor?settings=project__create'),
},
{
label: 'Load...',
@@ -202,6 +202,18 @@ function makeSettingsMenu(redirectWindow) {
label: 'View settings',
click: () => redirectWindow('/editor?settings=settings__view'),
},
{
label: 'Custom views',
click: () => redirectWindow('/editor?settings=settings__custom-views'),
},
{
label: 'MCP Server',
click: () => redirectWindow('/editor?settings=settings__mcp'),
},
{
label: 'Server port',
click: () => redirectWindow('/editor?settings=settings__port'),
},
],
},
{
+5 -5
View File
@@ -5,17 +5,17 @@
"version": "4.12.0",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
"@googleapis/sheets": "^14.0.0",
"@modelcontextprotocol/sdk": "^1.15.0",
"cookie": "1.0.2",
"cookie-parser": "1.4.7",
"cors": "2.8.6",
"dotenv": "^16.0.1",
"dotenv": "^17.0.0",
"express": "5.2.1",
"express-static-gzip": "3.0.1",
"express-validator": "7.3.2",
"fast-equals": "^6.0.0",
"google-auth-library": "^9.4.2",
"google-auth-library": "^10.5.0",
"lowdb": "^7.0.1",
"multer": "2.2.0",
"ontime-utils": "workspace:*",
@@ -31,11 +31,11 @@
"@types/multer": "2.1.0",
"@types/node": "catalog:",
"@types/ws": "^8.5.10",
"esbuild": "^0.24.0",
"esbuild": "^0.28.0",
"ontime-types": "workspace:*",
"server-timing": "^3.3.3",
"ts-essentials": "catalog:",
"tsx": "^4.19.2",
"tsx": "^4.23.12",
"typescript": "catalog:",
"vitest": "catalog:"
},
@@ -353,19 +353,19 @@ export function isLoadedPlayable(loadedEventId: EntryId, rundown: Readonly<Rundo
/** List of event properties which do not need the rundown to be regenerated */
enum RegenerateWhitelist {
'id', // adding it for completeness, users cannot change ID
'type', // adding it for completeness, users cannot change ID
'cue',
'title',
'note',
'endAction',
'timerType',
'countToEnd',
'colour',
'timeWarning',
'timeDanger',
'custom',
'triggers',
id, // adding it for completeness, users cannot change ID
type, // adding it for completeness, users cannot change ID
cue,
title,
note,
endAction,
timerType,
countToEnd,
colour,
timeWarning,
timeDanger,
custom,
triggers,
}
/**
+3 -6
View File
@@ -20,12 +20,9 @@ export function createMcpServer(): Server {
{ capabilities: { tools: {}, prompts: {}, resources: {} } },
);
server.setRequestHandler(
ListToolsRequestSchema,
async (): Promise<ListToolsResult> => ({
tools: TOOL_DEFINITIONS as unknown as ListToolsResult['tools'],
}),
);
server.setRequestHandler(ListToolsRequestSchema, async (): Promise<ListToolsResult> => ({
tools: TOOL_DEFINITIONS as unknown as ListToolsResult['tools'],
}));
server.setRequestHandler(CallToolRequestSchema, async (request): Promise<CallToolResult> => {
const { name, arguments: args = {} } = request.params;
@@ -1,7 +1,6 @@
import { EndAction, Instant, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import { EndAction, Playback, TimeOfDay, TimeStrategy, TimerPhase, TimerType } from 'ontime-types';
import { MILLIS_PER_HOUR, MILLIS_PER_MINUTE, MILLIS_PER_SECOND, dayInMs, millisToString } from 'ontime-utils';
import * as timeCore from '../../lib/time-core/timeCore.js';
import type { RuntimeState } from '../../stores/runtimeState.js';
import {
findDayOffset,
@@ -54,12 +53,11 @@ describe('getElapsed()', () => {
it('uses the current pause start while paused', () => {
const state = {
clock: 10 * MILLIS_PER_MINUTE,
_now: timeCore.toInstant((10 * MILLIS_PER_MINUTE) as TimeOfDay, timeCore.now()),
timer: {
startedAt: 2 * MILLIS_PER_MINUTE,
},
_timer: {
pausedAt: timeCore.toInstant((7 * MILLIS_PER_MINUTE) as TimeOfDay, timeCore.now()),
pausedAt: 7 * MILLIS_PER_MINUTE,
pausedDuration: 1 * MILLIS_PER_MINUTE,
},
} as RuntimeState;
@@ -977,40 +975,6 @@ describe('getRuntimeOffset()', () => {
expect(absolute).toBe(25);
});
it('paused time is delayed time when the pause spans midnight', () => {
const state = {
eventNow: {
id: '1',
timeStart: 23 * MILLIS_PER_HOUR, // 23:00
timeEnd: 1 * MILLIS_PER_HOUR, // 01:00
dayOffset: 0,
},
clock: 3 * MILLIS_PER_MINUTE, // 00:03 (after midnight)
_now: timeCore.toInstant((3 * MILLIS_PER_MINUTE) as TimeOfDay, timeCore.now()),
timer: {
startedAt: 23 * MILLIS_PER_HOUR, // started on time at 23:00
current: 25, // still counting down
addedTime: 0,
},
_timer: {
pausedAt: timeCore.toInstant(
(23 * MILLIS_PER_HOUR + 58 * MILLIS_PER_MINUTE) as TimeOfDay,
(timeCore.now() - dayInMs) as Instant,
), // 23:58, before midnight
pausedDuration: 0,
},
rundown: {
actualStart: 23 * MILLIS_PER_HOUR,
plannedStart: 23 * MILLIS_PER_HOUR,
currentDay: 0,
},
_startDayOffset: 0,
} as RuntimeState;
// paused from 23:58 to 00:03 -> so elapsed should still be 58 minutes
expect(getElapsed(state)).toBe(58 * MILLIS_PER_MINUTE);
});
it('offset doesnt exist if we havent started', () => {
const state = {
clock: 78480789,
+5 -7
View File
@@ -1,7 +1,6 @@
import { Day, MaybeNumber, TimeOfDay, TimerPhase } from 'ontime-types';
import { MILLIS_PER_HOUR, checkIsNow, dayInMs, isPlaybackActive } from 'ontime-utils';
import * as timeCore from '../lib/time-core/timeCore.js';
import type { RuntimeState } from '../stores/runtimeState.js';
/**
@@ -97,18 +96,17 @@ export function getCurrent(state: RuntimeState): number {
* Calculates active time elapsed since the timer started.
*/
export function getElapsed(state: RuntimeState): MaybeNumber {
const { clock, _now } = state;
const { clock } = state;
const { startedAt } = state.timer;
const { pausedDuration, pausedAt } = state._timer;
const { pausedAt, pausedDuration } = state._timer;
if (startedAt === null) {
return null;
}
const currentPauseDuration = pausedAt !== null ? timeCore.timeSince(_now, pausedAt) : 0;
const elapsedSinceStart = getTimeSinceStart(clock, startedAt);
const activeElapsed = elapsedSinceStart - pausedDuration - currentPauseDuration;
const referenceClock = pausedAt ?? clock;
const elapsedSinceStart = getTimeSinceStart(referenceClock, startedAt);
const activeElapsed = elapsedSinceStart - pausedDuration;
return Math.max(0, activeElapsed);
}
@@ -1,11 +1,10 @@
import { Instant, OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
import { OffsetMode, Playback, type TimeOfDay, TimerPhase } from 'ontime-types';
import { deepmerge } from 'ontime-utils';
import type { RuntimeState } from '../runtimeState.js';
const baseState: RuntimeState = {
clock: 0 as TimeOfDay,
_now: 0 as Instant,
eventNow: null,
eventNext: null,
eventFlag: null,
@@ -135,7 +135,7 @@ describe('mutation on runtimeState', () => {
playback: Playback.Pause,
addedTime: 0,
});
expect(newState._timer.pausedAt).toEqual(newState._now);
expect(newState._timer.pausedAt).toEqual(newState.clock);
success = pause();
expect(success).toBe(false);
@@ -248,59 +248,6 @@ describe('mutation on runtimeState', () => {
state = getState();
expect(state.timer.elapsed).toBe(3 * MILLIS_PER_MINUTE);
});
test('elapsed excludes a pause that spans midnight', async () => {
clearState();
// an event that runs over midnight (23:00 -> 01:00)
const event = {
...mockEvent,
id: 'elapsed-pause-midnight',
timeStart: 23 * MILLIS_PER_HOUR,
timeEnd: 1 * MILLIS_PER_HOUR,
duration: 2 * MILLIS_PER_HOUR,
};
const mockRundown = makeRundown({
entries: { [event.id]: event },
order: [event.id],
});
await initRundown(mockRundown, {});
vi.runAllTimers();
const { metadata, rundown } = rundownCache.get();
// start before midnight
vi.setSystemTime('jan 1 23:50');
load(event, rundown, metadata);
start();
// 8 minutes of active running before we pause
vi.setSystemTime('jan 1 23:58');
update();
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
pause();
// elapsed is active time since start, so it must not advance while paused,
// not even when the pause itself crosses midnight
vi.setSystemTime('jan 2 00:01');
update();
expect(getState().timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
// resume 5 minutes after pausing, having crossed midnight (23:58 -> 00:03)
vi.setSystemTime('jan 2 00:03');
start();
let state = getState();
// the accumulated pause count is 5 minutes, regardless of the midnight wrap
expect(state._timer.pausedDuration).toBe(5 * MILLIS_PER_MINUTE);
// and elapsed still reflects only the 8 active minutes
expect(state.timer.elapsed).toBe(8 * MILLIS_PER_MINUTE);
// 2 more active minutes after resume -> 10 minutes elapsed
vi.setSystemTime('jan 2 00:05');
update();
state = getState();
expect(state.timer.elapsed).toBe(10 * MILLIS_PER_MINUTE);
});
});
test('runtime offset', async () => {
+16 -21
View File
@@ -63,9 +63,7 @@ export type RuntimeState = {
// private properties of the timer calculations
_timer: {
forceFinish: Maybe<TimeOfDay>; // whether we should declare an event as finished, will contain the finish time
pausedAt: Maybe<Instant>;
/** Accumulate pause duration but dose not include the current pause */
pausedAt: Maybe<TimeOfDay>;
pausedDuration: number;
secondaryTarget: Maybe<TimeOfDay>;
hasFinished: boolean;
@@ -78,12 +76,10 @@ export type RuntimeState = {
_end: ExpectedMetadata;
_startEpoch: Maybe<Instant>;
_startDayOffset: Maybe<Day>;
_now: Instant;
};
const runtimeState: RuntimeState = {
clock: timeCore.timeOfDayNow(),
_now: timeCore.now(),
groupNow: null,
eventNow: null,
eventNext: null,
@@ -108,12 +104,6 @@ const runtimeState: RuntimeState = {
_startDayOffset: null,
};
/** set the current clock to ensure parity between _now and clock */
function setClock(state: RuntimeState) {
state._now = timeCore.now();
state.clock = timeCore.toTimeOfDay(state._now);
}
export function getState(): Readonly<RuntimeState> {
// create a shallow copy of the state
return {
@@ -146,7 +136,7 @@ export function clearEventData() {
runtimeState.rundown.selectedEventIndex = null;
runtimeState.timer.playback = Playback.Stop;
setClock(runtimeState);
runtimeState.clock = timeCore.timeOfDayNow();
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown
@@ -179,7 +169,7 @@ export function clearState() {
runtimeState._end = null;
runtimeState.timer.playback = Playback.Stop;
setClock(runtimeState);
runtimeState.clock = timeCore.timeOfDayNow();
runtimeState.timer = { ...runtimeStorePlaceholder.timer };
// when clearing, we maintain the total delay from the rundown
@@ -432,12 +422,15 @@ export function start(state: RuntimeState = runtimeState): boolean {
return false;
}
setClock(state);
const epoch = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
state.clock = now;
state.timer.secondaryTimer = null;
// add paused time if it exists
if (state._timer.pausedAt) {
const timeToAdd = state._now - state._timer.pausedAt;
const timeToAdd = state.clock - state._timer.pausedAt;
state.timer.addedTime += timeToAdd;
state._timer.pausedDuration += timeToAdd;
state._timer.pausedAt = null;
@@ -454,7 +447,7 @@ export function start(state: RuntimeState = runtimeState): boolean {
if (state.rundown.actualStart === null) {
state._startDayOffset = (findDayOffset(state.eventNow.timeStart, state.clock) + state.eventNow.dayOffset) as Day;
state.rundown.currentDay = state._startDayOffset;
state._startEpoch = state._now;
state._startEpoch = epoch;
state.rundown.actualStart = state.clock;
}
@@ -488,8 +481,8 @@ export function pause(state: RuntimeState = runtimeState): boolean {
}
state.timer.playback = Playback.Pause;
setClock(state);
state._timer.pausedAt = state._now;
state.clock = timeCore.timeOfDayNow();
state._timer.pausedAt = state.clock;
return true;
}
@@ -554,7 +547,9 @@ export type UpdateResult = {
export function update(): UpdateResult {
// 0. there are some things we always do
const previousClock = runtimeState.clock;
setClock(runtimeState); // we update the clock on every update call
const epoch = timeCore.now();
const now = timeCore.toTimeOfDay(epoch);
runtimeState.clock = now; // we update the clock on every update call
// 1. is playback idle?
if (!isPlaybackActive(runtimeState.timer.playback)) {
@@ -563,13 +558,13 @@ export function update(): UpdateResult {
// calculate currentDay from epoch (days elapsed since playback was started)
if (runtimeState._startEpoch !== null && runtimeState._startDayOffset !== null) {
const daysSinceStart = timeCore.daysSinceStart(runtimeState._startEpoch, runtimeState._now);
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 clockHasCrossedMidnight = hasCrossedMidnight(previousClock, runtimeState.clock);
const clockHasCrossedMidnight = hasCrossedMidnight(previousClock, now);
return updateIfWaitingToRoll(clockHasCrossedMidnight);
}
+5 -5
View File
@@ -41,14 +41,14 @@
"format:check": "oxfmt --check"
},
"devDependencies": {
"@playwright/test": "1.60.0",
"@playwright/test": "1.62.1",
"@types/node": "catalog:",
"cross-env": "^7.0.3",
"oxfmt": "^0.42.0",
"oxlint": "^1.57.0",
"oxlint-tsgolint": "^0.17.4",
"oxfmt": "^0.63.0",
"oxlint": "^1.78.0",
"oxlint-tsgolint": "^7.0.2001",
"rimraf": "catalog:",
"turbo": "2.8.20",
"turbo": "2.10.10",
"typescript": "catalog:"
},
"packageManager": "pnpm@11.1.2+sha512.415a1cc25974731e75455c1468371be74c5aa5fb7621b50d4056d222451609f11412f23fd602e6169f1e060466641f798597e1be961a10688836a67b16569499",
+1
View File
@@ -14,6 +14,7 @@
"nanoid": "^6.0.0"
},
"devDependencies": {
"@types/node": "catalog:",
"ontime-types": "workspace:*",
"typescript": "catalog:",
"vitest": "catalog:"
+1312 -2120
View File
File diff suppressed because it is too large Load Diff
+3 -3
View File
@@ -4,10 +4,10 @@ packages:
catalog:
'@types/node': 22.19.11
rimraf: 6.0.1
ts-essentials: 10.1.1
rimraf: 6.1.3
ts-essentials: 10.2.1
typescript: 7.0.2
vitest: 4.0.17
vitest: 4.1.10
allowBuilds:
'@parcel/watcher': true