diff --git a/frontend/src/__tests__/utils/queueEta.test.ts b/frontend/src/__tests__/utils/queueEta.test.ts new file mode 100644 index 000000000..807ac5950 --- /dev/null +++ b/frontend/src/__tests__/utils/queueEta.test.ts @@ -0,0 +1,27 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { formatETA } from '../../utils/date'; +import { formatQueueItemETA } from '../../utils/queueEta'; + +describe('formatQueueItemETA', () => { + beforeEach(() => { + vi.useFakeTimers(); + vi.setSystemTime(new Date('2026-07-31T18:00:00Z')); + }); + + afterEach(() => { + vi.useRealTimers(); + }); + + it('calculates the ETA from the current time and job duration', () => { + expect(formatQueueItemETA(90 * 60, '24h')).toBe( + formatETA(90, '24h'), + ); + }); + + it('returns null without a usable print duration', () => { + expect(formatQueueItemETA(null)).toBeNull(); + expect(formatQueueItemETA(undefined)).toBeNull(); + expect(formatQueueItemETA(0)).toBeNull(); + expect(formatQueueItemETA(-60)).toBeNull(); + }); +}); diff --git a/frontend/src/pages/QueuePage.tsx b/frontend/src/pages/QueuePage.tsx index 212dca0bb..02c984060 100644 --- a/frontend/src/pages/QueuePage.tsx +++ b/frontend/src/pages/QueuePage.tsx @@ -66,6 +66,7 @@ import { api, ApiError } from '../api/client'; import { PipelineRunsView } from './PipelineRunsPage'; import { type TimeFormat, formatETA, formatDuration, formatRelativeTime, parseUTCDate } from '../utils/date'; import { getBedTypeInfo } from '../utils/bedType'; +import { formatQueueItemETA } from '../utils/queueEta'; import type { PrintQueueItem, PrintQueueBulkUpdate, Permission, CalibrationMode } from '../api/client'; import { Card } from '../components/Card'; import { Button } from '../components/Button'; @@ -598,10 +599,20 @@ function SortableQueueItem({ {item.print_time_seconds && ( - - - {formatDuration(item.print_time_seconds)} - + <> + + + {formatDuration(item.print_time_seconds)} + + {!item.waiting_reason && ( + + ETA {formatQueueItemETA(item.print_time_seconds, timeFormat, t)} + + )} + )} {item.filament_used_grams && ( diff --git a/frontend/src/utils/queueEta.ts b/frontend/src/utils/queueEta.ts new file mode 100644 index 000000000..5282af121 --- /dev/null +++ b/frontend/src/utils/queueEta.ts @@ -0,0 +1,18 @@ +import { formatETA, type TimeFormat } from './date'; + +/** + * Formats the estimated completion time for a queue item if it were + * started at the current time. + * + * This is deliberately a per-job estimate rather than a cumulative + * queue forecast. + */ +export function formatQueueItemETA( + printTimeSeconds: number | null | undefined, + timeFormat: TimeFormat = 'system', + t?: Parameters[2], +): string | null { + if (printTimeSeconds == null || printTimeSeconds <= 0) return null; + + return formatETA(printTimeSeconds / 60, timeFormat, t); +}