refactor(report): finish via playback, one file per run

Uses the Finish the app already has, and stops rewriting the whole
history every time a show ends.

- The Go button already reads "Finish" on the last event and calls stop
  (playbackControl.utils). Closing the run hangs off that instead of a
  second Finish button of its own, so POST /report/runs/finish and the
  button in the overview are both gone. The run indicator stays as the
  way the feature is found, now a passive status that opens the reports
  panel.
- Reports move from one file per project to a directory per project with
  one file per run. Finishing a show wrote the entire history back to
  disk, which grew with every show ever recorded; it now writes only the
  run that just ended. Deleting a run unlinks one file and deleting a
  project removes one directory.
- A report that cannot be read is skipped instead of taking the rest of
  the history with it.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
This commit is contained in:
Claude
2026-08-09 14:59:57 +00:00
parent b21385ec4a
commit a851414d13
11 changed files with 262 additions and 223 deletions
-8
View File
@@ -60,14 +60,6 @@ export async function fetchOpenRun(options?: RequestOptions): Promise<OpenRun |
}
}
/**
* 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`);
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
}
export async function renameRun(id: string, label: string): Promise<ShowRun> {
const res = await axios.patch(`${reportUrl}/runs/${id}`, { label });
await ontimeQueryClient.invalidateQueries({ queryKey: REPORT });
@@ -1,16 +1,20 @@
.indicator {
display: flex;
align-items: center;
gap: 0.5rem;
white-space: nowrap;
}
.label {
display: flex;
align-items: center;
gap: 0.375rem;
white-space: nowrap;
background: none;
border: none;
padding: 0;
cursor: pointer;
font-size: calc(1rem - 3px);
color: $label-gray;
&:hover {
color: $ui-white;
}
}
.dot {
@@ -1,58 +1,39 @@
import { useState } from 'react';
import { useNavigate } from 'react-router';
import { finishRun } from '../../../common/api/report';
import Button from '../../../common/components/buttons/Button';
import Tooltip from '../../../common/components/tooltip/Tooltip';
import { useOpenRun } from '../../../common/hooks-query/useRuns';
import style from './RunIndicator.module.scss';
/**
* Shows that a show report is being recorded, and lets the operator close it.
* Shows that a report is being recorded for the show in progress.
*
* This is the feature's home in the editor: it appears only while a run is
* open, so it stays out of the way until it is relevant, and finishing here
* is what writes the run to history.
* This is how the feature is discovered: it appears with the first event and
* points at the reports panel. Ending the run is the existing Finish action
* on the last event, so there is no separate control here.
*/
export default function RunIndicator() {
const openRun = useOpenRun();
const [isFinishing, setIsFinishing] = useState(false);
const navigate = useNavigate();
if (!openRun) {
return null;
}
const handleFinish = async () => {
setIsFinishing(true);
try {
await finishRun();
// take the user to the report they just made, which is also how most
// people will discover that the history exists
void navigate('/editor?settings=sharing__report');
} catch (_error) {
/** the run stays open, the user can try again */
} finally {
setIsFinishing(false);
}
};
// startedAt is a wall clock instant, not a time of day, so formatTime does not apply
const since = new Date(openRun.startedAt).toLocaleTimeString(undefined, { timeStyle: 'short' });
return (
<div className={style.indicator}>
<Tooltip text='A report is being recorded for this show' render={<span />}>
<span className={style.label}>
<span className={style.dot} />
{/* startedAt is a wall clock instant, not a time of day, so it is not formatTime's job */}
Recording since{' '}
<span className={style.since}>
{new Date(openRun.startedAt).toLocaleTimeString(undefined, { timeStyle: 'short' })}
</span>
</span>
</Tooltip>
<Button size='small' variant='subtle' onClick={handleFinish} disabled={isFinishing}>
Finish run
</Button>
</div>
<Tooltip text='A report is being recorded. It is saved when you finish the show.' render={<span />}>
<button
type='button'
className={style.indicator}
onClick={() => navigate('/editor?settings=sharing__report')}
aria-label='Show reports'
>
<span className={style.dot} />
Recording since <span className={style.since}>{since}</span>
</button>
</Tooltip>
);
}