From f4d7c86ec0d5fbcd951b1b241ae36d168e71e9ea Mon Sep 17 00:00:00 2001 From: jojo <49631805+joj0hq@users.noreply.github.com> Date: Mon, 27 Jul 2026 16:31:45 +1000 Subject: [PATCH] fix(ui): parse naive-UTC timestamps consistently in formatAbsoluteDate (#953) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(ui): parse naive-UTC timestamps consistently in formatAbsoluteDate Backend timestamps are naive UTC (Python `datetime.utcnow()`) and are serialized without a timezone suffix. `formatDate` already normalizes these by appending `Z` before parsing, but `formatAbsoluteDate` called `new Date(date)` directly. Per the ES spec, a timezone-less date-time string is parsed as local time, so absolute timestamps were shown off by the viewer's UTC offset (e.g. +9h in JST) — and disagreed with the relative time rendered by `formatDate` for the same value (visible in the Captures detail panel, which uses both on `capture.created_at`). Extract the normalization into a shared `parseServerDate` helper and use it in both formatters. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(format): clarify parseServerDate comment on date-only vs date-time parsing ECMAScript parses date-only strings ("2026-07-23") as UTC but timezone-less date-time strings ("2026-07-23T10:00:00") as local time. The backend emits the latter, which is the case this helper normalizes. Corrects the comment per PR review feedback. Co-Authored-By: Claude Opus 4.8 (1M context) * docs(format): trim parseServerDate comment to match surrounding style Reduce the multi-line explanation to a single why-comment consistent with other utils comments. Co-Authored-By: Claude Opus 4.8 (1M context) --------- Co-authored-by: Claude Opus 4.8 (1M context) --- app/src/lib/utils/format.ts | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/app/src/lib/utils/format.ts b/app/src/lib/utils/format.ts index 0d01a7fd..74aaa7d3 100644 --- a/app/src/lib/utils/format.ts +++ b/app/src/lib/utils/format.ts @@ -23,27 +23,28 @@ function getDateLocale() { } } -export function formatDate(date: string | Date): string { - let dateObj: Date; - if (typeof date === 'string') { - const dateStr = date.trim(); - if (!dateStr.includes('Z') && !dateStr.match(/[+-]\d{2}:\d{2}$/)) { - dateObj = new Date(`${dateStr}Z`); - } else { - dateObj = new Date(dateStr); - } - } else { - dateObj = date; +// Backend timestamps are naive UTC — append `Z` so JS doesn't parse a +// timezone-less date-time string as local time. +function parseServerDate(date: string | Date): Date { + if (typeof date !== 'string') { + return date; } + const dateStr = date.trim(); + if (!dateStr.includes('Z') && !dateStr.match(/[+-]\d{2}:\d{2}$/)) { + return new Date(`${dateStr}Z`); + } + return new Date(dateStr); +} - return formatDistance(dateObj, new Date(), { +export function formatDate(date: string | Date): string { + return formatDistance(parseServerDate(date), new Date(), { addSuffix: true, locale: getDateLocale(), }).replace(/^about /i, ''); } export function formatAbsoluteDate(date: string | Date): string { - const dateObj = typeof date === 'string' ? new Date(date) : date; + const dateObj = parseServerDate(date); return dateObj.toLocaleString(i18n.language, { month: 'short', day: 'numeric',