feat(report): persistent show run history

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
This commit is contained in:
Claude
2026-08-08 11:08:28 +00:00
parent fb559ed9da
commit 3c2875a956
32 changed files with 2144 additions and 172 deletions
+50 -1
View File
@@ -1,5 +1,5 @@
import axios from 'axios';
import { OntimeReport } from 'ontime-types';
import { OntimeReport, ShowRun, ShowRunSummary } from 'ontime-types';
import { ontimeQueryClient } from '../../common/queryClient';
import { REPORT, apiEntryUrl } from './constants';
@@ -24,3 +24,52 @@ export async function deleteAllReport() {
await axios.delete(`${reportUrl}/all`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}
/**
* HTTP request to fetch the run history, optionally scoped to a rundown
*/
export async function fetchRuns(rundownId?: string, options?: RequestOptions): Promise<ShowRunSummary[]> {
const res = await axios.get(`${reportUrl}/runs`, {
signal: options?.signal,
params: rundownId ? { rundownId } : undefined,
});
return res.data;
}
/**
* HTTP request to fetch a single run, including its per event data
*/
export async function fetchRun(id: string, options?: RequestOptions): Promise<ShowRun> {
const res = await axios.get(`${reportUrl}/runs/${id}`, { signal: options?.signal });
return res.data;
}
/**
* HTTP request to fetch the most recently closed run, optionally scoped to a rundown
* @returns null if there is no closed run yet
*/
export async function fetchLatestRun(rundownId?: string, options?: RequestOptions): Promise<ShowRun | null> {
try {
const res = await axios.get(`${reportUrl}/runs/latest`, {
signal: options?.signal,
params: rundownId ? { rundownId } : undefined,
});
return res.data;
} catch (error) {
if (axios.isAxiosError(error) && error.response?.status === 404) {
return null;
}
throw error;
}
}
export async function renameRun(id: string, label: string): Promise<ShowRun> {
const res = await axios.patch(`${reportUrl}/runs/${id}`, { label });
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
return res.data;
}
export async function deleteRun(id: string) {
await axios.delete(`${reportUrl}/runs/${id}`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}