Skip to content

Make SERVER_TIMEZONE authoritative for streaks, coach, and day boundaries#219

Merged
d3mocide merged 1 commit into
mainfrom
claude/streaks-timezone-config-26ci4r
Jun 16, 2026
Merged

Make SERVER_TIMEZONE authoritative for streaks, coach, and day boundaries#219
d3mocide merged 1 commit into
mainfrom
claude/streaks-timezone-config-26ci4r

Conversation

@d3mocide

Copy link
Copy Markdown
Owner

Problem

Two timezone symptoms were reported:

  1. The coach reports in UTC in the evening and pulls data on the wrong day boundaries.
  2. The streak looked broken — the "Streak Details" header showed 0 Day Streak while the breakdown below listed Jun 15 / 14 / 13 each at 3/4 targets met (which should count).

SERVER_TIMEZONE is documented as the single source of truth for calendar-day boundaries (.env.example), but several paths ignored it.

Root causes & fixes

Coach reporting UTC

  • llm_client._inject_current_datetime prepended Current date/time (UTC): … to every LLM call using datetime.now(UTC). → Now injects the configured SERVER_TIMEZONE clock (with %Z).

Coach / rollups pulling UTC days

  • coach.query_nutrition_rollup grouped + filtered by hardcoded AT TIME ZONE 'UTC' (and filtered on CAST(ts AS date)). → Now groups and filters by :tz = SERVER_TIMEZONE.
  • coach_context logging-consistency count, and the daily biometric buckets in goals.recommend / profile_sync, all used hardcoded UTC even though their windows were server-local. → All bucket in SERVER_TIMEZONE.

Streaks driven by the browser, not the server

  • The frontend sent ?tz=<browserTz> to /today, /today/streak-history, /water, and /health/*, overriding the configured server timezone. → Backend now always resolves to SERVER_TIMEZONE and ignores the client hint; the frontend stops sending it and derives "Today" from the server's date.

Streak headline ≠ breakdown (the screenshot)

  • The /today headline streak added today's supplement nutrients to every historical day, which could flip earlier days off-track — so the headline read 0 while the per-day list still showed them on-track. → Supplements are now credited to the day they were actually logged, mirroring /today/streak-history (including supplement-only days).

Testing

  • Backend: full suite 185 passed; ruff + mypy (touched files) clean.
  • Frontend: pnpm type-check, pnpm lint (0 errors), pnpm test 97 passed. Updated WaterCard.test.tsx query keys.

Note

This intentionally removes the per-device timezone override so the configured server timezone governs everywhere, per the request that everything be pulled based on the server's timezone setting. The notification "nudge timezone" preference is left untouched (it's an explicit user setting, not a day-boundary override).

https://claude.ai/code/session_01NhMPok8EUdmK9o7pDKEx4a


Generated by Claude Code

…ries

The coach was reporting time in UTC and pulling rollups on UTC day
boundaries, and the streak headline could read 0 while the breakdown
showed on-track days. Root causes:

- llm_client injected "Current date/time (UTC)" into every LLM call, so
  the coach narrated the wrong evening date. Now injects the configured
  SERVER_TIMEZONE clock.
- coach query_nutrition_rollup and coach_context logging-consistency
  grouped by hardcoded UTC days; goals/profile_sync bucketed daily
  biometric totals by UTC even though their window was server-local.
  All now bucket in SERVER_TIMEZONE.
- /today and /today/streak-history (plus water and health "taken today")
  honored a client tz hint from the browser, overriding the configured
  server timezone. They now always use SERVER_TIMEZONE; the frontend no
  longer sends a tz param and derives "Today" from the server date.
- The /today headline streak added *today's* supplement nutrients to
  *every* historical day, flipping earlier days off-track and
  contradicting the per-day breakdown. Supplements are now credited to
  the day they were logged, mirroring /today/streak-history.

https://claude.ai/code/session_01NhMPok8EUdmK9o7pDKEx4a
@d3mocide d3mocide marked this pull request as ready for review June 16, 2026 19:42
@d3mocide d3mocide merged commit 20b1d46 into main Jun 16, 2026
4 checks passed
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.

2 participants