The automation form had grown a lifecycle picker that created and deleted
global triggers behind the user's back. It broke the model the panel is built
on — an automation is what to send, a trigger is when — and left the global
triggers section describing itself as a place to rename things made elsewhere.
Triggers now belong only to the triggers list and the event editor. The
automation form edits a title, filters and outputs, and saves in one request:
the two-request save, its partial-failure recovery and the snapshot it
reconciled against are all gone with it. An automation with no global trigger
offers to make one from its own row, opening the trigger form with the
automation preselected, so the connection is still one click away without the
form pretending to own it.
Other changes in the same pass:
- the form uses the compact modal instead of the wide one. Nothing in it
justified 1800px, and output fields now pair up two to a row rather than
stretching across four columns
- a blank automation gets its own header button beside Start from recipe,
instead of being a footnote under the recipe list
- the settings form seeds itself from the query when it resolves, as the other
settings panels do. It was showing automations as OFF while they were on
- the filter operator list goes back to the three master offered, which makes
the note about not_contains unnecessary rather than explanatory
- comments that narrated the change rather than explaining the code
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
Review pointed out that reconciling triggers breaks if one of them is deleted
by someone else first. The form deletes sequentially and the server rejected a
delete for a trigger that was already gone, so the whole save failed — and
failed again on every retry, because the trigger the form still wants removed
can never be removed. Two operators with the settings panel open, or a project
import that replaces the automation block, is enough to reach it.
Fixing that in the form would have meant recognising "already gone" from an
error message, since the refusal comes back as a 400 like any other. The
server is the better place: deleteAutomation fifty lines below already returns
early for an id it cannot find, for exactly this reason, so the two delete
functions now agree with each other and with what DELETE is supposed to mean.
Nothing depended on the throw — the controller was its only caller and no test
covered it.
Verified end to end: with the automation form open and its lifecycle unticked,
another client deletes that trigger, and the save now completes and closes
rather than failing with an error the user cannot clear.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
Review found that syncTriggers diffed additions against the snapshot taken when
the form opened, so a save that failed part way through would, on the next
attempt, create a second trigger for every cycle it had already created. The
same held in reverse: deletes were replayed too, and the server rejects a
delete for a trigger that is already gone, so the retry died on its first
request.
The snapshot now advances as each request succeeds. It is still seeded at mount
rather than from the live prop — settings are polled, and a save must not
remove a trigger the user could not see — but once a trigger is created or
deleted it becomes part of what this form knows the server holds. A retry is
then left with only the outstanding work, and unticking a lifecycle whose
trigger was created by the failed attempt now removes it rather than orphaning
it.
Reproducing that turned up a second problem in the same path. A failed save
reports itself through setError('root'), which react-hook-form counts against
isValid, which disabled Save. The retry the error message asks for was
unreachable until the user edited some unrelated field to force revalidation —
and editing a field was also what made the duplicate reachable. A root error on
its own no longer blocks submitting, and a new attempt clears the previous one
rather than leaving it under a successful save.
Verified by intercepting the second trigger request: the failed save leaves one
trigger, the retry adds only the missing one and keeps the first trigger's id.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
The server refused to delete an automation while any global trigger pointed
at it, so removing one meant finding its triggers in a second list and
deleting each by hand first.
The refusal was protecting against dead data, but a trigger pointing at a
deleted automation is the dead data. Both are now written in a single patch,
so there is no window where one outlives the other and nothing to roll back.
An automation attached to an event is still refused: that reference lives in
the rundown, and removing it is an edit to the show rather than to the
automation settings.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AfDKsy6PE3Rbyt32Fg4YKf
* chore: migrate eslint to oxlint
* chore: migrate prettier to oxfmt
* chore: migrate typescript
* chore: toThrow should have a expected value
* chore: cast test value as Day
* chore: small title fix
* chore: mocks should be hoisted
* chore: incorrect async useage
* chore: test should be inside description
* chore: test sohuld include an expeced
* chore: oxfmt
---------
Co-authored-by: alex-Arc <omnivox@LAPTOP-RC5SNBVV.localdomain>
* crude UI
* add server functionalety
* better event trigger list
* fix default value
* remove log
* add trigger to recalculate whitelist
* use DTO type
* fix rebase
* one callback for delete and submit
* refactor
* add invalid description
* clean up commits
* cleanup
* refactor: find event triggers in `triggerAutomations`
* refactor: switch to flat array for event triggers
* prevent deleting a automation that is in use
* refactor
* refactor: extract EventEditorCustom
* move trigger options to separate file
* move trigger edit down a file level
* cleanup
* spelling
* refactor: ui review proposal
* change default value
* fix delete filter
---------
Co-authored-by: Carlos Valente <carlosvalente@pm.me>