diff --git a/frontend/src/lib/components/schedules/scheduleDrift.test.ts b/frontend/src/lib/components/schedules/scheduleDrift.test.ts
index 747e3248b1..c170712552 100644
--- a/frontend/src/lib/components/schedules/scheduleDrift.test.ts
+++ b/frontend/src/lib/components/schedules/scheduleDrift.test.ts
@@ -1,48 +1,48 @@
import { describe, it, expect } from 'vitest'
-import { scheduleOutlastsItsInterval } from './scheduleDrift'
+import { runsOutlastingInterval } from './scheduleDrift'
const runs = (duration_ms: number) => Array.from({ length: 5 }, () => ({ duration_ms }))
// Each exemption below is a schedule that is genuinely running less often than
// its cron reads, and is still not something to report. Losing one of them turns
// the badge into noise on a correctly configured schedule.
-describe('scheduleOutlastsItsInterval', () => {
- it('flags runs that outlast the gap between slots', () => {
- expect(scheduleOutlastsItsInterval({ enabled: true, interval_s: 20, jobs: runs(50_000) })).toBe(
- true
+describe('runsOutlastingInterval', () => {
+ it('reports how long runs that outlast the gap between slots are taking', () => {
+ expect(runsOutlastingInterval({ enabled: true, interval_s: 20, jobs: runs(50_000) })).toBe(
+ 50_000
)
})
it('says nothing while the runs still fit', () => {
- expect(scheduleOutlastsItsInterval({ enabled: true, interval_s: 20, jobs: runs(5_000) })).toBe(
- false
- )
+ expect(
+ runsOutlastingInterval({ enabled: true, interval_s: 20, jobs: runs(5_000) })
+ ).toBeUndefined()
})
it('exempts a schedule that queues its next run as the previous one starts', () => {
expect(
- scheduleOutlastsItsInterval({
+ runsOutlastingInterval({
enabled: true,
queues_next_run_at_start: true,
interval_s: 20,
jobs: runs(50_000)
})
- ).toBe(false)
+ ).toBeUndefined()
})
it('exempts a disabled schedule, which is not running at all', () => {
expect(
- scheduleOutlastsItsInterval({ enabled: false, interval_s: 20, jobs: runs(50_000) })
- ).toBe(false)
+ runsOutlastingInterval({ enabled: false, interval_s: 20, jobs: runs(50_000) })
+ ).toBeUndefined()
})
it('waits for more than one run before calling it a pattern', () => {
expect(
- scheduleOutlastsItsInterval({
+ runsOutlastingInterval({
enabled: true,
interval_s: 20,
jobs: [{ duration_ms: 50_000 }]
})
- ).toBe(false)
+ ).toBeUndefined()
})
})
diff --git a/frontend/src/lib/components/schedules/scheduleDrift.ts b/frontend/src/lib/components/schedules/scheduleDrift.ts
index ec58d40be4..b4b54f44ee 100644
--- a/frontend/src/lib/components/schedules/scheduleDrift.ts
+++ b/frontend/src/lib/components/schedules/scheduleDrift.ts
@@ -1,26 +1,29 @@
const MIN_RUNS = 3
-/**
- * Whether a schedule's runs are taking longer than the gap between its slots.
- *
- * A plain script schedule queues its next run only once the previous one has
- * completed, so a run that outlasts the interval necessarily pushes the next
- * one to a later slot: the schedule quietly runs less often than its cron says.
- * Schedules that queue the next run as the previous one starts are exempt, and
- * the server says which those are.
- *
- * Reads the runs the schedules page has already loaded, and asks for a few of
- * them so that one slow run is not read as a change of cadence.
- */
-export function scheduleOutlastsItsInterval(schedule: {
+/** What the schedules page has already loaded about how a schedule is running. */
+export type ScheduleRunsSample = {
queues_next_run_at_start?: boolean
enabled?: boolean
interval_s?: number
jobs?: Array<{ duration_ms: number }>
-}): boolean {
- const { queues_next_run_at_start, enabled, interval_s, jobs } = schedule
- if (queues_next_run_at_start || !enabled || !interval_s || (jobs?.length ?? 0) < MIN_RUNS)
- return false
+}
+
+/**
+ * How long a schedule's runs have been taking, when that is longer than the gap
+ * between its slots, and `undefined` otherwise.
+ *
+ * A plain script schedule queues its next run only once the previous one has
+ * completed, so a run that outlasts the interval necessarily pushes the next one
+ * to a later slot: the schedule quietly runs less often than its cron says.
+ * Schedules that queue the next run as the previous one starts are exempt, and
+ * the server says which those are.
+ */
+export function runsOutlastingInterval(sample: ScheduleRunsSample): number | undefined {
+ const { queues_next_run_at_start, enabled, interval_s, jobs } = sample
+ if (queues_next_run_at_start || !enabled || !interval_s || (jobs?.length ?? 0) < MIN_RUNS) {
+ return undefined
+ }
const durations = jobs!.map((j) => j.duration_ms).sort((a, b) => a - b)
- return durations[durations.length >> 1] > interval_s * 1000
+ const median = durations[durations.length >> 1]
+ return median > interval_s * 1000 ? median : undefined
}
diff --git a/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte b/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte
index 5869225ff2..ed8d8b0b2d 100644
--- a/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte
+++ b/frontend/src/lib/components/triggers/schedules/ScheduleEditor.svelte
@@ -1,8 +1,15 @@
{#if open}
-