Commit Graph

2201 Commits

Author SHA1 Message Date
Claude cdde054b14 fix(automation): review fixes across the automations rework
Reviewing the branch turned up defects, several of them worse than the
problems the original commits set out to solve.

Destructive: deleting an automation removed its global triggers and only then
discovered a rundown event also referenced it. The delete failed, the user
cancelled, and their triggers were gone for good. The server reports trigger
references before event ones, so this was reachable whenever both existed.
Deleted triggers are now recreated when the delete is refused.

Silently wrong: a template read the project from disk while writes are
debounced by three seconds, so "make some automations, save them as a
template" could produce a template without them. Verified by hand: an
automation created milliseconds earlier now appears.

The demo shipped half a pair. The danger warning set a stage message visible
and nothing ever cleared it, so from the first event that hit its danger
window the message covered the countdown for the rest of the session. The
recipe library ships the clearing counterpart; the demo now does too, and the
whole cycle is verified against a running server.

Flood control undid itself. resetAutomationLogState ran on every onLoad, and
roll mode loads at every event boundary, so the "logging suppressed" notice
was re-emitted once per cue: exactly the flooding it exists to prevent. It
also wiped the throttle for onLoad and onStop immediately before writing to
it. Reset now happens on stop only, outside the early returns that made the
first attempt at this a no-op.

Trigger reconciliation diffed a mount-time selection against a live prop.
Settings are polled, so a trigger created in another tab while the form was
open would be deleted by a save that never saw it. It now diffs against the
snapshot, and says which triggers a save will remove rather than removing
several same-lifecycle triggers silently.

Also: a rundowns template no longer carries a phantom empty rundown from
makeNewProject; the partial-duplicate endpoint returns the name it actually
used, since collisions get renamed; the template flow invalidates the project
list rather than relying on a refetch on mount; the last-fired label drops to
a one minute cadence instead of holding a 1Hz timer per automation forever;
e2e cleanup moved to afterEach so a mid-test failure stops leaking state.

The e2e spec has now been run against a real server, green on both the demo
project and a blank one, leaving no residue.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
2026-08-08 16:43:29 +00:00
Claude 32734de679 feat(project): save part of a project as a template
Automations were already portable, contrary to the original framing: project
download carries them and the partial load already had an "Automation
Settings" toggle. What was missing is the other half of that loop. To share a
set of automations you had to hand over an entire show.

Save as template is the inverse of partial load: it writes a new project file
holding only the sections you pick. Automations, custom fields, URL presets
and view settings all become templates, and a template is an ordinary project
file, so it downloads, uploads and partial loads with no new format and no new
concepts.

createProjectWithPatch already builds exactly the right object, but it routes
through loadProject and would switch the running project. Saving a template
must not pull an operator out of the show they are running, so this writes the
file without loading it, the way duplicateProjectFile already does.

The automations panel gets the loop as a Share menu: save these automations as
a template, or go load one. The form says plainly that applying an automation
template replaces rather than merges, since triggers and automations are
coupled and safeMerge swaps the whole block, and that a template without
rundowns does not carry the events that point at its automations.

Adds the first e2e coverage automations have had.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
2026-08-08 15:41:57 +00:00
Claude fc8819cea6 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
2026-08-08 11:10:16 +00:00
Claude e7379c9add refactor(automation): explain a refused deletion instead of dumping the error
Deleting an automation had no confirmation at all, and when the server refused
because something still referenced it, the raw message landed in a stray row
underneath the table.

Deletion now confirms first, and names what it will take with it. Global
triggers are ours to clean up, so the dialog offers to delete them along with
the automation. A reference from a rundown event is not: editing rundown data
from a settings screen would be surprising and hard to undo, so that stays a
block, with the server's message and a pointer to the event editor.

This matters more once the demo project ships with automations, since those
are referenced by definition and every user who tries to remove one would
otherwise meet the raw error.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
2026-08-08 11:08:29 +00:00
Claude 5ea3845f0d 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
2026-08-08 11:06:11 +00:00
Claude f1bf8e8bee feat(automation): add a recipe library
The panel started empty with no examples, so the first thing a new user saw
was a form that assumes they already know what OSC, a lifecycle and a template
string are. Eight recipes now cover the common integrations plus three that
need no external software at all, so the library is useful on a bare laptop.

A recipe is not a new kind of object. Installing one posts an ordinary
automation and its triggers through the same endpoints the form uses, then
opens it in the editor: seeing that it is editable is the point.

Every external target defaults to this machine, so a mis-click cannot put
traffic on a venue network, and a test asserts that along with the rest of
the server's automation contract, since recipes are constants that would
otherwise rot silently into something that 400s on install.

