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:
Claude
2026-08-08 11:06:11 +00:00
parent f1bf8e8bee
commit 5ea3845f0d
12 changed files with 337 additions and 109 deletions
+13 -1
View File
@@ -1,4 +1,5 @@
import type { Client } from '../../definitions/Clients.type.js';
import type { TimerLifeCycle } from '../../definitions/core/TimerLifecycle.type.js';
import type { Log } from '../../definitions/runtime/Logger.type.js';
import type { RuntimeStore } from '../../definitions/runtime/RuntimeStore.type.js';
import type { MaybeNumber } from '../../utils/utils.type.js';
@@ -17,6 +18,7 @@ export enum MessageTag {
Log = 'log',
RuntimeData = 'runtime-data',
Refetch = 'refetch',
AutomationFired = 'automation-fired',
}
// CLIENT TO SERVER
@@ -36,6 +38,15 @@ type ListClientPacket = {
};
type RuntimePacket = { tag: MessageTag.RuntimeData; payload: Partial<RuntimeStore> };
/**
* Reports that an automation ran, so clients can show it is alive.
* Coalesced server side: high frequency lifecycles do not send one of these per fire.
*/
type AutomationFiredPacket = {
tag: MessageTag.AutomationFired;
payload: { automationId: string; cycle: TimerLifeCycle };
};
type RefetchPacket = {
tag: MessageTag.Refetch;
payload: {
@@ -58,4 +69,5 @@ export type WsPacketToClient =
| LogPacket
| ListClientPacket
| RuntimePacket
| RefetchPacket;
| RefetchPacket
| AutomationFiredPacket;
@@ -19,6 +19,7 @@ export type LogMessage = {
};
export enum LogOrigin {
Automation = 'AUTOMATION',
Client = 'CLIENT',
Playback = 'PLAYBACK',
Rx = 'RX',