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) <noreply@anthropic.com>
This commit is contained in:
2026-05-28 19:36:42 -04:00
parent 8c3900b998
commit c87e8e0932
6 changed files with 40 additions and 9 deletions
@@ -35,6 +35,7 @@
<script setup> <script setup>
import { computed } from 'vue' import { computed } from 'vue'
import { formatLocalDate } from '../../utils/date.js'
const props = defineProps({ const props = defineProps({
name: { type: String, required: true }, name: { type: String, required: true },
@@ -52,7 +53,7 @@ const stats = computed(() => {
parts.push(`${props.imageCount} image${props.imageCount === 1 ? '' : 's'}`) parts.push(`${props.imageCount} image${props.imageCount === 1 ? '' : 's'}`)
} }
if (props.lastAdded) { if (props.lastAdded) {
parts.push(`last added ${props.lastAdded.slice(0, 10)}`) parts.push(`last added ${formatLocalDate(props.lastAdded)}`)
} }
return parts.join(' · ') return parts.join(' · ')
}) })
@@ -32,6 +32,8 @@
</template> </template>
<script setup> <script setup>
import { formatLocalDate } from '../../utils/date.js'
defineProps({ defineProps({
platform: { type: Object, required: true }, platform: { type: Object, required: true },
credential: { type: Object, default: null }, credential: { type: Object, default: null },
@@ -39,8 +41,7 @@ defineProps({
defineEmits(['replace', 'remove']) defineEmits(['replace', 'remove'])
function fmtDate(iso) { function fmtDate(iso) {
if (!iso) return '—' return iso ? formatLocalDate(iso) : '—'
return iso.slice(0, 10)
} }
</script> </script>
@@ -7,7 +7,7 @@
</v-card-title> </v-card-title>
<v-card-text> <v-card-text>
<p class="text-caption" style="opacity: 0.75"> <p class="text-caption" style="opacity: 0.75">
{{ event.started_at }} {{ event.finished_at || '(running)' }} {{ formatDateTime(event.started_at) }} {{ event.finished_at ? formatDateTime(event.finished_at) : '(running)' }}
({{ fmtDuration(event.summary?.duration_seconds) }}) ({{ fmtDuration(event.summary?.duration_seconds) }})
</p> </p>
@@ -105,6 +105,7 @@ import { toast } from '../../utils/toast.js'
import { computed } from 'vue' import { computed } from 'vue'
import { copyText } from '../../utils/clipboard.js' import { copyText } from '../../utils/clipboard.js'
import { formatDateTime } from '../../utils/date.js'
const props = defineProps({ event: { type: Object, default: null } }) const props = defineProps({ event: { type: Object, default: null } })
const emit = defineEmits(['close']) const emit = defineEmits(['close'])
@@ -93,6 +93,7 @@ import { RouterLink } from 'vue-router'
import PlatformChip from '../subscriptions/PlatformChip.vue' import PlatformChip from '../subscriptions/PlatformChip.vue'
import { useSourcesStore } from '../../stores/sources.js' import { useSourcesStore } from '../../stores/sources.js'
import { downloadStatusColor, downloadStatusIcon, downloadStatusLabel } from '../../utils/downloadStatus.js' import { downloadStatusColor, downloadStatusIcon, downloadStatusLabel } from '../../utils/downloadStatus.js'
import { formatDateTime } from '../../utils/date.js'
const props = defineProps({ event: { type: Object, required: true } }) const props = defineProps({ event: { type: Object, required: true } })
defineEmits(['open']) defineEmits(['open'])
@@ -105,9 +106,7 @@ const statusIcon = computed(() => downloadStatusIcon(props.event.status))
const statusLabel = computed(() => downloadStatusLabel(props.event.status)) const statusLabel = computed(() => downloadStatusLabel(props.event.status))
function fmtTime(iso) { function fmtTime(iso) {
if (!iso) return '—' return iso ? formatDateTime(iso) : '—'
// 2026-05-27 23:36 — second granularity is in the row's title attr
return iso.slice(0, 16).replace('T', ' ')
} }
function fmtDuration(sec) { function fmtDuration(sec) {
if (sec == null) return '—' if (sec == null) return '—'
@@ -102,6 +102,7 @@ import { toast } from '../../utils/toast.js'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import PlatformChip from './PlatformChip.vue' import PlatformChip from './PlatformChip.vue'
import { useCredentialsStore } from '../../stores/credentials.js' import { useCredentialsStore } from '../../stores/credentials.js'
import { formatLocalDate } from '../../utils/date.js'
const props = defineProps({ const props = defineProps({
platform: { type: Object, required: true }, platform: { type: Object, required: true },
@@ -195,8 +196,7 @@ const howToFallback = computed(() => {
}) })
function fmtDate(iso) { function fmtDate(iso) {
if (!iso) return '—' return iso ? formatLocalDate(iso) : '—'
return iso.slice(0, 10)
} }
</script> </script>
+29
View File
@@ -32,3 +32,32 @@ export function formatRelative(iso, opts = {}) {
if (future) return diff <= 0 ? 'imminent' : `in ${body}` if (future) return diff <= 0 ? 'imminent' : `in ${body}`
return `${body} ago` 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',
})
}