OSC recipes are hidden in the cloud build, where OSC output is skipped.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
2026-08-08 11:00:44 +00:00
Claude 8ed708138d feat(automation): pick lifecycles from the automation itself
Getting one automation to fire took two modals and two names: create the
automation, then create a separate trigger with a title the user has no use
for. The automation form now carries a "Runs on" selection and reconciles the
global triggers on save, so a working automation is one form away.

The triggers card stays for the cases it is actually good at, naming triggers
and pointing several at one automation, and says so.

Saving now spans two requests. A create that succeeds followed by a failed
trigger sync keeps the automation id, so retrying edits the automation rather
than creating a second one, and the form stays open with the partial state
reported rather than closing on a half-applied save.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
2026-08-08 10:56:07 +00:00
Claude 0f9e436576 refactor(automation): make the automations panel legible at a glance
The list showed counts rather than meaning, and one column was mislabelled
"Trigger rule" while rendering the filter rule. The form buried the test and
delete actions inside four fixed grids, and the Test buttons discarded their
result entirely, so a failing output looked identical to a working one.

- list rows now say when an automation runs, what it sends, and flag the two
  silent misconfigurations: an automation with no triggers and one with no outputs
- outputs and filters render as cards with a shared header, replacing the fixed
  grids and the &nbsp; spacer hack used to fake a label-height cell
- test results are reported inline, keyed by field array id so removing an
  output cannot leave feedback on the wrong row
- filters gain a plain language summary, and the filter rule only shows when
  there is more than one filter to combine
- lifecycle labels are shared with the rundown event editor, which was showing
  raw enum values, and it now shows what the linked automation sends
- triggers pointing at a deleted automation say so instead of rendering an
  empty tag; the duplicates warning describes what actually happens

