mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-13 19:33:46 +00:00
refactor(report): a run becomes a report only when finished
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
This commit is contained in:
@@ -21,6 +21,7 @@ export const CSS_OVERRIDE = ['cssOverride'];
|
||||
export const CLIENT_LIST = ['clientList'];
|
||||
export const REPORT = ['report'];
|
||||
export const REPORT_RUNS = ['report', 'runs'];
|
||||
export const REPORT_OPEN_RUN = ['report', 'runs', 'open'];
|
||||
export const TRANSLATION = ['translation'];
|
||||
|
||||
// API URLs
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import axios from 'axios';
|
||||
import { OntimeReport, ShowRun, ShowRunSummary } from 'ontime-types';
|
||||
import { OntimeReport, OpenRun, ShowRun, ShowRunSummary } from 'ontime-types';
|
||||
|
||||
import { ontimeQueryClient } from '../../common/queryClient';
|
||||
import { REPORT, apiEntryUrl } from './constants';
|
||||
@@ -45,7 +45,23 @@ export async function fetchRun(id: string, options?: RequestOptions): Promise<Sh
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to close the run in progress, writing it to history immediately
|
||||
* HTTP request to fetch the run being recorded
|
||||
* @returns null when no run is in progress
|
||||
*/
|
||||
export async function fetchOpenRun(options?: RequestOptions): Promise<OpenRun | null> {
|
||||
try {
|
||||
const res = await axios.get(`${reportUrl}/runs/open`, { signal: options?.signal });
|
||||
return res.data;
|
||||
} catch (error) {
|
||||
if (axios.isAxiosError(error) && error.response?.status === 404) {
|
||||
return null;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request to finish the run in progress, which is what writes it to history
|
||||
*/
|
||||
export async function finishRun(): Promise<void> {
|
||||
await axios.post(`${reportUrl}/runs/finish`);
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { useQuery } from '@tanstack/react-query';
|
||||
import { ShowRun, ShowRunSummary } from 'ontime-types';
|
||||
import { OpenRun, ShowRun, ShowRunSummary } from 'ontime-types';
|
||||
import { MILLIS_PER_HOUR } from 'ontime-utils';
|
||||
|
||||
import { REPORT_RUNS } from '../api/constants';
|
||||
import { fetchRun, fetchRuns } from '../api/report';
|
||||
import { REPORT_OPEN_RUN, REPORT_RUNS } from '../api/constants';
|
||||
import { fetchOpenRun, fetchRun, fetchRuns } from '../api/report';
|
||||
|
||||
/**
|
||||
* Run history for the current project, optionally scoped to a rundown
|
||||
@@ -33,8 +33,18 @@ export function useRun(id: string | null) {
|
||||
return { data, status };
|
||||
}
|
||||
|
||||
/** The run currently in progress, if any */
|
||||
export function useOpenRun(): ShowRunSummary | null {
|
||||
const { data } = useRuns();
|
||||
return data.find((run) => run.endedAt === null) ?? null;
|
||||
/**
|
||||
* The run currently being recorded, if any.
|
||||
* A run in progress lives in memory on the server until it is finished, so it
|
||||
* cannot be found in the run history and needs its own query.
|
||||
*/
|
||||
export function useOpenRun(): OpenRun | null {
|
||||
const { data } = useQuery<OpenRun | null>({
|
||||
queryKey: REPORT_OPEN_RUN,
|
||||
queryFn: ({ signal }) => fetchOpenRun({ signal }),
|
||||
placeholderData: (previousData, _previousQuery) => previousData,
|
||||
staleTime: MILLIS_PER_HOUR,
|
||||
});
|
||||
|
||||
return data ?? null;
|
||||
}
|
||||
|
||||
@@ -42,8 +42,8 @@ export default function ReportSettings() {
|
||||
</Panel.SubHeader>
|
||||
<Panel.Divider />
|
||||
<Panel.Paragraph>
|
||||
A run is created the first time an event is started, and is added to history once playback stops. Every
|
||||
run keeps its own snapshot of the schedule, so editing the rundown later does not change past reports.
|
||||
Recording starts with the first event of a show and is saved here when you finish the run. Every report
|
||||
keeps its own snapshot of the schedule, so editing the rundown later does not change past reports.
|
||||
</Panel.Paragraph>
|
||||
<RunsList runs={runs} selectedRunId={selectedRunId} onSelect={setSelectedRunId} />
|
||||
</Panel.Card>
|
||||
|
||||
@@ -6,7 +6,6 @@ import { deleteRun, renameRun } from '../../../../../common/api/report';
|
||||
import { maybeAxiosError } from '../../../../../common/api/utils';
|
||||
import IconButton from '../../../../../common/components/buttons/IconButton';
|
||||
import Input from '../../../../../common/components/input/input/Input';
|
||||
import Tag from '../../../../../common/components/tag/Tag';
|
||||
import { preventEscape } from '../../../../../common/utils/keyEvent';
|
||||
import { cx } from '../../../../../common/utils/styleUtils';
|
||||
import * as Panel from '../../../panel-utils/PanelUtils';
|
||||
@@ -77,14 +76,13 @@ export default function RunsList({ runs, selectedRunId, onSelect }: RunsListProp
|
||||
<th>Started</th>
|
||||
<th>Drift</th>
|
||||
<th />
|
||||
<th />
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{runs.length === 0 && (
|
||||
<Panel.TableEmpty
|
||||
title='No runs yet'
|
||||
description='A run is created the first time an event is started, and is added to history once playback stops.'
|
||||
title='No reports yet'
|
||||
description='Recording starts with the first event of a show. Use Finish run in the editor to save the report here.'
|
||||
/>
|
||||
)}
|
||||
{runs.map((run) => {
|
||||
@@ -123,7 +121,6 @@ export default function RunsList({ runs, selectedRunId, onSelect }: RunsListProp
|
||||
<td>{run.rundownTitle}</td>
|
||||
<td>{new Date(run.startedAt).toLocaleString()}</td>
|
||||
<td>{formatDrift(run.summary.drift, run.summary.eventsRun)}</td>
|
||||
<td>{run.endedAt === null && <Tag variant='active'>Ongoing</Tag>}</td>
|
||||
<Panel.InlineElements align='end' relation='inner' as='td' onClick={(event) => event.stopPropagation()}>
|
||||
{!isRenaming && (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user