Progress is kept in memory while a show runs and written once, as a
complete report, when the operator finishes the run. The stored history
therefore never contains a partial run.
- Nothing writes on the cue path. triggerReportEntry only updates memory,
so the sidecar is untouched between the first event and finish.
- Removes the write coalescing added for the previous model: with a
single write per run there is nothing to debounce, so the timer, the
dirty flag, the flush export and the shutdown hook all go, and app.ts
returns to its previous state.
- ShowRun.endedAt is no longer nullable. A stored run is finished by
definition, so recovery of runs left open by a crash is gone along with
the concept, and the run list drops its ongoing state.
- Adds GET /report/runs/open for the editor indicator. /runs now means
finished reports only, which is also the contract Cloud sees.
- A run whose rundown was deleted before it finished now reports the
events that actually ran as its planned count, rather than zero.
Known gap: RestorePoint carries playback state only, so a crash mid show
loses the in progress report. The stored history is unaffected.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
Cuts the risk surface of the run history feature while keeping what it
delivers.
Stability:
- Sidecar writes were happening on every event stop, and each write
serialised the project's entire history. That put work proportional to
everything that ever ran onto the show critical path, growing without
bound. Writes during a run are now coalesced, with immediate writes for
anything a user did and a flush on finish, project change and shutdown.
- The editor no longer carries any of this feature's code. The per event
last run chip is gone, so RundownEventChip, RundownEventInner and
useProjectRundowns are back to their previous state. Each rundown row
had gained two extra query subscriptions, one of them polling.
- closeRun is no longer called from runtime.service, so the timer path is
untouched. Ending a run is now explicit, which also means a mid show
stop and restart no longer splits one show across two runs.
Discovery:
- A run indicator in the editor overview appears only while a run is
being recorded. It shows when recording started and carries the Finish
action, then links to the report. One component with one subscription,
rather than anything per row.
Smaller:
- report.parser drops from exhaustive validation of a file we write
ourselves to a shallow shape check; the failure mode is unchanged.
- getCombinedReport returns to its original shape, keeping only the
snapshot read that report accuracy depends on.
- Removes getLatestRun and GET /runs/latest, which only existed for the
chip, and hand rolled refetches the api layer already covers.
Production diff is down from ~1460 to ~1290 lines, and the four floating
promises the previous revision introduced are gone.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
Review of the run history feature turned up four defects, all in the
code that feature introduced.
- Runs were dated with a time of day rather than a wall clock instant.
`clock` and `rundown.actualStart` are millis since midnight, so every
run in the history list rendered as 1 January 1970, and getLatestRun
ordered runs incorrectly across days: a run at 09:00 today sorted below
one at 20:00 last week, so the "last run" chip and /runs/latest could
return an older run. Runs now take `_startEpoch`, the instant the show
began, and the type documents the invariant.
- A stop arriving with no run open is ignored. Loading a project stops
playback and reinitialises reporting in the same tick, and the trailing
stop was attributing the previous project's event to the new one.
- Run summaries counted planned events from the loaded rundown rather
than the rundown the run belongs to, so switching rundowns mid-run gave
the summary a denominator from a different rundown. A run whose rundown
has since been deleted now keeps its last known count instead of
dropping to zero.
- Recovery of runs left open by a crash only closed the first one. Any
others stayed dangling forever: permanently shown as ongoing and never
eligible as the latest run.
Also replaces the raw ISO default run label with a readable local date
and time, and adds a batching upsertRuns to the store so recovery writes
the sidecar once rather than racing one write per run.
Each fix has a regression test, verified to fail without the change.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
Turns the reporter from a live-only, single-run curiosity into a
persistent, per-project, per-rundown history of runs.
- Extend OntimeEventReport with a schedule snapshot (scheduledStart,
scheduledDuration) taken when an event starts, plus playCount. Reports
are now a record that survives later rundown edits, rather than a live
join against the current rundown.
- Add a run lifecycle: a run opens on the first event start and closes on
a full stop, archiving the previous run to history so the next start
begins fresh.
- Persist runs to a per-project sidecar file (services/report-service),
patterned on the existing restore-service, so a crash or restart no
longer loses the whole show's record. Runs are scoped by rundownId for
multi-rundown projects, cascade-deleted with their rundown or project,
renamed alongside a project rename, and deliberately not copied when a
project is duplicated.
- Extract the over/under/on-time variance and run summary maths shared by
the rundown chip, the report settings panel, and the server into
ontime-utils (getEventVariance, getRunSummary, countPlannedEvents).
- Extend the report API with run history endpoints (list, get, latest,
rename, delete) while keeping GET /report's existing shape so Companion
and HTTP automations are unaffected.
- Replace the settings report table with a run browser: a list of runs
with a rundown filter, inline rename, delete, and a detail view with
per-run summary stats and CSV export.
- Add a third, muted state to the rundown event chip: an event with
nothing in the current run yet previews how it went last time.
Fixes a real bug found while testing the new store: emptyStore() was a
shared object, so its runs array leaked mutations across project loads.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
* 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 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.
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