From c87e8e0932e95c77843aee72f2f4b7c6287e36f2 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 28 May 2026 19:36:42 -0400 Subject: [PATCH] fix(ui): render absolute timestamps in the viewer's local timezone Download event times showed raw UTC wall-clock (iso.slice). Added formatDateTime()/formatLocalDate() (local tz, robust to naive vs tz-aware ISO) and applied them to the download row + detail modal datetimes and the credential/artist date displays. formatPostDate stays UTC (date-only, locale-stable, unit-tested). Co-Authored-By: Claude Opus 4.7 (1M context) --- .../src/components/artist/ArtistHeader.vue | 3 +- .../credentials/PlatformCredentialRow.vue | 5 ++-- .../downloads/DownloadDetailModal.vue | 3 +- .../components/downloads/DownloadEventRow.vue | 5 ++-- .../subscriptions/CredentialCard.vue | 4 +-- frontend/src/utils/date.js | 29 +++++++++++++++++++ 6 files changed, 40 insertions(+), 9 deletions(-) diff --git a/frontend/src/components/artist/ArtistHeader.vue b/frontend/src/components/artist/ArtistHeader.vue index 93f4062..a2b1348 100644 --- a/frontend/src/components/artist/ArtistHeader.vue +++ b/frontend/src/components/artist/ArtistHeader.vue @@ -35,6 +35,7 @@ diff --git a/frontend/src/components/downloads/DownloadDetailModal.vue b/frontend/src/components/downloads/DownloadDetailModal.vue index e8c4697..260bf5e 100644 --- a/frontend/src/components/downloads/DownloadDetailModal.vue +++ b/frontend/src/components/downloads/DownloadDetailModal.vue @@ -7,7 +7,7 @@

- {{ event.started_at }} → {{ event.finished_at || '(running)' }} + {{ formatDateTime(event.started_at) }} → {{ event.finished_at ? formatDateTime(event.finished_at) : '(running)' }} ({{ fmtDuration(event.summary?.duration_seconds) }})

@@ -105,6 +105,7 @@ import { toast } from '../../utils/toast.js' import { computed } from 'vue' import { copyText } from '../../utils/clipboard.js' +import { formatDateTime } from '../../utils/date.js' const props = defineProps({ event: { type: Object, default: null } }) const emit = defineEmits(['close']) diff --git a/frontend/src/components/downloads/DownloadEventRow.vue b/frontend/src/components/downloads/DownloadEventRow.vue index c18b873..8a5a6fb 100644 --- a/frontend/src/components/downloads/DownloadEventRow.vue +++ b/frontend/src/components/downloads/DownloadEventRow.vue @@ -93,6 +93,7 @@ import { RouterLink } from 'vue-router' import PlatformChip from '../subscriptions/PlatformChip.vue' import { useSourcesStore } from '../../stores/sources.js' import { downloadStatusColor, downloadStatusIcon, downloadStatusLabel } from '../../utils/downloadStatus.js' +import { formatDateTime } from '../../utils/date.js' const props = defineProps({ event: { type: Object, required: true } }) defineEmits(['open']) @@ -105,9 +106,7 @@ const statusIcon = computed(() => downloadStatusIcon(props.event.status)) const statusLabel = computed(() => downloadStatusLabel(props.event.status)) function fmtTime(iso) { - if (!iso) return '—' - // 2026-05-27 23:36 — second granularity is in the row's title attr - return iso.slice(0, 16).replace('T', ' ') + return iso ? formatDateTime(iso) : '—' } function fmtDuration(sec) { if (sec == null) return '—' diff --git a/frontend/src/components/subscriptions/CredentialCard.vue b/frontend/src/components/subscriptions/CredentialCard.vue index 182b09e..ed2be30 100644 --- a/frontend/src/components/subscriptions/CredentialCard.vue +++ b/frontend/src/components/subscriptions/CredentialCard.vue @@ -102,6 +102,7 @@ import { toast } from '../../utils/toast.js' import { computed, ref } from 'vue' import PlatformChip from './PlatformChip.vue' import { useCredentialsStore } from '../../stores/credentials.js' +import { formatLocalDate } from '../../utils/date.js' const props = defineProps({ platform: { type: Object, required: true }, @@ -195,8 +196,7 @@ const howToFallback = computed(() => { }) function fmtDate(iso) { - if (!iso) return '—' - return iso.slice(0, 10) + return iso ? formatLocalDate(iso) : '—' } diff --git a/frontend/src/utils/date.js b/frontend/src/utils/date.js index 16f6fd3..48e9ddc 100644 --- a/frontend/src/utils/date.js +++ b/frontend/src/utils/date.js @@ -32,3 +32,32 @@ export function formatRelative(iso, opts = {}) { if (future) return diff <= 0 ? 'imminent' : `in ${body}` return `${body} ago` } + +// Parse a backend timestamp as a UTC instant. Backend serializes tz-aware +// UTC (…+00:00); if a value ever arrives without a tz designator we treat +// it as UTC so it still converts to the viewer's zone correctly. +function _parseUtc (iso) { + if (typeof iso !== 'string' || !iso) return null + const hasTz = /([zZ])$|([+-]\d{2}:?\d{2})$/.test(iso) + const d = new Date(hasTz ? iso : `${iso}Z`) + return isNaN(d.getTime()) ? null : d +} + +// Local-timezone date + time, e.g. "May 28, 7:30 PM". Use for absolute +// timestamps that were previously shown as raw UTC ISO. +export function formatDateTime (iso) { + const d = _parseUtc(iso) + if (!d) return '' + return d.toLocaleString(undefined, { + month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', + }) +} + +// Local-timezone date only, e.g. "May 28, 2026". +export function formatLocalDate (iso) { + const d = _parseUtc(iso) + if (!d) return '' + return d.toLocaleDateString(undefined, { + year: 'numeric', month: 'short', day: 'numeric', + }) +}