Compute day boundaries in the user's timezone, not the browser's#2934
Open
rosa wants to merge 2 commits into
Open
Compute day boundaries in the user's timezone, not the browser's#2934rosa wants to merge 2 commits into
rosa wants to merge 2 commits into
Conversation
The "today/yesterday" labels and other relative date displays compared day boundaries using beginningOfDay(), which snapped to midnight with the browser's local Date methods. #2790 swapped these for UTC methods, which fixed UTC-negative users in one direction but broke the symmetric case (cards created earlier the same day showing "yesterday" after local 7 PM), so it was reverted in 6d71385. Both attempts share a flaw: the client picked a day boundary that could disagree with the server. The server groups and renders days using the timezone from the `timezone` cookie (CurrentTimezone), but the client followed the browser's resolved timezone — which in a PWA/webview can resolve to UTC even when the cookie correctly holds the user's IANA zone. The two then disagreed by a day around the local-vs-UTC midnight boundary. Snap beginningOfDay() to midnight in the cookie's timezone instead, so the client's day boundaries match the server's. Falls back to the runtime timezone when the cookie is absent or invalid. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
This PR updates client-side “day boundary” calculations to align with the server’s timezone selection (from the timezone cookie), addressing off-by-one relative date labels for users whose browser-resolved timezone can differ from the server’s (notably in PWA/webview scenarios).
Changes:
- Reworks
beginningOfDay()to snap to midnight based on Y/M/D in the cookie timezone rather than the browser’s localDatemethods. - Introduces a cached
Intl.DateTimeFormat(...).formatToParts(...)formatter keyed by the current cookie timezone, with a fallback to the runtime timezone when invalid.
Tip
If you aren't ready for review, convert to a draft PR.
Click "Convert to draft" or run gh pr ready --undo.
Click "Ready for review" or run gh pr ready to reengage.
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Comment on lines
+52
to
+55
| function currentTimezone() { | ||
| const cookie = document.cookie.split("; ").find(entry => entry.startsWith("timezone=")) | ||
| return cookie ? decodeURIComponent(cookie.split("=")[1]) : undefined | ||
| } |
The server already resolves the `timezone` cookie into Time.zone (in CurrentTimezone) and renders day groupings with it. Rather than have the client re-read and parse the cookie, expose the resolved zone in a `timezone` meta tag and read it with getMetaContent — the same pattern bc3 uses. This makes the server the single source of truth: the client snaps day boundaries to the exact zone the server rendered with, and falls back to the runtime zone if the tag is missing or invalid. Co-Authored-By: Claude Opus 4.8 (1M context) <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.
Summary
Fixes the recurring bug where cards (and other relative dates) display as one day earlier for users in UTC-negative timezones — most recently reported by a customer in
America/Indianapoliswho saw cards created earlier the same day flip to "yesterday" after their local 7 PM.Card: https://app.basecamp.com/2914079/buckets/27/card_tables/cards/9749169131
Background
This is the third attempt at this bug:
beginningOfDay()snapped to midnight with the browser's localDatemethods.6d7138584(that revert also addedTime.zone.nameto the day-timeline fragment cache keys — kept here).Root cause
Both prior attempts share the same flaw: the client chose a day boundary that could disagree with the server.
timezonecookie (CurrentTimezone#set_current_timezone).beginningOfDay) followed the browser's resolved timezone — which in a PWA/webview can resolve to UTC even when the cookie correctly holds the user's IANA zone.So around the local-vs-UTC midnight boundary, client and server disagreed by a day. Picking local vs UTC just moved which side broke.
Fix
Make the server the single source of truth for the timezone and have the client snap day boundaries to that exact zone:
Time.zone. Expose it as a<meta name="timezone">tag in the shared head (Time.zone.name, which is the IANA identifier, orUTCwhen there's no cookie).getMetaContenthelper (mirrors the bc3 pattern) and havebeginningOfDay()read the meta tag — no cookie parsing in JS.beginningOfDay()usesIntl.DateTimeFormat({ timeZone })→formatToPartsto read Y/M/D in that zone, then anchors toDate.UTC(...)(both comparison anchors constructed the same way → exact, DST-proof day deltas). Falls back to the runtime timezone if the tag is missing or invalid.The client now always agrees with the server, whatever zone the server rendered with.
Verification
No JS unit-test harness exists in this repo (importmap, no
package.json), so the logic was checked standalone against the reported scenarios:Confirmed
ActiveSupport::TimeZone["America/New_York"].namereturns the IANA identifier ("America/New_York"), soTime.zone.nameis safe to feed toIntl.DateTimeFormat. Worth a manual check in the PWA before deploy.🤖 Generated with Claude Code