diff --git a/apps/client/src/common/models/OntimeSettings.ts b/apps/client/src/common/models/OntimeSettings.ts
index 2ef016c05..a12319f64 100644
--- a/apps/client/src/common/models/OntimeSettings.ts
+++ b/apps/client/src/common/models/OntimeSettings.ts
@@ -1,5 +1,4 @@
import { Settings } from 'ontime-types';
-import { normaliseAuxTimerNames } from 'ontime-utils';
export const ontimePlaceholderSettings: Settings = {
version: '4.0.0',
@@ -7,5 +6,5 @@ export const ontimePlaceholderSettings: Settings = {
operatorKey: null,
timeFormat: '24',
language: 'en',
- auxTimerNames: normaliseAuxTimerNames(),
+ auxTimerNames: ['', '', ''],
};
diff --git a/apps/client/src/common/utils/auxTimerUtils.ts b/apps/client/src/common/utils/auxTimerUtils.ts
index 4acf8caf3..ba4ccd77f 100644
--- a/apps/client/src/common/utils/auxTimerUtils.ts
+++ b/apps/client/src/common/utils/auxTimerUtils.ts
@@ -1,9 +1,3 @@
-/**
- * Resolves the display label for an aux timer.
- * Falls back to the provided default when no custom name is set.
- * @param name - the aux timer's custom name (from the runtime store)
- * @param fallback - label to use when no custom name is set
- */
export function getAuxTimerLabel(name: string | undefined, fallback: string): string {
const custom = name?.trim();
return custom ? custom : fallback;
diff --git a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx
index c7527d1ab..11f6413b4 100644
--- a/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx
+++ b/apps/client/src/features/app-settings/panel/automations-panel/OntimeActionForm.tsx
@@ -51,21 +51,21 @@ export default function OntimeActionForm({
}}
value={watch(`outputs.${index}.action`)}
options={[
- { value: 'aux1-pause', label: 'Aux timer 1: pause' },
- { value: 'aux2-pause', label: 'Aux timer 2: pause' },
- { value: 'aux3-pause', label: 'Aux timer 3: pause' },
+ { value: 'aux1-pause', label: 'Aux 1: pause' },
+ { value: 'aux2-pause', label: 'Aux 2: pause' },
+ { value: 'aux3-pause', label: 'Aux 3: pause' },
- { value: 'aux1-start', label: 'Aux timer 1: start' },
- { value: 'aux2-start', label: 'Aux timer 2: start' },
- { value: 'aux3-start', label: 'Aux timer 3: start' },
+ { value: 'aux1-start', label: 'Aux 1: start' },
+ { value: 'aux2-start', label: 'Aux 2: start' },
+ { value: 'aux3-start', label: 'Aux 3: start' },
- { value: 'aux1-stop', label: 'Aux timer 1: stop' },
- { value: 'aux2-stop', label: 'Aux timer 2: stop' },
- { value: 'aux3-stop', label: 'Aux timer 3: stop' },
+ { value: 'aux1-stop', label: 'Aux 1: stop' },
+ { value: 'aux2-stop', label: 'Aux 2: stop' },
+ { value: 'aux3-stop', label: 'Aux 3: stop' },
- { value: 'aux1-set', label: 'Aux timer 1: set' },
- { value: 'aux2-set', label: 'Aux timer 2: set' },
- { value: 'aux3-set', label: 'Aux timer 3: set' },
+ { value: 'aux1-set', label: 'Aux 1: set' },
+ { value: 'aux2-set', label: 'Aux 2: set' },
+ { value: 'aux3-set', label: 'Aux 3: set' },
{ value: 'playback-start', label: 'Playback: start' },
{ value: 'playback-stop', label: 'Playback: stop' },
diff --git a/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx b/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx
index cff483035..d6fdf4a6b 100644
--- a/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx
+++ b/apps/client/src/features/app-settings/panel/settings-panel/AuxTimerSettings.tsx
@@ -1,5 +1,5 @@
import { Settings } from 'ontime-types';
-import { auxTimerNameMaxLength, numberOfAuxTimers } from 'ontime-utils';
+import { auxTimerNameMaxLength } from 'ontime-utils';
import { useEffect } from 'react';
import { useForm } from 'react-hook-form';
@@ -12,9 +12,6 @@ import useSettings from '../../../../common/hooks-query/useSettings';
import { preventEscape } from '../../../../common/utils/keyEvent';
import * as Panel from '../../panel-utils/PanelUtils';
-/** zero based index of each aux timer, used to address the auxTimerNames array */
-const auxTimerIndexes = Array.from({ length: numberOfAuxTimers }, (_, index) => index);
-
export default function AuxTimerSettings() {
const { data, status, refetch } = useSettings();
const {
@@ -30,7 +27,6 @@ export default function AuxTimerSettings() {
},
});
- // update form if we get new data from server
useEffect(() => {
if (data) {
reset(data);
@@ -79,16 +75,18 @@ export default function AuxTimerSettings() {
{errors.root?.message}
- {auxTimerIndexes.map((index) => (
-
-
-
-
- ))}
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/apps/client/src/features/control/playback/PlaybackControl.module.scss b/apps/client/src/features/control/playback/PlaybackControl.module.scss
index 2d6ab5f0a..eee06cbab 100644
--- a/apps/client/src/features/control/playback/PlaybackControl.module.scss
+++ b/apps/client/src/features/control/playback/PlaybackControl.module.scss
@@ -8,6 +8,9 @@
align-items: center;
justify-content: space-between;
margin-top: 1rem;
+}
+
+.label {
font-size: $inner-section-text-size;
color: $label-gray;
}
diff --git a/apps/client/src/features/control/playback/PlaybackControl.tsx b/apps/client/src/features/control/playback/PlaybackControl.tsx
index bdc7065ef..8b69cdcb7 100644
--- a/apps/client/src/features/control/playback/PlaybackControl.tsx
+++ b/apps/client/src/features/control/playback/PlaybackControl.tsx
@@ -27,7 +27,7 @@ export default function PlaybackControl() {
timerPhase={data.timerPhase}
/>
- Aux timers
+ Aux timers
): {
const operatorKey = settings?.operatorKey;
const timeFormat = settings?.timeFormat;
const language = settings?.language;
- const auxTimerNames = normaliseAuxTimerNames(settings?.auxTimerNames);
+ const auxTimerNames = sanitiseAuxTimerNames(settings?.auxTimerNames);
const version = '4.5.0';
db.settings = {
version,
diff --git a/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts b/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts
index 48906eb2e..aab720630 100644
--- a/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts
+++ b/apps/server/src/api-data/settings/__tests__/settings.parser.test.ts
@@ -20,7 +20,7 @@ describe('parseSettings()', () => {
});
});
- it('carries custom aux timer names through and normalises to a length-3 array', () => {
+ it('carries custom aux timer names through and pads to a length-3 array', () => {
const result = parseSettings({
settings: { version: '1', auxTimerNames: ['Speaker'] } as unknown as Settings,
});
@@ -35,7 +35,6 @@ describe('parseSettings()', () => {
});
it('creates the aux timer names for project files made before the feature existed', () => {
- // a settings object as found in a project file which predates aux timer naming
const oldSettings = {
version: '4.5.0',
editorKey: null,
@@ -47,7 +46,6 @@ describe('parseSettings()', () => {
const result = parseSettings({ settings: oldSettings as Settings });
expect(result.auxTimerNames).toStrictEqual(['', '', '']);
- // the rest of the settings are untouched
expect(result).toMatchObject({ timeFormat: '24', language: 'en' });
});
});
diff --git a/apps/server/src/api-data/settings/settings.parser.ts b/apps/server/src/api-data/settings/settings.parser.ts
index 3e19847f0..1e7d89e0c 100644
--- a/apps/server/src/api-data/settings/settings.parser.ts
+++ b/apps/server/src/api-data/settings/settings.parser.ts
@@ -1,5 +1,5 @@
import { DatabaseModel, Settings } from 'ontime-types';
-import { normaliseAuxTimerNames } from 'ontime-utils';
+import { sanitiseAuxTimerNames } from 'ontime-utils';
import { getPartialProject } from '../../models/dataModel.js';
@@ -23,7 +23,7 @@ export function parseSettings(data: Partial): Settings {
operatorKey: data.settings.operatorKey ?? defaultSettings.operatorKey,
timeFormat: data.settings.timeFormat ?? defaultSettings.timeFormat,
language: data.settings.language ?? defaultSettings.language,
- // property added in v4.6.0, older project files will not contain it
- auxTimerNames: normaliseAuxTimerNames(data.settings.auxTimerNames),
+ // older project files predate this property
+ auxTimerNames: sanitiseAuxTimerNames(data.settings.auxTimerNames),
};
}
diff --git a/apps/server/src/api-data/settings/settings.validation.ts b/apps/server/src/api-data/settings/settings.validation.ts
index eebac488e..e83ea679d 100644
--- a/apps/server/src/api-data/settings/settings.validation.ts
+++ b/apps/server/src/api-data/settings/settings.validation.ts
@@ -1,5 +1,5 @@
import { body } from 'express-validator';
-import { normaliseAuxTimerNames } from 'ontime-utils';
+import { sanitiseAuxTimerNames } from 'ontime-utils';
import { requestValidationFunction } from '../validation-utils/validationFunction.js';
@@ -28,11 +28,7 @@ export const validateSettings = [
pinValidator('operatorKey'),
body('timeFormat').isString().isIn(['12', '24']).withMessage('Time format can only be "12" or "24"'),
body('language').isString().trim().notEmpty(),
- body('auxTimerNames')
- .isArray()
- .withMessage('auxTimerNames must be an array')
- // normalise to a fixed length array of trimmed, length capped strings
- .customSanitizer(normaliseAuxTimerNames),
+ body('auxTimerNames').isArray().withMessage('auxTimerNames must be an array').customSanitizer(sanitiseAuxTimerNames),
requestValidationFunction,
];
diff --git a/apps/server/src/app.ts b/apps/server/src/app.ts
index 395a35cc0..a3560b2ce 100644
--- a/apps/server/src/app.ts
+++ b/apps/server/src/app.ts
@@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser';
import cors from 'cors';
import express from 'express';
import { LogOrigin, SimpleDirection, SimplePlayback, runtimeStorePlaceholder } from 'ontime-types';
-import { normaliseAuxTimerNames } from 'ontime-utils';
+import { sanitiseAuxTimerNames } from 'ontime-utils';
import serverTiming from 'server-timing';
import { oscServer } from './adapters/OscAdapter.js';
@@ -206,7 +206,7 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb
* Module initialises the services and provides initial payload for the store
*/
const state = getState();
- const auxTimerNames = normaliseAuxTimerNames(getDataProvider().getSettings().auxTimerNames);
+ const [auxName1, auxName2, auxName3] = sanitiseAuxTimerNames(getDataProvider().getSettings().auxTimerNames);
eventStore.init({
clock: state.clock,
timer: state.timer,
@@ -222,27 +222,27 @@ export const startServer = async (): Promise<{ message: string; serverPort: numb
current: timerConfig.auxTimerDefault,
playback: SimplePlayback.Stop,
direction: SimpleDirection.CountDown,
- name: auxTimerNames[0] ?? '',
+ name: auxName1,
},
auxtimer2: {
duration: timerConfig.auxTimerDefault,
current: timerConfig.auxTimerDefault,
playback: SimplePlayback.Stop,
direction: SimpleDirection.CountDown,
- name: auxTimerNames[1] ?? '',
+ name: auxName2,
},
auxtimer3: {
duration: timerConfig.auxTimerDefault,
current: timerConfig.auxTimerDefault,
playback: SimplePlayback.Stop,
direction: SimpleDirection.CountDown,
- name: auxTimerNames[2] ?? '',
+ name: auxName3,
},
ping: 1,
});
- // seed the aux timer names onto the running service so they persist across commands
- auxTimerService.loadNames(auxTimerNames);
+ // AuxTimerService owns its own SimpleTimer instances, so the store above doesn't update them
+ auxTimerService.loadNames(getDataProvider().getSettings().auxTimerNames);
// initialise message service
messageService.init(eventStore.set, eventStore.get);
diff --git a/apps/server/src/classes/simple-timer/SimpleTimer.ts b/apps/server/src/classes/simple-timer/SimpleTimer.ts
index 8969b74ef..157e9c2a1 100644
--- a/apps/server/src/classes/simple-timer/SimpleTimer.ts
+++ b/apps/server/src/classes/simple-timer/SimpleTimer.ts
@@ -29,9 +29,6 @@ export class SimpleTimer {
};
}
- /**
- * Sets the custom name of the timer
- */
public setName(name: string): SimpleTimerState {
this.state.name = name;
return this.state;
diff --git a/apps/server/src/models/dataModel.ts b/apps/server/src/models/dataModel.ts
index 5bd188db8..e77c557d0 100644
--- a/apps/server/src/models/dataModel.ts
+++ b/apps/server/src/models/dataModel.ts
@@ -1,5 +1,5 @@
import { DatabaseModel, Rundown } from 'ontime-types';
-import { generateId, normaliseAuxTimerNames } from 'ontime-utils';
+import { generateId } from 'ontime-utils';
import { ONTIME_VERSION } from '../ONTIME_VERSION.js';
@@ -30,7 +30,7 @@ const dbModel: DatabaseModel = {
operatorKey: null,
timeFormat: '24',
language: 'en',
- auxTimerNames: normaliseAuxTimerNames(),
+ auxTimerNames: ['', '', ''],
},
viewSettings: {
overrideStyles: false,
diff --git a/apps/server/src/models/demoProject.ts b/apps/server/src/models/demoProject.ts
index 5dd5755d6..0bc1ad97c 100644
--- a/apps/server/src/models/demoProject.ts
+++ b/apps/server/src/models/demoProject.ts
@@ -1,5 +1,4 @@
import { DatabaseModel, OntimeView } from 'ontime-types';
-import { normaliseAuxTimerNames } from 'ontime-utils';
import { backstageRundown, broadcastRundown, stageRundown } from './demoRundowns.js';
@@ -30,7 +29,7 @@ export const demoDb: DatabaseModel = {
operatorKey: null,
timeFormat: '24',
language: 'en',
- auxTimerNames: normaliseAuxTimerNames(),
+ auxTimerNames: ['', '', ''],
},
viewSettings: {
dangerColor: '#ff7300',
diff --git a/apps/server/src/services/aux-timer-service/AuxTimerService.ts b/apps/server/src/services/aux-timer-service/AuxTimerService.ts
index affd60664..2437cabb4 100644
--- a/apps/server/src/services/aux-timer-service/AuxTimerService.ts
+++ b/apps/server/src/services/aux-timer-service/AuxTimerService.ts
@@ -1,5 +1,5 @@
import { RuntimeStore, SimpleDirection, SimplePlayback } from 'ontime-types';
-import { normaliseAuxTimerNames } from 'ontime-utils';
+import { sanitiseAuxTimerNames } from 'ontime-utils';
import { SimpleTimer } from '../../classes/simple-timer/SimpleTimer.js';
import { timerConfig } from '../../setup/config.js';
@@ -26,13 +26,11 @@ export class AuxTimerService {
}
/**
- * Applies custom names to the aux timers and broadcasts the change.
- * Names are given in aux timer order (index 0 is aux timer 1).
- * Used to seed the names at bootstrap and to keep them in sync
- * with the settings of the loaded project.
+ * Called at bootstrap and whenever the loaded project's settings change,
+ * so the running timers reflect the current project's aux timer names.
*/
loadNames(names?: string[]) {
- const [name1, name2, name3] = normaliseAuxTimerNames(names);
+ const [name1, name2, name3] = sanitiseAuxTimerNames(names);
const patch: AuxTimerStateUpdate = {
auxtimer1: this.aux1.setName(name1),
auxtimer2: this.aux2.setName(name2),
diff --git a/apps/server/src/services/project-service/ProjectService.ts b/apps/server/src/services/project-service/ProjectService.ts
index a2c01ae98..3c4a9b4a0 100644
--- a/apps/server/src/services/project-service/ProjectService.ts
+++ b/apps/server/src/services/project-service/ProjectService.ts
@@ -90,7 +90,7 @@ async function loadProject(projectData: DatabaseModel, fileName: string, rundown
// stop the runtime service
runtimeService.stop();
- // the aux timer names belong to the project, apply the ones from the newly loaded project
+ // AuxTimerService holds its own state, independent of the loaded project, so it needs to be updated explicitly
auxTimerService.loadNames(projectData.settings.auxTimerNames);
// load the rundown given by key otherwise load the first in the project
@@ -352,7 +352,7 @@ export async function patchCurrentProject(data: Partial) {
// we can pass some stuff straight to the data provider
await getDataProvider().mergeIntoData(rest);
- // the settings may contain new aux timer names, apply them and notify the clients
+ // AuxTimerService holds its own state, so a settings patch needs to be applied to it explicitly
if (rest.settings) {
auxTimerService.loadNames(getDataProvider().getSettings().auxTimerNames);
sendRefetch(RefetchKey.Settings);
diff --git a/packages/utils/index.ts b/packages/utils/index.ts
index 7f6b201b3..b0891f4fb 100644
--- a/packages/utils/index.ts
+++ b/packages/utils/index.ts
@@ -100,11 +100,7 @@ export {
export { isPlaybackActive } from './src/playback-utils/playbackstate.js';
// aux timers
-export {
- auxTimerNameMaxLength,
- normaliseAuxTimerNames,
- numberOfAuxTimers,
-} from './src/aux-timer-utils/auxTimerUtils.js';
+export { auxTimerNameMaxLength, sanitiseAuxTimerNames } from './src/aux-timer-utils/auxTimerUtils.js';
//Colour
export {
diff --git a/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts b/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts
index e2e3d3947..eee892e3b 100644
--- a/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts
+++ b/packages/utils/src/aux-timer-utils/auxTimerUtils.test.ts
@@ -1,31 +1,30 @@
-import { auxTimerNameMaxLength, normaliseAuxTimerNames, numberOfAuxTimers } from './auxTimerUtils.js';
+import { auxTimerNameMaxLength, sanitiseAuxTimerNames } from './auxTimerUtils.js';
-describe('normaliseAuxTimerNames()', () => {
+describe('sanitiseAuxTimerNames()', () => {
it('generates the default value when given nothing', () => {
- expect(normaliseAuxTimerNames()).toStrictEqual(['', '', '']);
+ expect(sanitiseAuxTimerNames()).toStrictEqual(['', '', '']);
});
- it('always returns an entry per aux timer', () => {
- expect(normaliseAuxTimerNames(['Speaker'])).toHaveLength(numberOfAuxTimers);
- expect(normaliseAuxTimerNames(['a', 'b', 'c', 'extra'])).toStrictEqual(['a', 'b', 'c']);
+ it('always returns exactly three entries', () => {
+ expect(sanitiseAuxTimerNames(['a', 'b', 'c', 'extra'])).toStrictEqual(['a', 'b', 'c']);
});
it('pads missing entries with an empty string', () => {
- expect(normaliseAuxTimerNames(['Speaker'])).toStrictEqual(['Speaker', '', '']);
+ expect(sanitiseAuxTimerNames(['Speaker'])).toStrictEqual(['Speaker', '', '']);
});
it('trims whitespace', () => {
- expect(normaliseAuxTimerNames([' Speaker ', '', ''])).toStrictEqual(['Speaker', '', '']);
+ expect(sanitiseAuxTimerNames([' Speaker ', '', ''])).toStrictEqual(['Speaker', '', '']);
});
it('caps the name length', () => {
const tooLong = 'a'.repeat(auxTimerNameMaxLength + 10);
- expect(normaliseAuxTimerNames([tooLong])[0]).toHaveLength(auxTimerNameMaxLength);
+ expect(sanitiseAuxTimerNames([tooLong])[0]).toHaveLength(auxTimerNameMaxLength);
});
it('falls back to defaults for malformed data', () => {
- expect(normaliseAuxTimerNames('not-an-array')).toStrictEqual(['', '', '']);
- expect(normaliseAuxTimerNames(null)).toStrictEqual(['', '', '']);
- expect(normaliseAuxTimerNames([42, {}, undefined])).toStrictEqual(['', '', '']);
+ expect(sanitiseAuxTimerNames('not-an-array')).toStrictEqual(['', '', '']);
+ expect(sanitiseAuxTimerNames(null)).toStrictEqual(['', '', '']);
+ expect(sanitiseAuxTimerNames([42, {}, undefined])).toStrictEqual(['', '', '']);
});
});
diff --git a/packages/utils/src/aux-timer-utils/auxTimerUtils.ts b/packages/utils/src/aux-timer-utils/auxTimerUtils.ts
index 19260634a..78d6b7ff4 100644
--- a/packages/utils/src/aux-timer-utils/auxTimerUtils.ts
+++ b/packages/utils/src/aux-timer-utils/auxTimerUtils.ts
@@ -1,22 +1,16 @@
-/** Number of aux timers available in ontime */
-export const numberOfAuxTimers = 3;
-
/** Maximum length of a user given aux timer name */
export const auxTimerNameMaxLength = 30;
-/**
- * Normalises user or file provided aux timer names into a
- * fixed length array of trimmed, length capped strings.
- * Missing or malformed entries fallback to an empty string,
- * which consumers render as the default label.
- * Used when parsing project files, when validating API payloads
- * and to generate the default value.
- */
-export function normaliseAuxTimerNames(maybeNames?: unknown): string[] {
- const source = Array.isArray(maybeNames) ? maybeNames : [];
-
- return Array.from({ length: numberOfAuxTimers }, (_, index) => {
- const value = source[index];
- return typeof value === 'string' ? value.trim().slice(0, auxTimerNameMaxLength) : '';
- });
+function sanitiseAuxTimerName(value: unknown): string {
+ return typeof value === 'string' ? value.trim().slice(0, auxTimerNameMaxLength) : '';
+}
+
+/**
+ * Ontime has three aux timers. Given whatever was found on disk or in a request body,
+ * returns a name for each of them, so callers never need to deal with a missing
+ * or malformed auxTimerNames (eg. a project file saved before this feature existed).
+ */
+export function sanitiseAuxTimerNames(names?: unknown): [string, string, string] {
+ const source = Array.isArray(names) ? names : [];
+ return [sanitiseAuxTimerName(source[0]), sanitiseAuxTimerName(source[1]), sanitiseAuxTimerName(source[2])];
}
diff --git a/packages/utils/src/rundown-utils/rundownUtils.mock.ts b/packages/utils/src/rundown-utils/rundownUtils.mock.ts
index 84e9b54cb..cab85e3f7 100644
--- a/packages/utils/src/rundown-utils/rundownUtils.mock.ts
+++ b/packages/utils/src/rundown-utils/rundownUtils.mock.ts
@@ -1,8 +1,6 @@
import type { DatabaseModel } from 'ontime-types';
import { EndAction, OntimeView, SupportedEntry, TimeStrategy, TimerType } from 'ontime-types';
-import { normaliseAuxTimerNames } from '../aux-timer-utils/auxTimerUtils.js';
-
export const demoDb: DatabaseModel = {
rundowns: {
default: {
@@ -344,7 +342,7 @@ export const demoDb: DatabaseModel = {
operatorKey: null,
timeFormat: '24',
language: 'en',
- auxTimerNames: normaliseAuxTimerNames(),
+ auxTimerNames: ['', '', ''],
},
viewSettings: {
dangerColor: '#ff7300',