Skip to content

fix(utils): now() returns timezone-aware UTC - #11

Closed
jcfrei wants to merge 1 commit into
masterfrom
fix/now-utc
Closed

fix(utils): now() returns timezone-aware UTC#11
jcfrei wants to merge 1 commit into
masterfrom
fix/now-utc

Conversation

@jcfrei

@jcfrei jcfrei commented Jul 29, 2026

Copy link
Copy Markdown
Contributor

Why

Surfaced by a CRM note set via the API showing ~2h behind the actual Zürich time.

now() returned a naive isoformat() — no timezone. A client parsing that string (new Date(...)) reads a zone-less datetime as browser-local, so a timestamp stamped on a UTC server renders shifted by the viewer's UTC offset. Notes whose timestamp the frontend set via toISOString() (with Z) rendered correctly, so the two producers of the same instant disagreed on encoding — API/chat-set timestamps drifted, UI-set ones didn't.

Fix

now()datetime.now(timezone.utc).isoformat() (…+00:00). Every stored creation / modified / occurred_at is now an unambiguous instant that round-trips correctly. nowdate() is unchanged — date-only fields (posting_date, due_date) stay zone-free.

Safety

  • No core code parses these datetime strings back for comparison — the only fromisoformat calls are on date-only fields (date.fromisoformat), which use nowdate().
  • api/auth.py already uses aware UTC (datetime.now(timezone.utc)) for JWT expiry, so nothing mixes naive/aware datetimes.
  • Columns are TEXT, ordered lexicographically; new (…+00:00) and old (naive) values share the fixed-width YYYY-MM-DDTHH:MM:SS.ffffff prefix, so time ordering is preserved.
  • Existing rows keep their stored (naive) value — only newly written timestamps carry the zone.

Test

tests/test_utils_now.py asserts now() is aware UTC and nowdate() is date-only; wired into CI as a backend-agnostic step.

🤖 Generated with Claude Code

… instant

now() returned a naive isoformat() (no zone). A client parsing it (JS
`new Date(...)`) reads a zone-less datetime as browser-local, so a timestamp
stamped on a UTC server rendered shifted by the viewer's UTC offset — a note set
server-side showed ~2h behind in Zürich, while a record whose timestamp the
frontend set via toISOString() (with Z) showed correctly. The two producers
disagreed on the encoding of the same instant.

now() now returns datetime.now(timezone.utc).isoformat() (…+00:00), so every
stored creation/modified/occurred_at is an unambiguous instant. nowdate() is
unchanged (date-only fields stay zone-free). No core code parses these datetime
strings back for comparison (only date-only fields via date.fromisoformat), and
auth already uses aware UTC for JWT expiry — so nothing mixes naive/aware.
Existing rows keep their naive value; only new writes carry the zone.

tests/test_utils_now.py guards the format (aware UTC datetime, date-only date);
wired into CI as a backend-agnostic step.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
jcfrei added a commit that referenced this pull request Jul 29, 2026
fix(utils): now() returns timezone-aware UTC — timestamps read as one instant (#11)

now() returned a naive isoformat(); a client parsing it as browser-local
rendered UTC-server timestamps shifted by the viewer's offset (e.g. an
API-set CRM note ~2h behind in Zürich). now() now returns
datetime.now(timezone.utc).isoformat(). nowdate() unchanged.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@jcfrei

jcfrei commented Jul 29, 2026

Copy link
Copy Markdown
Contributor Author

Squash-merged into master as 3e2dc31 and released as v0.6.7.

@jcfrei jcfrei closed this Jul 29, 2026
@jcfrei
jcfrei deleted the fix/now-utc branch July 29, 2026 10:09
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant