fix(frontend): #1602 render print-run / spool-usage / camera-token / SpoolBuddy timestamps in local time

Four frontend formatDate / formatDateTime helpers called new Date(iso)
  directly on backend timestamps that have no timezone indicator. Per
  ECMAScript, a bare "2026-06-02T07:50:00" is parsed as local time, so
  a UTC-stored value got displayed as if its numeric components were
  already local — visually identical to UTC. Same shape as the #504
  fix from Feb 2026, which patched 13 sites but missed these four:
  PrintLogTable and SpoolUsageHistory hadn't been written yet;
  CameraTokensPage and SpoolBuddySettingsPage existed but were
  overlooked.

  Reporter #2 (@IndividualGhost1905) confirmed with a UTC+3 host: print
  log shows UTC clock value, system date shows local. PrintLogTable is
  the "logs/completion time" they called out; the other three are the
  same pattern in nearby surfaces.

  Replaced the bare new Date(iso) calls with parseUTCDate(iso) from
  utils/date.ts — the same helper every other date formatter in the
  codebase already uses. It appends "Z" to naive ISO strings and
  parses TZ-tagged strings as-is. CameraTokensPage.isExpired got the
  same fix because comparing a misparsed Date against Date.now() would
  produce false "not expired" / "expired" results around the TZ-offset
  boundary.

  Reporter #1's printer-card ETA complaint (10:50 + 57m showing 09:48)
  is NOT addressed by this fix. That ETA comes from
  formatETA(remainingMinutes) which is purely client-side (new Date()
  plus minutes from the WebSocket payload, then toLocaleTimeString)
  — for it to render UTC the browser timezone itself would need to
  be UTC, which is a browser / OS config issue.

  Audit confirmed no other regressions: grepped every new Date( call
  in frontend/src/. Remaining sites either pass an epoch-ms number,
  use the result only for .getTime() arithmetic where the offset
  cancels, already wrap in parseUTCDate fallback, or consume a
  backend timestamp that includes "+00:00" (FailureDetectionSettings
  reads obico_detection.py's tz-aware isoformat).
This commit is contained in:
maziggy
2026-06-03 10:39:07 +02:00
parent cc25cbe774
commit e38267065c
7 changed files with 63 additions and 59 deletions
+1
View File
File diff suppressed because one or more lines are too long
+3 -2
View File
@@ -2,6 +2,7 @@ import { useQuery } from '@tanstack/react-query';
import { useTranslation } from 'react-i18next';
import { Loader2 } from 'lucide-react';
import { api } from '../api/client';
import { parseUTCDate } from '../utils/date';
interface PrintLogTableProps {
archiveId: number;
@@ -17,8 +18,8 @@ function formatDuration(seconds: number | null): string {
function formatDate(isoString: string | null): string {
if (!isoString) return '—';
const d = new Date(isoString);
return d.toLocaleString();
const d = parseUTCDate(isoString);
return d ? d.toLocaleString() : '—';
}
export function PrintLogTable({ archiveId }: PrintLogTableProps) {
@@ -5,8 +5,10 @@ import { api } from '../api/client';
import type { SpoolUsageRecord } from '../api/client';
import { Button } from './Button';
import { useToast } from '../contexts/ToastContext';
import { parseUTCDate } from '../utils/date';
function formatDate(dateStr: string): string {
const date = new Date(dateStr);
const date = parseUTCDate(dateStr);
if (!date) return '';
return date.toLocaleDateString('en-GB', { day: '2-digit', month: '2-digit', year: '2-digit' }) +
' ' + date.toLocaleTimeString('en-GB', { hour: '2-digit', minute: '2-digit' });
}
+5 -3
View File
@@ -21,18 +21,20 @@ import { Copy, Plus, Trash2, AlertTriangle } from 'lucide-react';
import { api, type LongLivedCameraToken } from '../api/client';
import { useToast } from '../contexts/ToastContext';
import { useAuth } from '../contexts/AuthContext';
import { parseUTCDate } from '../utils/date';
const DEFAULT_LIFETIME_DAYS = 90;
const MAX_LIFETIME_DAYS = 365;
function formatDate(iso: string | null): string {
if (!iso) return '—';
const d = new Date(iso);
return d.toLocaleString();
const d = parseUTCDate(iso);
return d ? d.toLocaleString() : '—';
}
function isExpired(iso: string): boolean {
return new Date(iso).getTime() < Date.now();
const d = parseUTCDate(iso);
return d ? d.getTime() < Date.now() : false;
}
interface CreateTokenFormProps {
@@ -6,6 +6,7 @@ import type { SpoolBuddyOutletContext } from '../../components/spoolbuddy/SpoolB
import { spoolbuddyApi, type SpoolBuddyDevice } from '../../api/client';
import { DiagnosticModal } from '../../components/spoolbuddy/DiagnosticModal';
import { FileText, Wand2, Zap } from 'lucide-react';
import { parseUTCDate } from '../../utils/date';
function formatUptime(seconds: number): string {
@@ -18,13 +19,10 @@ function formatUptime(seconds: number): string {
function formatDateTime(iso: string | null): string {
if (!iso) return '-';
try {
const d = new Date(iso);
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' +
d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
} catch {
return '-';
}
const d = parseUTCDate(iso);
if (!d) return '-';
return d.toLocaleDateString(undefined, { month: 'short', day: 'numeric' }) + ' ' +
d.toLocaleTimeString(undefined, { hour: '2-digit', minute: '2-digit' });
}
const BLANK_OPTIONS = [
File diff suppressed because one or more lines are too long
+1 -1
View File
@@ -26,7 +26,7 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-BuuCTvNJ.js"></script>
<script type="module" crossorigin src="/assets/index-DLXkorBx.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-C3FyyVE7.css">
</head>
<body>