mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-11 10:23:54 +00:00
feat(demo): ship the demo project with working automations
The demo had automations empty and switched off, so the feature stayed invisible to exactly the users most likely to be exploring. It was also inconsistent with the blank project default, where automations are on. Three automations, none of which touch the network. Two fire: an aux timer that runs with the event, on a global trigger, and a wrap-up message on an event level trigger, which is the only way per-event triggers get found by browsing rather than by reading the docs. The third is a fully formed OSC example with no trigger attached at all, so there is something realistic to read and edit without anything being sent anywhere. OSC input stays off: never open a listening socket the user did not ask for. The ids are hand written literals and nothing in the type system checks that a trigger resolves to an automation, so a test covers that, along with the promise that nothing bound to a trigger can reach off this machine. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import { isHTTPOutput, isOSCOutput, isOntimeEvent, timerLifecycleValues } from 'ontime-types';
|
||||
|
||||
import { parseAutomation } from '../../api-data/automation/automation.validation.js';
|
||||
import { demoDb } from '../demoProject.js';
|
||||
|
||||
/**
|
||||
* The demo ships with automations so the feature is visible on first run.
|
||||
* They are hand written literals: nothing in the type system checks that a trigger
|
||||
* resolves to an automation, or that the demo cannot reach out to the network.
|
||||
*/
|
||||
describe('demo project automations', () => {
|
||||
const { automations, triggers } = demoDb.automation;
|
||||
|
||||
it('is enabled, otherwise the automations are invisible', () => {
|
||||
expect(demoDb.automation.enabledAutomations).toBe(true);
|
||||
});
|
||||
|
||||
it('does not open a listening socket', () => {
|
||||
expect(demoDb.automation.enabledOscIn).toBe(false);
|
||||
});
|
||||
|
||||
it('keys every automation by its own id', () => {
|
||||
for (const [key, automation] of Object.entries(automations)) {
|
||||
expect(automation.id).toBe(key);
|
||||
}
|
||||
});
|
||||
|
||||
it('passes the same validation as a user created automation', () => {
|
||||
for (const automation of Object.values(automations)) {
|
||||
expect(() => parseAutomation(automation)).not.toThrow();
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves every global trigger to an automation', () => {
|
||||
for (const trigger of triggers) {
|
||||
expect(timerLifecycleValues).toContain(trigger.trigger);
|
||||
expect(Object.hasOwn(automations, trigger.automationId)).toBe(true);
|
||||
}
|
||||
});
|
||||
|
||||
it('resolves every event level trigger to an automation', () => {
|
||||
for (const rundown of Object.values(demoDb.rundowns)) {
|
||||
for (const entry of Object.values(rundown.entries)) {
|
||||
if (!isOntimeEvent(entry)) {
|
||||
continue;
|
||||
}
|
||||
for (const trigger of entry.triggers) {
|
||||
expect(timerLifecycleValues).toContain(trigger.trigger);
|
||||
expect(Object.hasOwn(automations, trigger.automationId)).toBe(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
it('never sends outside this machine, whatever the user presses', () => {
|
||||
// an automation only fires if something triggers it
|
||||
const boundIds = new Set<string>(triggers.map((trigger) => trigger.automationId));
|
||||
for (const rundown of Object.values(demoDb.rundowns)) {
|
||||
for (const entry of Object.values(rundown.entries)) {
|
||||
if (isOntimeEvent(entry)) {
|
||||
entry.triggers.forEach((trigger) => boundIds.add(trigger.automationId));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of boundIds) {
|
||||
for (const output of automations[id].outputs) {
|
||||
expect(isOSCOutput(output)).toBe(false);
|
||||
expect(isHTTPOutput(output)).toBe(false);
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
@@ -1,4 +1,4 @@
|
||||
import { DatabaseModel, OntimeView } from 'ontime-types';
|
||||
import { DatabaseModel, OntimeView, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
import { backstageRundown, broadcastRundown, stageRundown } from './demoRundowns.js';
|
||||
|
||||
@@ -76,11 +76,56 @@ export const demoDb: DatabaseModel = {
|
||||
label: 'PowerPoint Slide',
|
||||
},
|
||||
},
|
||||
/**
|
||||
* The demo ships with working automations so the engine is visible the first time
|
||||
* someone presses Play, rather than hidden behind an empty settings panel.
|
||||
*
|
||||
* Everything that actually fires is an Ontime action: the demo must not put traffic
|
||||
* on whatever network it happens to be opened on. The OSC entry is there to be read
|
||||
* and edited, and is deliberately left without a trigger.
|
||||
*
|
||||
* The ids are hand written and must match the map keys. Ids are only generated for
|
||||
* automations created through the DAO, so literals are safe here.
|
||||
*/
|
||||
automation: {
|
||||
enabledAutomations: false,
|
||||
enabledAutomations: true,
|
||||
// never open a listening socket without the user asking for it
|
||||
enabledOscIn: false,
|
||||
oscPortIn: 8888,
|
||||
triggers: [],
|
||||
automations: {},
|
||||
triggers: [
|
||||
{
|
||||
id: 'demo-trigger-aux',
|
||||
title: 'Demo: aux timer on start',
|
||||
trigger: TimerLifeCycle.onStart,
|
||||
automationId: 'demo-aux-timer',
|
||||
},
|
||||
],
|
||||
automations: {
|
||||
'demo-aux-timer': {
|
||||
id: 'demo-aux-timer',
|
||||
title: 'Demo: run Aux Timer 1 with the event',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
outputs: [
|
||||
{ type: 'ontime', action: 'aux1-set', time: '00:05:00' },
|
||||
{ type: 'ontime', action: 'aux1-start' },
|
||||
],
|
||||
},
|
||||
'demo-danger-message': {
|
||||
id: 'demo-danger-message',
|
||||
title: 'Demo: warn the stage at danger',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
// self labelled, so nobody mistakes it for something Ontime does on its own
|
||||
outputs: [{ type: 'ontime', action: 'message-set', text: 'Demo automation: please wrap up', visible: true }],
|
||||
},
|
||||
'demo-osc-example': {
|
||||
id: 'demo-osc-example',
|
||||
title: 'Demo: OSC to a lighting console (example, not wired up)',
|
||||
filterRule: 'all',
|
||||
filters: [],
|
||||
outputs: [{ type: 'osc', targetIP: '127.0.0.1', targetPort: 8000, address: '/ontime/go', args: '1' }],
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { SupportedEntry, TimeStrategy, EndAction, TimerType, Day, Rundown } from 'ontime-types';
|
||||
import { SupportedEntry, TimeStrategy, EndAction, TimerType, Day, Rundown, TimerLifeCycle } from 'ontime-types';
|
||||
|
||||
export const stageRundown: Rundown = {
|
||||
id: 'default',
|
||||
@@ -120,7 +120,16 @@ export const stageRundown: Rundown = {
|
||||
Audio_Notes: '1x Wireless Hand Held',
|
||||
PowerPoint_Name: 'HoldingSlide.pptx',
|
||||
},
|
||||
triggers: [],
|
||||
// demonstrates that an automation can be attached to a single event, which is
|
||||
// otherwise only discoverable by reading the docs. See demoProject.ts
|
||||
triggers: [
|
||||
{
|
||||
id: 'demo-event-trigger',
|
||||
title: 'Wrap up warning',
|
||||
trigger: TimerLifeCycle.onDanger,
|
||||
automationId: 'demo-danger-message',
|
||||
},
|
||||
],
|
||||
},
|
||||
fa593e: {
|
||||
id: 'fa593e',
|
||||
|
||||
Reference in New Issue
Block a user