Skip to content

fix(devices): emit stored timestamps as explicit UTC instants - #168

Merged
jherforth merged 1 commit into
jherforth:mainfrom
mrramam:fix/device-updatetime-utc
Sep 14, 2026
Merged

jherforth merged 1 commit into
jherforth:mainfrom
mrramam:fix/device-updatetime-utc

Conversation

@mrramam

@mrramam mrramam commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

The Devices tab shows Last Updated about seven hours ahead of the truth in US/Pacific.

CURRENT_TIMESTAMP is always UTC by SQLite's specification it ignores TZ, so a correctly configured container still writes UTC 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 lands an offset away from the stored instant:

stored  "2026-09-02 04:50:32"
shown    9/2/2026, 4:50:32 AM
correct  9/1/2026, 9:50:32 PM

Host or container timezone settings cannot fix this, and changing them is not the answer.

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.

Sweeping for other readers of the same columns turned up two more, both unreported:

  • getDeviceUpdateTimeMs feeds the Last-Modified header 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/status same offset, user-visible.

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. 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.

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 jherforth added the bug Something isn't working label Sep 14, 2026
@jherforth jherforth added this to the 1.9 milestone Sep 14, 2026
@jherforth
jherforth merged commit 1ea784f into jherforth:main Sep 14, 2026
2 checks passed
@github-project-automation github-project-automation Bot moved this from Backlog to Done in HomeGlow Kanban Sep 14, 2026
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>
@mrramam
mrramam deleted the fix/device-updatetime-utc branch September 14, 2026 17:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

2 participants