An entry revision advances on every change to that entry, which makes it a
cheap marker for what has moved on in a rundown. The client did not maintain
it: an optimistic entry carried the old revision, so the entry the server
returned always differed from ours by that one field.
That difference is enough to defeat structural sharing. A new entries object
recomputes the rundown metadata, which fires the effect behind the list, which
re-renders every visible row. Editing one title on a fourteen entry rundown
cost eight structural and sixty-one attribute mutations, twice over.
- optimistic patches apply the same revision bump the server does, so an
unchanged entry keeps its identity and a refetch costs nothing to render
- the rundown metadata memo keys on the fields the derivation reads, so a
change of rundown revision alone no longer churns the list
- editing an entry now reads the server response, which is the authority on
what was applied, and only writes when it differs from what we predicted
Measured on the same edit: eight to four structural, sixty-one to forty-one
attribute mutations. Both changes are needed, neither helps on its own.
Two bugs found on the way. A swap stored the pre increment revision, so
swapped entries were indistinguishable from untouched ones. The optimistic
batch edit spread the request envelope onto the entry rather than the patch,
so it never applied the edit it was predicting.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a5cbVjNC5XXF88b2PkUCa
Two independent issues in the revision strategy.
The client guard only skipped a refetch when the revision matched exactly,
while its comment described skipping anything not newer. A message which
arrives after a refetch has already brought in a later revision forced a
needless refetch. Compare with <= so the guard does what it claims.
Renaming a rundown re-initialised it, which stops playback. Renaming the
loaded rundown during a show therefore stopped the show. A title has no
bearing on the schedule, so it now goes through an ordinary transaction:
the title is persisted, the revision bumped and clients notified, with no
runtime involvement. Renaming to the current title is a no-op.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a5cbVjNC5XXF88b2PkUCa
Only the editor and the cuesheet can be pointed at a background rundown.
Everything else always works against the rundown being played, so it has
no business taking part in a rundown scope.
Split the readers by that assumption:
- useRundownById holds the query and the shared derivation
- useRundown reads from the enclosing scope, for the editor and cuesheet
- useLoadedRundown resolves the loaded rundown directly, for the viewers,
the operator, app settings and the overview
This drops the app wide scope provider, so views which read no rundown
(studio, project info) no longer subscribe to the rundown list, and a
scope becomes what it claims to be: a panel concern which throws when
used outside a panel.
It also fixes the overview, which reads runtime ids (the playing group,
the next flag). Those only exist in the loaded rundown, so resolving them
against a scope returned nothing while a background rundown was open in
the cuesheet.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a5cbVjNC5XXF88b2PkUCa
- extract getRundownCacheKey, the fallback to the bootstrap alias for an
unresolved rundown id was spelled out in two readers and missing in the
selection store, which could not resolve shift/ctrl ranges during startup
- an unresolved target is no longer reported as the loaded rundown, it was
briefly enabling run mode and playback following before a rundown existed
- only the reader which bootstrapped drops the /current alias, a reader on
another rundown was evicting a cache others still relied on
- the selection store returns a new Set on toggle and unselect, components
selecting the set by reference never saw the change
- useSelectAndRevealEntry takes its rundown from the scope, callers were
threading an id for no other reason
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a5cbVjNC5XXF88b2PkUCa
The editor and cuesheet had three overlapping notions of which rundown
they were showing: loaded-only data hooks, a prop-drilled RundownSource,
and a selection store which read the loaded rundown out of the query
cache. Rendering two rundowns at once was not possible.
Introduce a rundown scope: a context carrying the rundown identity
(id, isLoaded and a selection store) which the utility hooks read from.
useRundown, useEntry and friends keep their signatures and call sites,
they simply resolve their rundown from the enclosing scope. The app
mounts a scope at the root which follows the loaded rundown, so runtime
views are unaffected; the editor and cuesheet mount an editable scope
which also binds entry actions to the same rundown.
- useEventSelection becomes a per-scope store factory, so panels hold
independent cursors and the store is handed its rundown instead of
reaching for the loaded one
- the cuesheet selection is generalised to useRundownSelection with a
namespace, so each surface persists its own choice
- the clipboard records the rundown an entry was copied from, leaving
room for pasting across rundowns
- removes useScopedRundown and the prop-drilled RundownSource
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a5cbVjNC5XXF88b2PkUCa
Search every text field with optional filters, keep result selection stable, and expose clearer match context and counts. Cover query parsing, matching, mixed-entry indexing, and the reveal flow.
* 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
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
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