From 3a19303d4d20a091e36f7dead3cd7649cd6d02a1 Mon Sep 17 00:00:00 2001 From: null Date: Thu, 4 Jun 2026 21:42:34 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20SQLite=20timestamp=20timezone=20ambiguit?= =?UTF-8?q?y=20=E2=80=94=20convert=20to=20proper=20UTC=20ISO=20strings?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- routes/status.js | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/routes/status.js b/routes/status.js index a3c1b06..455ab40 100644 --- a/routes/status.js +++ b/routes/status.js @@ -18,6 +18,20 @@ function errorMessage(err) { return err?.message || String(err); } +// SQLite stores datetime('now') as "YYYY-MM-DD HH:MM:SS" (UTC, no Z). +// Without the Z suffix, JS Date parses it as LOCAL time, creating timezone +// inconsistencies when mixed with ISO strings that do have Z. +// This ensures all timestamps sent to clients are unambiguous UTC ISO strings. +function toIso(sqliteOrIso) { + if (!sqliteOrIso) return null; + const s = String(sqliteOrIso).trim(); + // Already ISO with Z or offset — return as-is + if (s.includes('T') && (s.endsWith('Z') || s.match(/[+-]\d{2}:\d{2}$/))) return s; + // SQLite format "YYYY-MM-DD HH:MM:SS" — append Z to declare it UTC + if (/^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}/.test(s)) return s.replace(' ', 'T') + 'Z'; + return s; +} + function monthRange(now) { const year = now.getFullYear(); const month = now.getMonth() + 1; @@ -291,7 +305,7 @@ router.get('/', async (req, res) => { source_count: sourceRow.source_count ?? 0, account_count: accountRow.account_count ?? 0, transaction_count: txRow.transaction_count ?? 0, - last_sync_at: sourceRow.last_sync_at || null, + last_sync_at: toIso(sourceRow.last_sync_at), last_error: errorRow?.last_error || null, }; } @@ -305,7 +319,7 @@ router.get('/', async (req, res) => { const raw = getSetting('cleanup_last_result'); cleanup = { ok: true, - last_run_at: getSetting('cleanup_last_run_at') || null, + last_run_at: toIso(getSetting('cleanup_last_run_at')), last_result: raw ? JSON.parse(raw) : null, }; } catch { /* non-fatal */ }