Repository navigation
Slimbooks v2.1.1
Store one timestamp format, UTC, and render it on the viewer's clock
Two shapes were being written into the same TEXT columns:
2026-08-12T13:54:13.241Z insertRecord/updateRecord, via toISOString()
2026-08-12 13:54:13 the column defaults and the dialect
Both are UTC, so this was never a timezone bug. It was a comparison bug:
these columns are TEXT on both backends, every comparison is
lexicographic, and a space sorts below T — so 2026-08-12 23:00:00
sorted before 2026-08-12T01:00:00.241Z. Any window query spanning the
two formats returned the wrong rows, silently and only sometimes.
Everything now stores YYYY-MM-DDTHH:MM:SSZ — 20 characters, fixed
width, whole seconds. The SQL spelling was the smaller change but is not
a format JavaScript parses to a defined instant: the standard covers the
T/Z form and says nothing about the space form, so V8 reads it as
local time. The database moves to the format its only consumer can read.
Calendar-day columns are narrowed back to YYYY-MM-DD. A due date
written as an instant rendered as one day or the next depending on where
the reader was sitting; a due date is a day.
Migration 014 rewrites existing rows, in JavaScript rather than SQL —
the obvious statement needs || on SQLite and CONCAT on MySQL, and a
migration that branches on the engine is what this codebase avoids. It
is idempotent per row, so a second run touches nothing.
On the display side, eleven screens imported a formatDateSync that
hard-coded MM/DD/YYYY in en-US and parsed with a bare new Date(...),
so the format chosen in Settings applied on some screens and not others,
and a bare yyyy-MM-dd due date showed as the previous day for every
viewer west of UTC. That copy is gone; the import path now resolves to
the implementation in date.util.ts, which reads the user's settings and
shares one formatter with the async path. formatTimeSync and
formatDateTimeSync join it, and the settings are mirrored into
localStorage so the first paint is already in the chosen format.
Two standing guards, since a one-time sweep is what missed the last
round: portableSql.test.ts now also fails on toISOString() written to
a timestamp column, and the dialect tests evaluate their expressions
against a real engine instead of pinning the text — a pinned string is
equally happy with a format that does not parse.
Verified against MySQL 9.7 and SQLite: 2363 tests, both drivers, nothing
skipped. A real upgrade of a database carrying both legacy shapes
reorders correctly afterwards.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com