Compare commits

...

13 Commits

Author SHA1 Message Date
Carlos Valente 03c9f51b47 chore: update demo project 2025-10-17 12:20:18 +02:00
Carlos Valente 8d00fd3966 bump version to 4.0.1 2025-10-17 12:20:18 +02:00
Carlos Valente 80fd21354e fix: apply styling to overtime timer 2025-10-16 21:13:26 +02:00
Carlos Valente a87b15825a fix: end message scaling 2025-10-16 21:13:26 +02:00
Carlos Valente 6a8f98c469 fix: end message only shows if timer is in overtime 2025-10-16 21:13:26 +02:00
jwetzell 8685298430 Merge pull request #1830 from cpvalente/fix/linux-project-download 2025-10-15 07:25:57 -05:00
jwetzell d43af2db57 pass options to project download to allow folders beginning with a dot 2025-10-14 19:51:15 -05:00
jwetzell ef718c48e4 Merge pull request #1828 from cpvalente/npm-trusted-publisher
update cli and resolver workflows for new NPM auth
2025-10-14 16:18:53 -05:00
Alex Christoffer Rasmussen 45e7373279 Small clean-up (#1827)
* chore: avoid shadow variable name

* chore: map could just be foreach

* clean uneeded template literal

* lint
2025-10-13 20:24:25 +02:00
jwetzell 6af2d6e021 update cli and resolver workflows for new NPM auth 2025-10-12 14:11:52 -05:00
Alex Christoffer Rasmussen 9b36f36f73 fix: next cue (#1821)
* fix next cue

* Update apps/server/src/services/runtime-service/runtime.service.ts

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>

---------

Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2025-10-12 14:42:55 +02:00
Carlos Valente 3a0dbf057e chore: update docs 2025-10-12 07:48:25 +02:00
Carlos Valente dae84ab48a chore: update screenshots 2025-10-12 07:48:25 +02:00
23 changed files with 149 additions and 77 deletions
Binary file not shown.

Before

Width:  |  Height:  |  Size: 183 KiB

After

Width:  |  Height:  |  Size: 259 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 336 KiB

After

Width:  |  Height:  |  Size: 149 KiB

+3 -4
View File
@@ -45,10 +45,9 @@ jobs:
- name: Copy external
run: cp -R apps/server/src/external apps/cli/external
- name: Setup pnpm auth config
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
- name: Install newer version of npm
run: npm install -g npm@11
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
+3 -4
View File
@@ -34,10 +34,9 @@ jobs:
- name: Build project packages
run: pnpm build:resolver
- name: Setup pnpm auth config
run: pnpm config set '//registry.npmjs.org/:_authToken' "${NODE_AUTH_TOKEN}"
env:
NODE_AUTH_TOKEN: ${{secrets.NPM_TOKEN}}
# This will be included in v24 of NodeJS so when the project upgrades to that this can be removed
- name: Install newer version of npm
run: npm install -g npm@11
- name: Publish to NPM
run: pnpm publish --access public --no-git-checks
-2
View File
@@ -26,8 +26,6 @@ We do our best to have most topics covered by the documentation. However, if you
Let us know!
Ontime improves from the collaboration with its users. We would like to understand how you use Ontime and appreciate your feedback.
We would also like to include a testimonials section in our ✨new website✨. It would be great to showcase the diversity of users running Ontime.
# Ontime
Ontime is a browser-based application that manages event rundowns, scheduling, and cueing.
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@getontime/cli",
"version": "4.0.0",
"version": "4.0.1",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-ui",
"version": "4.0.0",
"version": "4.0.1",
"private": true,
"type": "module",
"dependencies": {
+2 -2
View File
@@ -92,10 +92,10 @@
.end-message {
text-align: center;
font-size: 11.5vw;
line-height: 0.9em;
font-weight: 600;
color: var(--timer-overtime-color-override, $timer-finished-color);
padding: 0;
height: 100%;
}
.timer {
@@ -117,7 +117,7 @@
// use a class instead of a phase, to allow suppressing overtime style
&--finished {
color: var(--timer-colour, var(--timer-overtime-color-override, $timer-finished-color));
color: var(--timer-overtime-color-override, $timer-finished-color);
}
&[data-phase="warning"] {
+1 -1
View File
@@ -105,7 +105,7 @@ export function getShowModifiers(
}
return {
showEndMessage: freezeOvertime && freezeMessage !== '',
showEndMessage: phase === TimerPhase.Overtime && freezeOvertime && freezeMessage !== '',
showFinished: phase === TimerPhase.Overtime,
showWarning: phase === TimerPhase.Warning,
showDanger: phase === TimerPhase.Danger,
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime-electron",
"version": "4.0.0",
"version": "4.0.1",
"author": "Carlos Valente",
"description": "Time keeping for live events",
"repository": "https://github.com/cpvalente/ontime",
+1 -1
View File
@@ -2,7 +2,7 @@
"name": "ontime-server",
"type": "module",
"main": "src/index.ts",
"version": "4.0.0",
"version": "4.0.1",
"exports": "./src/index.js",
"dependencies": {
"@googleapis/sheets": "^5.0.5",
+1 -1
View File
@@ -109,7 +109,7 @@ export async function projectDownload(req: Request, res: Response) {
return;
}
res.download(pathToFile, filename, (error) => {
res.download(pathToFile, filename, { dotfiles: 'allow' }, (error) => {
if (error) {
const message = getErrorMessage(error);
res.status(500).send({ message });
@@ -355,7 +355,7 @@ export function migrateRundown(
const newCustom: EntryCustomFields = {};
if (translationTable) {
Object.entries(custom).map(([key, value]) => {
Object.entries(custom).forEach(([key, value]) => {
const newKey = translationTable.get(key);
if (newKey) {
newCustom[newKey] = value;
@@ -19,8 +19,10 @@ exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`
"Warning Time",
"Danger Time",
"Skip",
"Custom Field",
"Images",
"Video Notes",
"Audio Notes",
"PowerPoint Name",
"PowerPoint Slide",
],
[
"e2163f",
@@ -33,16 +35,21 @@ exports[`rundownToTabular() > returns an array of arrays describing a rundown 1`
"",
"",
"",
"Go to Settings cog wheel top left of Editor, Project > Create",
"Moderator - Emma Thompson
Speakers
- Liam Carter + Sophia Patel
- Ethan Brooks
- Lucas Bennett",
"milestone",
"",
"",
"",
"",
"Create Custom Fields to store specific text information or images.
Go to Settings > Project data > Custom fields",
"https://www.getontime.no/images/icons/ontime-logo.png",
"",
"",
"",
"",
],
[
"7eaf99",
@@ -63,6 +70,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"9bf60f",
@@ -81,8 +90,11 @@ Go to Settings > Project data > Custom fields",
"00:10:00",
"00:05:00",
"",
"Put additional info here",
"",
"Camera + PowerPoint on stream
PowerPoint on screens",
"2x Wireless Hand Helds",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"bf71a2",
@@ -103,6 +115,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"c2697f",
@@ -121,8 +135,11 @@ Go to Settings > Project data > Custom fields",
"00:02:00",
"00:01:00",
"",
"",
"",
"Cameras on stream
PowerPoint on screens",
"1x Wireless Hand Held",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"fa593e",
@@ -141,8 +158,11 @@ Go to Settings > Project data > Custom fields",
"00:02:00",
"00:01:00",
"",
"",
"",
"Camera + PowerPoint on stream
PowerPoint on screens",
"2x Wireless Hand Helds",
"Session1.pptx",
"https://www.getontime.no/images/aux/demo-slide2.webp",
],
[
"a8b0b3",
@@ -163,6 +183,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"",
@@ -201,6 +223,8 @@ Go to Settings > Project data > Custom fields",
"",
"",
"",
"",
"",
],
[
"0aaa7d",
@@ -213,15 +237,16 @@ Go to Settings > Project data > Custom fields",
"01:00:00",
"TRUE",
"",
"Buffet in lobby
Music plays, holding slide on screens",
"Buffet in lobby",
"count-down",
"",
"00:02:00",
"00:01:00",
"",
"",
"",
"Holding slide on screens",
"House music",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"",
@@ -260,6 +285,8 @@ Music plays, holding slide on screens",
"",
"",
"",
"",
"",
],
[
"02afca",
@@ -280,6 +307,8 @@ Music plays, holding slide on screens",
"",
"",
"",
"",
"",
],
[
"75ce86",
@@ -298,8 +327,14 @@ Music plays, holding slide on screens",
"00:02:00",
"00:01:00",
"",
"",
"",
"Camera + PPT + Video on stream
PowerPoint + Video on screens
Video file: Session2.mp4",
"1x Wireless Hand Held
1x Video with audio",
"Session2.pptx",
"https://www.getontime.no/images/aux/demo-slide3.webp",
],
[
"e10ed9",
@@ -318,8 +353,10 @@ Music plays, holding slide on screens",
"00:02:00",
"00:01:00",
"",
"",
"",
"Holding slide on screens",
"1x Wireless Hand Held",
"HoldingSlide.pptx",
"https://www.getontime.no/images/aux/demo-slide1.webp",
],
[
"07df89",
@@ -340,6 +377,8 @@ Music plays, holding slide on screens",
"",
"",
"",
"",
"",
],
[
"",
@@ -52,7 +52,7 @@ router.get('/:rundownId/export', validateRundownExport, (req: Request, res: Resp
const customFields = getProjectCustomFields();
const buffer = generateExcelFile(rundown, customFields);
res.setHeader('Content-Disposition', `attachment;`);
res.setHeader('Content-Disposition', 'attachment;');
res.setHeader('Content-Type', EXCEL_MIME);
res.setHeader('Content-Length', buffer.length.toString());
res.status(200).send(buffer);
+2 -2
View File
@@ -123,8 +123,8 @@ function formatTimer(number) {
* @param {number} number - The number to pad
* @returns {string} The padded number string
*/
function leftPad(number) {
return Math.floor(number).toString().padStart(2, '0');
function leftPad(val) {
return Math.floor(val).toString().padStart(2, '0');
}
}
+51 -17
View File
@@ -28,13 +28,9 @@ export const demoDb: DatabaseModel = {
type: SupportedEntry.Milestone,
cue: 'Demo',
title: 'Clear all, or Create New Project to start fresh',
note: 'Go to Settings cog wheel top left of Editor, Project > Create',
note: 'Moderator - Emma Thompson\n\nSpeakers\n- Liam Carter + Sophia Patel\n- Ethan Brooks\n- Lucas Bennett',
colour: '#9d9d9d',
custom: {
Custom_Field:
'Create Custom Fields to store specific text information or images.\n\nGo to Settings > Project data > Custom fields',
Images: 'https://www.getontime.no/images/icons/ontime-logo.png',
},
custom: {},
parent: null,
revision: 0,
},
@@ -78,7 +74,10 @@ export const demoDb: DatabaseModel = {
timeWarning: 600000,
timeDanger: 300000,
custom: {
Custom_Field: 'Put additional info here',
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Camera + PowerPoint on stream\nPowerPoint on screens',
Audio_Notes: '2x Wireless Hand Helds',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
@@ -117,7 +116,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Cameras on stream\nPowerPoint on screens',
Audio_Notes: '1x Wireless Hand Held',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
fa593e: {
@@ -144,7 +148,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide2.webp',
Video_Notes: 'Camera + PowerPoint on stream\nPowerPoint on screens',
Audio_Notes: '2x Wireless Hand Helds',
PowerPoint_Name: 'Session1.pptx',
},
triggers: [],
},
a8b0b3: {
@@ -187,7 +196,7 @@ export const demoDb: DatabaseModel = {
timerType: TimerType.CountDown,
countToEnd: false,
skip: false,
note: 'Buffet in lobby\nMusic plays, holding slide on screens',
note: 'Buffet in lobby',
colour: '#779BE7',
delay: 0,
dayOffset: 0,
@@ -197,7 +206,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Holding slide on screens',
Audio_Notes: 'House music',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
'6b0edb': {
@@ -250,7 +264,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide3.webp',
Video_Notes: 'Camera + PPT + Video on stream\nPowerPoint + Video on screens\n\nVideo file: Session2.mp4',
Audio_Notes: '1x Wireless Hand Held\n1x Video with audio',
PowerPoint_Name: 'Session2.pptx',
},
triggers: [],
},
e10ed9: {
@@ -277,7 +296,12 @@ export const demoDb: DatabaseModel = {
revision: 0,
timeWarning: 120000,
timeDanger: 60000,
custom: {},
custom: {
PowerPoint_Slide: 'https://www.getontime.no/images/aux/demo-slide1.webp',
Video_Notes: 'Holding slide on screens',
Audio_Notes: '1x Wireless Hand Held',
PowerPoint_Name: 'HoldingSlide.pptx',
},
triggers: [],
},
'07df89': {
@@ -341,15 +365,25 @@ export const demoDb: DatabaseModel = {
},
],
customFields: {
Custom_Field: {
Video_Notes: {
type: 'text',
colour: '#FFAB33',
label: 'Video Notes',
},
Audio_Notes: {
type: 'text',
colour: '#339E4E',
label: 'Audio Notes',
},
PowerPoint_Name: {
type: 'text',
colour: '#3E75E8',
label: 'Custom Field',
label: 'PowerPoint Name',
},
Images: {
PowerPoint_Slide: {
type: 'image',
colour: '#ED3333',
label: 'Images',
label: 'PowerPoint Slide',
},
},
automation: {
@@ -50,7 +50,7 @@ export function isRestorePoint(restorePoint: unknown): restorePoint is RestorePo
return false;
}
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
if (!is.number(restorePoint.startEpoch) && restorePoint.startEpoch !== null) {
return false;
}
@@ -273,13 +273,14 @@ class RuntimeService {
public startByCue(cue: string): boolean {
const state = runtimeState.getState();
const rundown = getCurrentRundown();
const { playableEventOrder } = getRundownMetadata();
const { timedEventOrder } = getRundownMetadata();
const event = findNextPlayableWithCue(
rundown,
playableEventOrder,
timedEventOrder,
cue,
state.rundown.selectedEventIndex ?? undefined,
state.timer.playback === Playback.Armed, // If we are armed allow the armed event to be considered for playback
);
if (!event) {
@@ -341,6 +342,7 @@ class RuntimeService {
playableEventOrder,
cue,
state.rundown.selectedEventIndex ?? undefined,
false,
);
if (!event) {
@@ -118,24 +118,25 @@ export function findNextPlayableId(playableEventsOrder: EntryId[], currentEventI
*/
export function findNextPlayableWithCue(
rundown: Rundown,
playableEventsOrder: EntryId[],
timedEventsOrder: EntryId[],
targetCue: string,
currentEventIndex = 0,
allowCurrent = false,
): OntimeEvent | undefined {
const lowerCaseCue = targetCue.toLowerCase();
const startFromIndex = allowCurrent ? currentEventIndex : currentEventIndex + 1;
for (let i = currentEventIndex; i < playableEventsOrder.length; i++) {
const eventId = playableEventsOrder[i];
for (let i = startFromIndex; i < timedEventsOrder.length; i++) {
const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event;
}
}
for (let i = 0; i < currentEventIndex; i++) {
const eventId = playableEventsOrder[i];
for (let i = 0; i < startFromIndex; i++) {
const eventId = timedEventsOrder[i];
const event = rundown.entries[eventId];
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue.toLowerCase() === lowerCaseCue) {
if (isOntimeEvent(event) && isPlayableEvent(event) && event.cue === targetCue) {
return event;
}
}
@@ -377,7 +377,7 @@ export async function upload(sheetId: string, options: ImportMap) {
const titleMetadata = Object.values(sheetMetadata)[0];
if (titleMetadata === undefined) {
throw new Error(`Sheet read failed: failed to find title row`);
throw new Error('Sheet read failed: failed to find title row');
}
const titleRow = titleMetadata['row'];
const updateRundown = Array<sheets_v4.Schema$Request>();
+4 -4
View File
@@ -11,14 +11,14 @@ describe('parseExcelDate', () => {
['1899-12-30T07:00:00.000Z', 28800000],
['1899-12-30T08:00:10.000Z', 32410000],
['1899-12-30T08:30:00.000Z', 34200000],
])(`handles %s`, (fromExcel, expected) => {
])('handles %s', (fromExcel, expected) => {
expect(parseExcelDate(fromExcel)).toBe(expected);
});
});
describe('parses a time string that passes validation', () => {
test.each([['10:00:00'], ['10:00'], ['10:00AM'], ['10:00am'], ['10:00PM'], ['10:00pm']])(
`handles %s`,
'handles %s',
(fromExcel) => {
expect(parseExcelDate(fromExcel)).not.toBe(0);
},
@@ -26,13 +26,13 @@ describe('parseExcelDate', () => {
});
describe('uses numeric fields as minutes', () => {
test.each([[1], [10], [100]])(`handles numeric fields %s`, (fromExcel) => {
test.each([[1], [10], [100]])('handles numeric fields %s', (fromExcel) => {
expect(parseExcelDate(fromExcel)).toBe(fromExcel * MILLIS_PER_MINUTE);
});
});
describe('returns 0 on other strings', () => {
test.each([['test'], [''], ['x']])(`handles invalid fields %s`, (fromExcel) => {
test.each([['test'], [''], ['x']])('handles invalid fields %s', (fromExcel) => {
expect(parseExcelDate(fromExcel)).toBe(0);
});
});
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "ontime",
"version": "4.0.0",
"version": "4.0.1",
"description": "Time keeping for live events",
"keywords": [
"ontime",