not_contains stays out of the operator list: the type and the runtime support
it but the server validation list omits it, so it cannot be saved.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpbLJVVT26tzWkduck1M9H
2026-08-08 10:53:27 +00:00
Carlos Valente fb559ed9da refactor(progress bar): reset when event changes 2026-08-05 15:30:22 +02:00
Carlos Valente 24be49ef66 refactor(settings): polish settings UI 2026-08-02 13:44:28 +02:00
Carlos Valente f0c284b708 refactor(link): add row presentation variant 2026-08-02 13:44:28 +02:00
Carlos Valente 9e42f18299 feat(settings): add searchable navigation 2026-08-02 13:44:28 +02:00
Carlos Valente bdd815678b refactor(ui): extend link and modal variants 2026-08-02 13:44:28 +02:00
Carlos Valente 12fc76b8ff refactor(op): simplify UI elements 2026-08-02 12:55:49 +02:00
Carlos Valente 8ed0a1689e refactor(op): clarify settings text 2026-08-02 12:55:49 +02:00
Carlos Valente ba18b8dba7 refactor(ui): rundown title UI polish 2026-08-02 12:55:49 +02:00
Carlos Valente 608247157b refactor(rundown): cap rundown names to 64 characters 2026-08-02 12:55:49 +02:00
Carlos Valente 514b6c3a02 refactor(navigation): custom params indicators only for current view 2026-08-02 12:55:49 +02:00
Carlos Valente 2050735014 refactor(ui): title alignment is logo aware 2026-08-02 12:55:49 +02:00
Carlos Valente 76b1341432 refactor(logo): upload on submit form 2026-08-02 10:39:58 +02:00
Carlos Valente c7422fc7c7 refactor(logo): improve flow for managing logo 2026-08-02 10:39:58 +02:00
Carlos Valente c93b10cea9 chore: bump deepmerge-ts package 2026-08-02 10:39:58 +02:00
Carlos Valente 8a7f9530d2 chore: bump nanoid package 2026-08-02 10:39:58 +02:00
Carlos Valente 94d54529ee refactor(views): consistent loading and empty states 2026-08-01 16:25:14 +02:00
Carlos Valente f7535651f6 refactor(info): improve UI consistency and polish 2026-08-01 14:41:39 +02:00
Carlos Valente 5cf36f049a refactor(modal): allow wider modals 2026-07-25 14:28:45 +02:00
Carlos Valente c6248b0c72 refactor(import): improve preview UI 2026-07-25 14:28:45 +02:00
Alex Christoffer Rasmussen 2a890cf2b3 fix: issue where a countToEnd would lead to incorrect expected times (#2149)
* fix: issue where a count-to-end would lead to incorrect expected times

* fix: include add time in overtime when countToEnd

* fix: ui and server use same calculation for expected end
v4.11.0
2026-07-25 11:22:24 +02:00
Carlos Valente a8c611911d refactor(rundown): new entries are appended to rundown 2026-07-22 07:17:39 +02:00
Carlos Valente 8363f06a5c chore(rundown): improve documentation of insert logic 2026-07-22 07:17:39 +02:00
will@t1d.dev 5f040092cb fix: timer-legacy view not served in desktop distributions
The server serves html/timer-legacy.html and html/login.html from disk
at runtime, relative to the bundled server. The Docker image copies
these files but the electron packaging did not, so the view 404ed in
all desktop distributions.

Additionally, AppImages mount at /tmp/.mount_*, a hidden directory.
Express sendFile refuses paths containing dot-segments by default
(returns 404 without touching disk), so the view failed on Linux even
with the file packaged. Allow dotfiles for this route; the request
path is fixed so no user input is affected.
2026-07-19 10:02:45 +02:00
Carlos Valente 347c748dd9 feat(rundown): add shortcut to jump to current element 2026-07-18 17:03:02 +02:00
Carlos Valente f234a1f892 fix(rundown): prevent skipped events from influencing day calculations 2026-07-18 16:52:36 +02:00
Carlos Valente 0c5e87b7e7 fix(ui): prevent warning indicator from overflowing container 2026-07-18 16:52:36 +02:00
Carlos Valente 64082ceac0 refactor(rundown): improve visibility of count to end 2026-07-18 16:45:55 +02:00
Carlos Valente f598e8dab1 refactor(time control): improve handling of overtime and count to end 2026-07-18 16:45:55 +02:00
Carlos Valente 0c84a3ff9e fix: delays account for midnight 2026-07-18 16:45:55 +02:00
Carlos Valente c8760b5e9c bump version to 4.11.0 2026-07-18 15:24:25 +02:00
Claude 34360a5b8f feat(import): add merge strategy and new-rundown destination to spreadsheet import
Co-authored-by: Carlos Valente <34649812+cpvalente@users.noreply.github.com>
2026-07-17 22:26:19 +02:00
Claude 5e6debcce0 fix(views): center timer digits with flexbox for embedded browser compatibility
The Stage Timer and PiP Timer views relied on `align-content: center` to
vertically center the timer digits inside `.timer-container`, which is a
plain block element. Aligning children of a block container via
`align-content` only works in Chromium 123+ (Firefox 125+, Safari 17.4+).

Embedded browsers such as vMix Browser Input (CEF V115) and other older
CEF/Chromium-based production tools ignore the property, so the digits
fall back to the top of the container while every other element renders
correctly.

Make `.timer-container` a real flex column and center with
`justify-content: center`, which is universally supported and matches the
centering approach used elsewhere in the codebase. The removed
`justify-self`/`align-self` were no-ops on a full-width flex item.

Fixes #2126

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014T6ENZ3r6JXZb1fpYw2oNY
2026-07-17 20:59:49 +02:00
Carlos Valente c191786e50 refactor(navigation): clear params from editor drawer 2026-07-17 15:50:42 +02:00
Carlos Valente 6911e37c18 refactor(automation): improve automation UX 2026-07-17 10:53:45 +02:00
Claude 86b2132cdc feat(automation): expose group title and allow setting secondary text
Surface {{groupNow.*}} template variables (title, note, colour, times,
custom fields) in the automation template autocomplete so events inside a
group can reference their group. The runtime store already carries
groupNow, so substitution and filters worked already; this makes it
discoverable.

Extend the message-secondary action with an optional text field so an
automation can set the secondary message content, not just its source.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LZYPZVdZLWowU7DyzGkWyy
2026-07-17 10:53:45 +02:00
Carlos Valente 25f0dba83a chore: add codegraph to gitignore 2026-07-16 10:46:10 +02:00
Carlos Valente daa032e92c ux(finder): add navigation shortcuts 2026-07-14 12:04:44 +02:00
Carlos Valente ae65ec97d6 fix(shortcut): correct shortcut to delete 2026-07-14 12:04:44 +02:00
Carlos Valente e7e4302100 refactor(shortcuts): improve readability of info element 2026-07-14 12:04:44 +02:00
Carlos Valente 7e7d35980a refactor(op): improve status recognition 2026-07-14 11:58:59 +02:00
Carlos Valente 5001dda284 feat(op): add group visual relation 2026-07-14 11:58:59 +02:00
Carlos Valente a75d35bdc0 refactor(op): improve delay indicator styles 2026-07-14 11:58:59 +02:00