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 */ }