mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-16 04:43:35 +00:00
3c2875a956
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
139 lines
3.8 KiB
TypeScript
139 lines
3.8 KiB
TypeScript
import { EntryId, MaybeNumber, OntimeEventReport, OntimeReport, RundownEntries, isOntimeEvent } from 'ontime-types';
|
||
import { MILLIS_PER_SECOND } from 'ontime-utils';
|
||
|
||
import { makeCSVFromArrayOfArrays } from '../../../../common/utils/csv';
|
||
import { formatDuration, formatTime } from '../../../../common/utils/time';
|
||
|
||
/**
|
||
* Signed drift for a run, eg "+4m 12s" / "-1m". A run with no completed
|
||
* events has no meaningful drift to report.
|
||
*/
|
||
export function formatDrift(drift: number, eventsRun: number): string {
|
||
if (eventsRun === 0) return '–';
|
||
if (Math.abs(drift) < MILLIS_PER_SECOND) return 'On time';
|
||
return `${drift > 0 ? '+' : '-'}${formatDuration(Math.abs(drift), false)}`;
|
||
}
|
||
|
||
export type CombinedReport = {
|
||
id: EntryId;
|
||
index: number;
|
||
title: string;
|
||
cue: string;
|
||
scheduledStart: number;
|
||
actualStart: MaybeNumber;
|
||
scheduledEnd: number;
|
||
actualEnd: MaybeNumber;
|
||
playCount: number;
|
||
};
|
||
|
||
/**
|
||
* Creates a combined report joining a run's per event data with the rundown.
|
||
*
|
||
* A run's report keeps its own snapshot of the schedule (`scheduledStart` /
|
||
* `scheduledDuration`), so an event that ran is shown against the schedule as
|
||
* it was at the time, not against whatever the rundown has been edited to since.
|
||
*
|
||
* Events that ran but no longer exist in the rundown (deleted since the run)
|
||
* are still included, from the snapshot alone, so a historical run stays a
|
||
* complete record even after the rundown changes.
|
||
*/
|
||
export function getCombinedReport(
|
||
report: OntimeReport,
|
||
rundownEntries: RundownEntries,
|
||
flatOrder: EntryId[],
|
||
): CombinedReport[] {
|
||
if (Object.keys(report).length === 0 && flatOrder.length === 0) return [];
|
||
|
||
const combinedReport: CombinedReport[] = [];
|
||
const seen = new Set<EntryId>();
|
||
let index = 1;
|
||
|
||
for (const id of flatOrder) {
|
||
const entry = rundownEntries[id];
|
||
if (!entry || !isOntimeEvent(entry)) continue;
|
||
|
||
seen.add(id);
|
||
combinedReport.push(makeCombinedEntry(id, index, entry.title, entry.cue, report[id], entry.timeStart, entry.timeEnd));
|
||
index++;
|
||
}
|
||
|
||
for (const [id, reportEntry] of Object.entries(report)) {
|
||
if (seen.has(id)) continue;
|
||
combinedReport.push(
|
||
makeCombinedEntry(
|
||
id,
|
||
index,
|
||
'(deleted event)',
|
||
'–',
|
||
reportEntry,
|
||
reportEntry.scheduledStart,
|
||
reportEntry.scheduledStart + reportEntry.scheduledDuration,
|
||
),
|
||
);
|
||
index++;
|
||
}
|
||
|
||
return combinedReport;
|
||
}
|
||
|
||
function makeCombinedEntry(
|
||
id: EntryId,
|
||
index: number,
|
||
title: string,
|
||
cue: string,
|
||
reportEntry: OntimeEventReport | undefined,
|
||
fallbackStart: number,
|
||
fallbackEnd: number,
|
||
): CombinedReport {
|
||
if (!reportEntry) {
|
||
return {
|
||
id,
|
||
index,
|
||
title,
|
||
cue,
|
||
scheduledStart: fallbackStart,
|
||
scheduledEnd: fallbackEnd,
|
||
actualStart: null,
|
||
actualEnd: null,
|
||
playCount: 0,
|
||
};
|
||
}
|
||
|
||
return {
|
||
id,
|
||
index,
|
||
title,
|
||
cue,
|
||
scheduledStart: reportEntry.scheduledStart,
|
||
scheduledEnd: reportEntry.scheduledStart + reportEntry.scheduledDuration,
|
||
actualStart: reportEntry.startedAt,
|
||
actualEnd: reportEntry.endedAt,
|
||
playCount: reportEntry.playCount,
|
||
};
|
||
}
|
||
|
||
const csvHeader = ['Index', 'Title', 'Cue', 'Scheduled Start', 'Actual Start', 'Scheduled End', 'Actual End', 'Play count'];
|
||
|
||
/**
|
||
* Transforms a CombinedReport into a CSV string
|
||
*/
|
||
export function makeReportCSV(combinedReport: CombinedReport[]) {
|
||
const csv: string[][] = [];
|
||
csv.push(csvHeader);
|
||
|
||
for (const entry of combinedReport) {
|
||
csv.push([
|
||
String(entry.index),
|
||
entry.title,
|
||
entry.cue,
|
||
formatTime(entry.scheduledStart),
|
||
formatTime(entry.actualStart),
|
||
formatTime(entry.scheduledEnd),
|
||
formatTime(entry.actualEnd),
|
||
String(entry.playCount),
|
||
]);
|
||
}
|
||
|
||
return makeCSVFromArrayOfArrays(csv);
|
||
}
|