-
-
+
+
+
{Math.round(status.progress || 0)}%
+
+
+ {status.remaining_time != null && status.remaining_time > 0 && (
+ <>
+
+
+ {formatDuration(status.remaining_time * 60)}
+
+
+ ETA {formatETA(status.remaining_time, timeFormat, t)}
+
+ >
+ )}
+ {status.layer_num != null && status.total_layers != null && status.total_layers > 0 && (
+
+
+ {status.layer_num}/{status.total_layers}
+
+ )}
-
{t('queue.printingInProgress')}
)}
diff --git a/frontend/src/pages/StreamOverlayPage.tsx b/frontend/src/pages/StreamOverlayPage.tsx
index aed6f53c9..cd1e06fb8 100644
--- a/frontend/src/pages/StreamOverlayPage.tsx
+++ b/frontend/src/pages/StreamOverlayPage.tsx
@@ -5,6 +5,7 @@ import { useTranslation } from 'react-i18next';
import { Layers, Clock, Timer, Printer } from 'lucide-react';
import { api } from '../api/client';
import type { PrinterStatus } from '../api/client';
+import { formatDuration, formatETA, type TimeFormat } from '../utils/date';
type TFunction = (key: string, options?: Record
) => string;
@@ -46,31 +47,6 @@ function parseConfig(params: URLSearchParams): OverlayConfig {
};
}
-function formatTime(seconds: number): string {
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
-}
-
-function formatETA(remainingMinutes: number, t: TFunction): string {
- const now = new Date();
- const eta = new Date(now.getTime() + remainingMinutes * 60 * 1000);
- const today = new Date();
- today.setHours(0, 0, 0, 0);
- const etaDay = new Date(eta);
- etaDay.setHours(0, 0, 0, 0);
-
- const timeStr = eta.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
-
- if (etaDay.getTime() === today.getTime()) {
- return timeStr;
- } else if (etaDay.getTime() === today.getTime() + 86400000) {
- return `${t('streamOverlay.tomorrow')} ${timeStr}`;
- } else {
- return eta.toLocaleDateString([], { weekday: 'short' }) + ' ' + timeStr;
- }
-}
-
function getStatusText(status: PrinterStatus, t: TFunction): string {
if (status.stg_cur_name) return status.stg_cur_name;
@@ -146,6 +122,14 @@ export function StreamOverlayPage() {
refetchInterval: 2000,
});
+ // Fetch settings info
+ const { data: settings } = useQuery({
+ queryKey: ['settings'],
+ queryFn: api.getSettings,
+ });
+
+ const timeFormat: TimeFormat = settings?.time_format || 'system';
+
// WebSocket for real-time updates
useEffect(() => {
if (!id) return;
@@ -298,14 +282,14 @@ export function StreamOverlayPage() {
- {formatTime(status.remaining_time * 60)}
+ {formatDuration(status.remaining_time * 60)}
- {t('streamOverlay.eta')} {formatETA(status.remaining_time, t)}
+ {t('streamOverlay.eta')} {formatETA(status.remaining_time, timeFormat, t)}
>
diff --git a/frontend/src/utils/amsHelpers.ts b/frontend/src/utils/amsHelpers.ts
index e1af13e06..3180da48d 100644
--- a/frontend/src/utils/amsHelpers.ts
+++ b/frontend/src/utils/amsHelpers.ts
@@ -94,17 +94,6 @@ export function getGlobalTrayId(
return amsId * 4 + trayId;
}
-/**
- * Format seconds to human readable time string.
- */
-export function formatTime(seconds: number | null | undefined): string {
- if (!seconds) return '';
- const hours = Math.floor(seconds / 3600);
- const minutes = Math.floor((seconds % 3600) / 60);
- if (hours > 0) return `${hours}h ${minutes}m`;
- return `${minutes}m`;
-}
-
/**
* Get minimum datetime for scheduling (now + 1 minute).
* Returns ISO string format for datetime-local input.
diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts
index 1d53de926..4798cd54f 100644
--- a/frontend/src/utils/date.ts
+++ b/frontend/src/utils/date.ts
@@ -376,3 +376,51 @@ export function formatTimeOnly(
const finalOptions = applyTimeFormat({ ...defaultOptions, ...options }, timeFormat);
return date.toLocaleTimeString([], finalOptions);
}
+
+/**
+ * Calculate and format an ETA based on remaining minutes from now.
+ *
+ * @param remainingMinutes - Minutes until completion
+ * @param timeFormat - Time format setting ('system', '12h', '24h')
+ * @param t - Optional i18n translation function
+ * @returns Formatted ETA string (e.g., "3:45 PM", "Tomorrow 9:30 AM", "Wed 2:00 PM")
+ */
+export function formatETA(
+ remainingMinutes: number,
+ timeFormat: 'system' | '12h' | '24h' = 'system',
+ t?: (key: string) => string
+): string {
+ const now = new Date();
+ const eta = new Date(now.getTime() + remainingMinutes * 60 * 1000);
+
+ const today = new Date();
+ today.setHours(0, 0, 0, 0);
+ const etaDay = new Date(eta);
+ etaDay.setHours(0, 0, 0, 0);
+
+ const timeOptions: Intl.DateTimeFormatOptions = { hour: '2-digit', minute: '2-digit' };
+ if (timeFormat === '12h') timeOptions.hour12 = true;
+ else if (timeFormat === '24h') timeOptions.hour12 = false;
+
+ const timeStr = eta.toLocaleTimeString([], timeOptions);
+ const dayDiff = Math.floor((etaDay.getTime() - today.getTime()) / 86400000);
+
+ if (dayDiff === 0) return timeStr;
+ if (dayDiff === 1) return `${t?.('common.tomorrow') ?? 'Tomorrow'} ${timeStr}`;
+ return `${eta.toLocaleDateString([], { weekday: 'short' })} ${timeStr}`;
+}
+
+/**
+ * Format a duration in seconds to a human-readable string, with null handling.
+ *
+ * @param seconds - Duration in seconds, or null/undefined
+ * @returns Formatted string (e.g., "2h 30m", "45m") or "--" if no value
+ */
+export function formatDuration(seconds: number | null | undefined): string {
+ if (seconds == null || seconds < 0) return '--';
+
+ const hours = Math.floor(seconds / 3600);
+ const minutes = Math.floor((seconds % 3600) / 60);
+
+ return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
+}
\ No newline at end of file
From 7134007e6a207510683264174becde2b64e156aa Mon Sep 17 00:00:00 2001
From: AneoPsy
Date: Sun, 15 Feb 2026 22:47:06 -1000
Subject: [PATCH 2/3] fix(queue): optimize printer status polling and minor
cleanup
- Only poll printer status for actively printing items
- Fix indentation in useQuery block
- Add trailing newline to utils/date.ts
---
frontend/src/pages/QueuePage.tsx | 10 +++++-----
frontend/src/utils/date.ts | 2 +-
2 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/frontend/src/pages/QueuePage.tsx b/frontend/src/pages/QueuePage.tsx
index 803686248..795de094a 100644
--- a/frontend/src/pages/QueuePage.tsx
+++ b/frontend/src/pages/QueuePage.tsx
@@ -315,11 +315,11 @@ function SortableQueueItem({
t: (key: string, options?: Record) => string;
}) {
const { data: status } = useQuery({
- queryKey: ['printerStatus', item.printer_id],
- queryFn: () => api.getPrinterStatus(item.printer_id!),
- refetchInterval: 30000,
- enabled: item.printer_id !== undefined && item.printer_id !== null,
-});
+ queryKey: ['printerStatus', item.printer_id],
+ queryFn: () => api.getPrinterStatus(item.printer_id!),
+ refetchInterval: 30000,
+ enabled: item.printer_id != null && printerState === 'printing',
+ });
const canReorder = hasPermission('queue:reorder');
const {
attributes,
diff --git a/frontend/src/utils/date.ts b/frontend/src/utils/date.ts
index 4798cd54f..14bcd4baf 100644
--- a/frontend/src/utils/date.ts
+++ b/frontend/src/utils/date.ts
@@ -423,4 +423,4 @@ export function formatDuration(seconds: number | null | undefined): string {
const minutes = Math.floor((seconds % 3600) / 60);
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
-}
\ No newline at end of file
+}
From 78fdb27fa534e3a243477b6b0151cb0b2347c4aa Mon Sep 17 00:00:00 2001
From: MartinNYHC
Date: Mon, 16 Feb 2026 09:55:30 +0100
Subject: [PATCH 3/3] Fix indentation in QueuePage.tsx
---
frontend/src/pages/QueuePage.tsx | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/src/pages/QueuePage.tsx b/frontend/src/pages/QueuePage.tsx
index 795de094a..91123c437 100644
--- a/frontend/src/pages/QueuePage.tsx
+++ b/frontend/src/pages/QueuePage.tsx
@@ -498,7 +498,7 @@ function SortableQueueItem({
{/* Progress bar for printing items - TODO: integrate with WebSocket */}
{isPrinting && status && (