mirror of
https://github.com/cpvalente/ontime.git
synced 2026-08-31 20:09:11 +00:00
refactor(report): one summary shape for the show and its groups
A group heading is a summary of the events inside it in the same way the show summary is a summary of the groups, so both are now built on one shell and read the same way: the conclusion at full weight on the left, the workings beside it, and how much of it ran along the bottom. The group's dense dot separated stat row is gone; it carries the same shape at less weight so it belongs to the summary rather than competing with it. The show's headline becomes its total plan deviation, the figure a producer is actually asked about afterwards, read back underneath as the two parts it is made of: the lateness it inherited from its start and the time the show itself lost or made up. The previous "recovered during the show" phrasing named only the second part while showing a figure for neither. Drops the counts of events over, under and on time. Almost nothing lands on the exact second, so those buckets described rounding rather than the show. The single worst overrun, which was already computed but never shown, replaces them: it names the event that blew the schedule. Also: - a group with events still to run no longer reports a variance, which otherwise read as a large underrun for a block that had simply not finished. This matches the guard already in place at show level. - GroupReport.changeover becomes untimed. The same gap can be a changeover, a timer started late, or an item run without a timer at all, and in roll mode it is the scheduled gap. Naming it for what was measured rather than for a cause avoids asserting a workflow. - the summary names the rundown it reports on, since a project can hold several. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019nr3FbLbM8gB8Jm771YgTV
This commit is contained in:
@@ -63,23 +63,32 @@ export type GroupReport = {
|
||||
/** wall time from the first event starting to the last one ending */
|
||||
elapsed: MaybeNumber;
|
||||
/**
|
||||
* Time inside the block not covered by a running event, ie changeovers.
|
||||
* elapsed minus the sum of the events' own durations.
|
||||
* Time inside the block with no event running: elapsed minus the sum of the
|
||||
* events' own durations. Deliberately named for what was measured rather
|
||||
* than for a cause, since the same gap can be a changeover, a timer started
|
||||
* late, or an event run without the timer at all.
|
||||
*/
|
||||
untimed: MaybeNumber;
|
||||
/**
|
||||
* Measured against targetDuration where set, otherwise scheduledDuration.
|
||||
* Null while the group is incomplete: the events still to run would make a
|
||||
* partial group read as a large underrun.
|
||||
*/
|
||||
changeover: MaybeNumber;
|
||||
/** measured against targetDuration where set, otherwise scheduledDuration */
|
||||
variance: MaybeNumber;
|
||||
eventsRun: number;
|
||||
eventsPlanned: number;
|
||||
};
|
||||
|
||||
/** Counts across the events in the report */
|
||||
/**
|
||||
* Counts across the events in the report.
|
||||
*
|
||||
* Deliberately not a tally of events over, under and on time: events rarely
|
||||
* land on the exact second, so those buckets describe rounding more than they
|
||||
* describe the show. The single worst overrun is the fact that acts on.
|
||||
*/
|
||||
export type RunSummary = {
|
||||
eventsRun: number;
|
||||
eventsPlanned: number;
|
||||
eventsOver: number;
|
||||
eventsUnder: number;
|
||||
eventsOnTime: number;
|
||||
/** largest single overrun, answers "what blew the schedule" */
|
||||
worstOverrun: { id: EntryId; delta: number } | null;
|
||||
};
|
||||
|
||||
@@ -172,8 +172,8 @@ describe('getGroupReports()', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('reports the time a block spent not running an event', () => {
|
||||
// 5 minute changeover between the two events
|
||||
it('reports the time a block spent with no event running', () => {
|
||||
// 5 minutes between one event ending and the next starting
|
||||
const report: OntimeReport = {
|
||||
a: makeEntry({ startedAt: 0, endedAt: 10 * MIN, scheduledDuration: 10 * MIN }),
|
||||
b: makeEntry({ startedAt: 15 * MIN, endedAt: 25 * MIN, scheduledDuration: 10 * MIN }),
|
||||
@@ -182,7 +182,7 @@ describe('getGroupReports()', () => {
|
||||
const [group] = getGroupReports(report, entries, ['act1']);
|
||||
|
||||
expect(group.elapsed).toBe(25 * MIN);
|
||||
expect(group.changeover).toBe(5 * MIN);
|
||||
expect(group.untimed).toBe(5 * MIN);
|
||||
});
|
||||
|
||||
it('falls back to what was scheduled when no target was set', () => {
|
||||
@@ -200,7 +200,9 @@ describe('getGroupReports()', () => {
|
||||
expect(group.variance).toBe(4 * MIN);
|
||||
});
|
||||
|
||||
it('reads a partly run block as incomplete rather than as an overrun', () => {
|
||||
it('withholds variance while a block still has events to run', () => {
|
||||
// one of two events run: the block has not spent its 30 minute budget yet,
|
||||
// reporting it now would read as a 20 minute underrun
|
||||
const report: OntimeReport = {
|
||||
a: makeEntry({ startedAt: 0, endedAt: 10 * MIN, scheduledDuration: 10 * MIN }),
|
||||
};
|
||||
@@ -209,12 +211,15 @@ describe('getGroupReports()', () => {
|
||||
|
||||
expect(group.eventsRun).toBe(1);
|
||||
expect(group.eventsPlanned).toBe(2);
|
||||
expect(group.variance).toBeNull();
|
||||
// what did happen is still reported
|
||||
expect(group.elapsed).toBe(10 * MIN);
|
||||
});
|
||||
|
||||
it('has no actuals for a block which never ran', () => {
|
||||
const [group] = getGroupReports({}, entries, ['act1']);
|
||||
|
||||
expect(group).toMatchObject({ elapsed: null, changeover: null, variance: null, eventsRun: 0 });
|
||||
expect(group).toMatchObject({ elapsed: null, untimed: null, variance: null, eventsRun: 0 });
|
||||
});
|
||||
|
||||
it('leaves skipped events out of the block', () => {
|
||||
@@ -232,20 +237,14 @@ describe('getGroupReports()', () => {
|
||||
});
|
||||
|
||||
describe('getRunSummary()', () => {
|
||||
it('counts how events landed', () => {
|
||||
it('counts how much of the show ran', () => {
|
||||
const report: OntimeReport = {
|
||||
a: makeEntry({ endedAt: 15000 }), // over
|
||||
b: makeEntry({ endedAt: 6000 }), // under
|
||||
c: makeEntry({ endedAt: 10000 }), // on time
|
||||
a: makeEntry({ endedAt: 15000 }),
|
||||
b: makeEntry({ endedAt: 6000 }),
|
||||
c: makeEntry({ endedAt: 10000 }),
|
||||
};
|
||||
|
||||
expect(getRunSummary(report, 4)).toMatchObject({
|
||||
eventsRun: 3,
|
||||
eventsPlanned: 4,
|
||||
eventsOver: 1,
|
||||
eventsUnder: 1,
|
||||
eventsOnTime: 1,
|
||||
});
|
||||
expect(getRunSummary(report, 4)).toMatchObject({ eventsRun: 3, eventsPlanned: 4 });
|
||||
});
|
||||
|
||||
it('identifies the worst overrun', () => {
|
||||
@@ -258,6 +257,11 @@ describe('getRunSummary()', () => {
|
||||
expect(getRunSummary(report, 3).worstOverrun).toEqual({ id: 'b', delta: 20000 });
|
||||
});
|
||||
|
||||
it('has no worst overrun when nothing ran long', () => {
|
||||
const report: OntimeReport = { a: makeEntry({ endedAt: 6000 }), b: makeEntry({ endedAt: 10000 }) };
|
||||
expect(getRunSummary(report, 2).worstOverrun).toBeNull();
|
||||
});
|
||||
|
||||
it('ignores events which did not complete', () => {
|
||||
const report: OntimeReport = { a: makeEntry({ endedAt: 15000 }), b: makeEntry({ endedAt: null }) };
|
||||
expect(getRunSummary(report, 2).eventsRun).toBe(1);
|
||||
|
||||
@@ -164,6 +164,9 @@ export function getGroupReports(report: OntimeReport, entries: RundownEntries, o
|
||||
const elapsed = actualStart === null || actualEnd === null ? null : elapsedBetween(actualStart, actualEnd);
|
||||
// the budget if the user set one, otherwise what they scheduled into it
|
||||
const measuredAgainst = group.targetDuration ?? scheduledDuration;
|
||||
// a group with events still to run has not spent its budget yet: comparing
|
||||
// what it has used so far against the whole would report a large underrun
|
||||
const isComplete = eventsRun > 0 && eventsRun === eventsPlanned;
|
||||
|
||||
groups.push({
|
||||
id: group.id,
|
||||
@@ -174,9 +177,9 @@ export function getGroupReports(report: OntimeReport, entries: RundownEntries, o
|
||||
actualStart,
|
||||
actualEnd,
|
||||
elapsed,
|
||||
// whatever the block spent not running an event
|
||||
changeover: elapsed === null ? null : Math.max(0, elapsed - ranDuration),
|
||||
variance: elapsed === null ? null : elapsed - measuredAgainst,
|
||||
// whatever the block spent with no event running
|
||||
untimed: elapsed === null ? null : Math.max(0, elapsed - ranDuration),
|
||||
variance: elapsed === null || !isComplete ? null : elapsed - measuredAgainst,
|
||||
eventsRun,
|
||||
eventsPlanned,
|
||||
});
|
||||
@@ -185,14 +188,17 @@ export function getGroupReports(report: OntimeReport, entries: RundownEntries, o
|
||||
return groups;
|
||||
}
|
||||
|
||||
/** Counts across the events in the report */
|
||||
/**
|
||||
* Counts across the events in the report.
|
||||
*
|
||||
* Only how much of the show ran, and which single event ran longest over its
|
||||
* schedule. Bucketing every event as over, under or on time reads as precision
|
||||
* the data does not have: almost nothing lands on the exact second.
|
||||
*/
|
||||
export function getRunSummary(report: OntimeReport, eventsPlanned: number): RunSummary {
|
||||
const summary: RunSummary = {
|
||||
eventsRun: 0,
|
||||
eventsPlanned,
|
||||
eventsOver: 0,
|
||||
eventsUnder: 0,
|
||||
eventsOnTime: 0,
|
||||
worstOverrun: null,
|
||||
};
|
||||
|
||||
@@ -204,15 +210,8 @@ export function getRunSummary(report: OntimeReport, eventsPlanned: number): RunS
|
||||
|
||||
summary.eventsRun += 1;
|
||||
|
||||
if (variance.status === 'over') {
|
||||
summary.eventsOver += 1;
|
||||
if (summary.worstOverrun === null || variance.delta > summary.worstOverrun.delta) {
|
||||
summary.worstOverrun = { id, delta: variance.delta };
|
||||
}
|
||||
} else if (variance.status === 'under') {
|
||||
summary.eventsUnder += 1;
|
||||
} else {
|
||||
summary.eventsOnTime += 1;
|
||||
if (variance.status === 'over' && (summary.worstOverrun === null || variance.delta > summary.worstOverrun.delta)) {
|
||||
summary.worstOverrun = { id, delta: variance.delta };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user