fix(devices): emit stored timestamps as explicit UTC instants - #168
Merged
Merged
Conversation
The Devices tab showed Last Updated about seven hours ahead of the truth in US/Pacific. SQLite's CURRENT_TIMESTAMP is always UTC and ignores TZ, and it writes "YYYY-MM-DD HH:MM:SS" with no zone marker. V8 reads a space-separated, offset-less timestamp as local time, so every consumer landed an offset away from the stored instant. The digits look plausible, which is why it survived. Container and host timezone configuration cannot fix this; the backend already has the correct TZ and still writes UTC, as specified. Timestamps now become ISO-8601 with an explicit Z at the API boundary, through one helper, rather than each caller knowing the storage convention. Storage is unchanged, so no migration and no mixed formats in ORDER BY. Three consumers were reading the ambiguous form: - GET /api/devices — the Last Updated column, the reported symptom. - getDeviceUpdateTimeMs — feeds the Last-Modified header on three device endpoints, which advertised a time in the future west of UTC. ETag drives the 304, so this was wrong rather than harmful. - GET /api/connections/google/status — "Connected <time>", same offset. Found by sweeping for other columns written by CURRENT_TIMESTAMP. Unusable values return null rather than the raw string: a client renders null as Unknown, but renders an ambiguous string as a confidently wrong local time. The tests set a non-UTC zone and assert the naive parse is wrong there before asserting the helper is right — in a UTC process, which is where CI runs, both the bug and its fix are invisible. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
jherforth
added a commit
to mrramam/HomeGlow
that referenced
this pull request
Sep 14, 2026
jherforth#168 has landed on main as a squash, so this branch's own copy of that commit collided with it. The shared files -- utils/sqliteTime.js and its unit tests -- are byte-identical between the two, so nothing from the review of jherforth#168 is lost by taking either side. The only real conflict was in apiEndpoints.test.js, where both sides append a test to the same place: main's device-updateTime test from jherforth#168 and this branch's chore-history created_at test. Both are kept. server/index.js auto-merged; checked by hand rather than trusted -- one import of the helpers, jherforth#168's four call sites intact, and withIsoCreatedAt applied at all three chore-history endpoints. Verified: server 224/224, client 272/272, translation parity, build clean, and the formatter tests pass under America/Los_Angeles and Asia/Tokyo as well as UTC. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The Devices tab shows Last Updated about seven hours ahead of the truth in US/Pacific.
CURRENT_TIMESTAMPis always UTC by SQLite's specification it ignoresTZ, so a correctly configured container still writes UTC and it writesYYYY-MM-DD HH:MM:SSwith no zone marker. V8 reads a space-separated, offset-less timestamp as local time, so every consumer lands an offset away from the stored instant:Host or container timezone settings cannot fix this, and changing them is not the answer.
Timestamps now become ISO-8601 with an explicit
Zat the API boundary, through one helper, rather than each caller knowing the storage convention. Storage is unchanged.Sweeping for other readers of the same columns turned up two more, both unreported:
getDeviceUpdateTimeMsfeeds theLast-Modifiedheader on three device endpoints, so west of UTC that header advertised a time in the future. ETag drives the 304, so this was wrong rather than harmful.GET /api/connections/google/statussame offset, user-visible.Unusable values return
nullrather than the raw string: a client rendersnullas "Unknown", but renders an ambiguous string as a confidently wrong local time. Both render sites already guard the falsy case.Testing. Server suite passes with one new endpoint test. The unit tests set a non-UTC zone and assert the naive parse is wrong there before asserting the helper is right - in a UTC process, which is where CI runs, both the bug and the fix are invisible, so a test written without that control passes either way.