fix: treat naive backend timestamps as UTC for relative labels
Build / build (push) Successful in 10m35s
Build / build (push) Successful in 10m35s
Backend emits store.Now() as "2026-04-23 14:05:32" — UTC but without a Z marker or T separator. JavaScript parses such strings as local time, so every "N ago" label is skewed by the browser's UTC offset (3h off in UTC+3, causing the visible mismatch with the tooltip). Normalise bare/space-separated timestamps to UTC inside toDate() so every $fmt.* call, including relative, matches the absolute timestamp shown in the tooltip. InstanceCard drops its own parser and delegates to $fmt.relative for consistency.
This commit is contained in:
@@ -9,6 +9,7 @@
|
|||||||
import ConfirmDialog from './ConfirmDialog.svelte';
|
import ConfirmDialog from './ConfirmDialog.svelte';
|
||||||
import { IconPlay, IconStop, IconRestart, IconTrash, IconExternalLink, IconEvents } from '$lib/components/icons';
|
import { IconPlay, IconStop, IconRestart, IconTrash, IconExternalLink, IconEvents } from '$lib/components/icons';
|
||||||
import { t } from '$lib/i18n';
|
import { t } from '$lib/i18n';
|
||||||
|
import { fmt } from '$lib/format/datetime';
|
||||||
import * as api from '$lib/api';
|
import * as api from '$lib/api';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -31,17 +32,7 @@
|
|||||||
: instance.subdomain ? `https://${instance.subdomain}` : ''
|
: instance.subdomain ? `https://${instance.subdomain}` : ''
|
||||||
);
|
);
|
||||||
|
|
||||||
const timeSinceCreated = $derived(() => {
|
const timeSinceCreated = $derived(() => $fmt.relative(instance.created_at));
|
||||||
const created = new Date(instance.created_at);
|
|
||||||
const now = new Date();
|
|
||||||
const diffMs = now.getTime() - created.getTime();
|
|
||||||
const diffMins = Math.floor(diffMs / 60_000);
|
|
||||||
if (diffMins < 60) return `${diffMins}m ago`;
|
|
||||||
const diffHours = Math.floor(diffMins / 60);
|
|
||||||
if (diffHours < 24) return `${diffHours}h ago`;
|
|
||||||
const diffDays = Math.floor(diffHours / 24);
|
|
||||||
return `${diffDays}d ago`;
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handleAction(action: 'stop' | 'start' | 'restart' | 'remove') {
|
async function handleAction(action: 'stop' | 'start' | 'restart' | 'remove') {
|
||||||
loading = true;
|
loading = true;
|
||||||
|
|||||||
@@ -18,6 +18,23 @@ export type DateInput = string | number | Date | null | undefined;
|
|||||||
|
|
||||||
const PLACEHOLDER = '—';
|
const PLACEHOLDER = '—';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Backend emits `store.Now()` as `"2006-01-02 15:04:05"` (UTC, but no `Z`).
|
||||||
|
* Bare ISO strings without a zone (e.g. `"2026-04-23T14:05:32"`) show up too.
|
||||||
|
* JavaScript interprets both as **local** time, which silently skews every
|
||||||
|
* relative label ("3h ago") by the user's UTC offset. Normalise anything
|
||||||
|
* that lacks an explicit zone marker to UTC before constructing the Date.
|
||||||
|
*/
|
||||||
|
function normalizeIsoUtc(input: string): string {
|
||||||
|
const trimmed = input.trim();
|
||||||
|
// Already has Z or ±HH:MM / ±HHMM offset → trust it.
|
||||||
|
if (/(Z|[+-]\d{2}:?\d{2})$/.test(trimmed)) return trimmed;
|
||||||
|
// SQLite-style "YYYY-MM-DD HH:MM:SS(.ffff)?" — swap space for T, append Z.
|
||||||
|
const sqliteMatch = trimmed.match(/^(\d{4}-\d{2}-\d{2})[ T](\d{2}:\d{2}(?::\d{2}(?:\.\d+)?)?)$/);
|
||||||
|
if (sqliteMatch) return `${sqliteMatch[1]}T${sqliteMatch[2]}Z`;
|
||||||
|
return trimmed;
|
||||||
|
}
|
||||||
|
|
||||||
function toDate(input: DateInput): Date | null {
|
function toDate(input: DateInput): Date | null {
|
||||||
if (input === null || input === undefined || input === '') return null;
|
if (input === null || input === undefined || input === '') return null;
|
||||||
if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
|
if (input instanceof Date) return isNaN(input.getTime()) ? null : input;
|
||||||
@@ -28,7 +45,7 @@ function toDate(input: DateInput): Date | null {
|
|||||||
const d = new Date(ms);
|
const d = new Date(ms);
|
||||||
return isNaN(d.getTime()) ? null : d;
|
return isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
const d = new Date(input);
|
const d = new Date(normalizeIsoUtc(input));
|
||||||
return isNaN(d.getTime()) ? null : d;
|
return isNaN(d.getTime()) ? null : d;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user