diff --git a/js/ui.js b/js/ui.js index 4bbb462..f481c60 100644 --- a/js/ui.js +++ b/js/ui.js @@ -39,17 +39,25 @@ function esc(str) { return d.innerHTML; } +// SQLite datetime('now') → 'YYYY-MM-DD HH:MM:SS' (UTC, no timezone marker). +// Appending 'Z' tells JS to parse it as UTC rather than local time. +function parseUtc(d) { + if (typeof d !== 'string') return new Date(d); + const hasZone = d.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(d); + return new Date(hasZone ? d : d.replace(' ', 'T') + 'Z'); +} + function fmtDate(d) { if (!d) return '—'; try { - return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: getTimezone() }); + return parseUtc(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: getTimezone() }); } catch (e) { return d; } } function fmtDateFull(d) { if (!d) return '—'; try { - return new Date(d).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: getTimezone(), timeZoneName: 'short' }); + return parseUtc(d).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: getTimezone(), timeZoneName: 'short' }); } catch (e) { return d; } } diff --git a/tests/helpers.test.js b/tests/helpers.test.js index 587aa14..3b7c550 100644 --- a/tests/helpers.test.js +++ b/tests/helpers.test.js @@ -58,16 +58,22 @@ describe('esc', () => { // ── fmtDate() ───────────────────────────────────────────────────────────────── -function fmtDate(d) { +function parseUtc(d) { + if (typeof d !== 'string') return new Date(d) + const hasZone = d.endsWith('Z') || /[+-]\d{2}:\d{2}$/.test(d) + return new Date(hasZone ? d : d.replace(' ', 'T') + 'Z') +} + +function fmtDate(d, tz = 'UTC') { if (!d) return '—' try { - return new Date(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric' }) + return parseUtc(d).toLocaleDateString('en-US', { year: 'numeric', month: 'short', day: 'numeric', timeZone: tz }) } catch (e) { return d } } describe('fmtDate', () => { it('formats a valid ISO date string', () => { - const result = fmtDate('2024-03-15T00:00:00') + const result = fmtDate('2024-03-15T12:00:00Z') expect(result).toMatch(/Mar/) expect(result).toMatch(/15/) expect(result).toMatch(/2024/) @@ -91,7 +97,7 @@ describe('fmtDate', () => { function fmtDateFull(d, tz = 'UTC') { if (!d) return '—' try { - return new Date(d).toLocaleString('en-US', { + return parseUtc(d).toLocaleString('en-US', { year: 'numeric', month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit', timeZone: tz, timeZoneName: 'short', @@ -118,6 +124,12 @@ describe('fmtDateFull', () => { expect(result).toMatch(/EDT/) }) + it('treats SQLite-format timestamps (space, no Z) as UTC', () => { + // SQLite datetime('now') → 'YYYY-MM-DD HH:MM:SS', no timezone marker. + // Must parse identically to the same moment expressed as ISO UTC. + expect(fmtDateFull('2024-03-15 18:30:00', 'UTC')).toBe(fmtDateFull('2024-03-15T18:30:00Z', 'UTC')) + }) + it('returns — for null', () => { expect(fmtDateFull(null)).toBe('—') })