mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 21:03:29 +00:00
feat(automation): make a successful fire visible
Every logger call in the automation module was a failure path, so a working automation and a misconfigured one looked identical: nothing happened either way, as far as the user could see. Two channels, because they answer different questions. A new AUTOMATION log origin says what happened and when. A coalesced socket message feeds a "last fired" column in the panel, which answers whether an automation is alive at all: one that stays blank while its neighbours tick is the clearest sign that a filter or a trigger is wrong. Flood control is the whole difficulty here. onClock fires every second and the logger queue holds 100 entries, so per-fire logging on a continuous lifecycle would evict everything else within two minutes. Those cycles log a single notice per load explaining the silence and nothing after; the rest dedupe inside a one second window so a rapid reload does not spam. The socket message still goes out for continuous cycles, throttled to once a second, because the panel needs it to show the automation is running. Log.tsx carried six copies of the same twelve line button. Adding a seventh origin was the moment to collapse them, so the next one is free. The client log store was also unbounded while the server queue is capped; it now holds 500 entries. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
This commit is contained in:
+8
-1
@@ -12,6 +12,7 @@ import { preventEscape } from '../../../../common/utils/keyEvent';
|
||||
import { isOnlyNumbers } from '../../../../common/utils/regex';
|
||||
import { isOntimeCloud } from '../../../../externals';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import useAppSettingsNavigation from '../../useAppSettingsNavigation';
|
||||
|
||||
const oscApiDocsUrl = 'https://docs.getontime.no/api/protocols/osc/';
|
||||
|
||||
@@ -32,6 +33,7 @@ export default function AutomationSettingsForm({
|
||||
oscInputState,
|
||||
isLoading,
|
||||
}: AutomationSettingsProps) {
|
||||
const { setLocation } = useAppSettingsNavigation();
|
||||
const {
|
||||
handleSubmit,
|
||||
reset,
|
||||
@@ -97,7 +99,12 @@ export default function AutomationSettingsForm({
|
||||
<span>- An automation is what to send: OSC and HTTP messages, or an action inside Ontime.</span>
|
||||
<span>- A trigger is when to send it. Triggers for a single event live in the event editor.</span>
|
||||
<span>- OSC Input tells Ontime to listen to messages on the specific port.</span>
|
||||
<ExternalLink href={oscApiDocsUrl}>See the docs</ExternalLink>
|
||||
<Info.Footer>
|
||||
<ExternalLink href={oscApiDocsUrl}>See the docs</ExternalLink>
|
||||
<Button variant='ghosted' size='small' onClick={() => setLocation('network__log')}>
|
||||
Automations report to the event log
|
||||
</Button>
|
||||
</Info.Footer>
|
||||
</Info>
|
||||
</Panel.Section>
|
||||
|
||||
|
||||
+5
@@ -1,3 +1,8 @@
|
||||
.muted {
|
||||
color: $muted-gray;
|
||||
}
|
||||
|
||||
.lastFired {
|
||||
font-size: $aux-text-size;
|
||||
color: $green-400;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Automation, AutomationDTO, NormalisedAutomation, Trigger } from 'ontime-types';
|
||||
import { Fragment, useMemo, useState } from 'react';
|
||||
import { Fragment, useEffect, useMemo, useState } from 'react';
|
||||
import { IoAdd, IoPencil, IoSparklesOutline, IoTrash } from 'react-icons/io5';
|
||||
|
||||
import { deleteAutomation } from '../../../../common/api/automation';
|
||||
@@ -10,6 +10,7 @@ import Info from '../../../../common/components/info/Info';
|
||||
import Tag from '../../../../common/components/tag/Tag';
|
||||
import { getLifecycleLabel } from '../../../../common/constants/timerLifecycle';
|
||||
import useAutomationSettings from '../../../../common/hooks-query/useAutomationSettings';
|
||||
import { useAutomationFired } from '../../../../common/stores/automationFired';
|
||||
import { summariseOutputs } from '../../../../common/utils/automationOutputs';
|
||||
import * as Panel from '../../panel-utils/PanelUtils';
|
||||
import AutomationForm from './AutomationForm';
|
||||
@@ -61,6 +62,7 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
};
|
||||
|
||||
const lifecyclesByAutomation = useMemo(() => groupTriggersByAutomation(triggers), [triggers]);
|
||||
const { fired } = useAutomationFired();
|
||||
|
||||
const arrayAutomations = Object.keys(automations);
|
||||
|
||||
@@ -105,10 +107,11 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
<Panel.Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th style={{ width: '35%' }}>Title</th>
|
||||
<th style={{ width: '25%' }}>Runs on</th>
|
||||
<th style={{ width: '15%' }}>Filter rule</th>
|
||||
<th style={{ width: '30%' }}>Title</th>
|
||||
<th style={{ width: '22%' }}>Runs on</th>
|
||||
<th style={{ width: '13%' }}>Filter rule</th>
|
||||
<th style={{ width: '15%' }}>Sends</th>
|
||||
<th style={{ width: '12%' }}>Last fired</th>
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -164,6 +167,9 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
))
|
||||
)}
|
||||
</Panel.InlineElements>
|
||||
<td>
|
||||
<LastFired at={fired[automationId]?.at} />
|
||||
</td>
|
||||
<Panel.InlineElements align='end' relation='inner' as='td'>
|
||||
<IconButton
|
||||
variant='ghosted-white'
|
||||
@@ -186,7 +192,7 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
})}
|
||||
{deleteError && (
|
||||
<tr>
|
||||
<td colSpan={5}>
|
||||
<td colSpan={6}>
|
||||
<Panel.Error>{deleteError}</Panel.Error>
|
||||
</td>
|
||||
</tr>
|
||||
@@ -198,3 +204,41 @@ export default function AutomationsList({ automations, triggers, enabledAutomati
|
||||
</Panel.Section>
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Shows how long ago an automation last ran.
|
||||
* An automation that never ticks while its neighbours do is the clearest signal
|
||||
* that something upstream of it, a filter or a trigger, is wrong
|
||||
*/
|
||||
function LastFired({ at }: { at?: number }) {
|
||||
const [, setTick] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (at === undefined) {
|
||||
return;
|
||||
}
|
||||
const interval = setInterval(() => setTick((value) => value + 1), 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [at]);
|
||||
|
||||
if (at === undefined) {
|
||||
return <span className={style.muted}>—</span>;
|
||||
}
|
||||
|
||||
return <span className={style.lastFired}>{formatElapsed(Date.now() - at)}</span>;
|
||||
}
|
||||
|
||||
function formatElapsed(elapsed: number): string {
|
||||
const seconds = Math.max(0, Math.floor(elapsed / 1000));
|
||||
if (seconds < 5) {
|
||||
return 'just now';
|
||||
}
|
||||
if (seconds < 60) {
|
||||
return `${seconds}s ago`;
|
||||
}
|
||||
const minutes = Math.floor(seconds / 60);
|
||||
if (minutes < 60) {
|
||||
return `${minutes}m ago`;
|
||||
}
|
||||
return `${Math.floor(minutes / 60)}h ago`;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user