Skip to content

fix(server): sort calendar events by instant, not offset text - #171

Open
charan-rathore wants to merge 2 commits into
CopilotKit:mainfrom
charan-rathore:fix/calendar-mixed-offset-order
Open

charan-rathore wants to merge 2 commits into
CopilotKit:mainfrom
charan-rathore:fix/calendar-mixed-offset-order

Conversation

@charan-rathore

Copy link
Copy Markdown
Contributor

The three server calendar read paths sorted ISO timestamp strings lexically, which orders mixed-offset events wrong: 14:00Z was returned after 09:00-07:00 even though it starts an hour earlier. This compares parsed instants instead in events(), snapshot(), and sectionSnapshot(calendar). All-day civil-date strings keep the existing Date.parse UTC convention; this is not an all-day-zone redesign.

What I ran:

  • New tests/calendar-event-order.test.ts covers all three read paths with mixed-offset timed events. It fails against the old string sort (one assertion, reversed order) and passes with the instant comparison.
  • With the fix: the new regression plus api, domain, google, and workflows suites, 73 passed.
  • tsc --noEmit and Biome on the touched files are clean.

Not run: conversation-calendar.test.ts SIGKILLs after five passing tests on both the clean base and this branch in my environment (resource limit, not an assertion failure), so I am not claiming that suite. No live Google or mobile visual validation; the change is server sorting only.

The three server calendar read paths sorted ISO timestamp strings
lexically, which orders mixed-offset events wrong: 14:00Z was returned
after 09:00-07:00 even though it starts an hour earlier. Compare parsed
instants instead in events(), snapshot(), and sectionSnapshot(calendar).
All-day civil-date strings keep the existing Date.parse UTC convention;
this is not an all-day-zone redesign.

Adds tests/calendar-event-order.test.ts covering all three read paths
with mixed-offset timed events; it fails against the old string sort
and passes with the instant comparison.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>

@NathanTarbert NathanTarbert left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @charan-rathore. Sorting by the actual moment an event starts, instead of by how the time is written, is the right fix, and the server side does that now. Two things keep it from reaching users yet.

The mobile app sorts the events again by their text. The Calendar screen does it at screens.tsx:655, and the Today screen does it at screens.tsx:79. Both use a.start.localeCompare(b.start), so they undo the server's order, and people still see a 9:00 Pacific meeting listed before a 14:00 UTC one. Updating those two places as well would finish the fix.

All-day events can land in the wrong spot. An all-day event's start is just a date, like 2026-10-10, and Date.parse reads that as midnight UTC. For someone in Pacific time, an all-day event on the 10th now sorts before a meeting on the evening of the 9th, which the old text sort got right. In zones ahead of UTC, it goes the other way, and morning meetings sort ahead of that day's all-day event. Treating all-day events as the start of that day in the user's own time zone, or sorting them by date before timed events, would keep them in place.

A start time that doesn't parse can scramble the list. Date.parse returns NaN for a missing or malformed start. That makes the comparison inconsistent, so events near the bad one can come back in a random order. The old text sort always gave a stable order. Falling back to the text comparison when either value doesn't parse would keep that safety.

The same comparison is written out three times in workspace.ts, at lines 107, 331 and 389. That's probably how the two copies in the app were missed. A single compareEventStart in packages/domain, used by both the server and the app, would keep all of them in step and give the all-day handling one home.

…-day and fallback handling

The mobile app re-sorted events by their start text (Calendar and Today
screens), undoing the server order. A date-only all-day start parsed as
midnight UTC, placing it inside the previous day west of Greenwich and
after same-morning events east of it. An unparseable start made
Date.parse comparisons inconsistent.

Add compareEventStart to packages/domain: timed events order by
instant, all-day (or date-only) events anchor to the start of their day
in their own time zone, and an unparseable start falls back to the text
comparison so the order stays total. Use it in all three server sort
sites and both app screens; move zonedInstant and startOfZonedDay into
the domain package so both sides share them.

Signed-off-by: Charan Rathore <180254320+charan-rathore@users.noreply.github.com>
@charan-rathore

Copy link
Copy Markdown
Contributor Author

Thanks, all four points check out. Pushed a round that finishes the fix:

The app no longer undoes the order: the Calendar and Today screens were re-sorting by start text, and both now use the same comparator as the server.

All-day events anchor to their own zone. A date-only start ("2026-10-10") is now read as the start of that day in the event's timeZone instead of midnight UTC, so it lands after the previous evening's meeting in Pacific and before a same-morning meeting in Auckland. startOfZonedDay already clamps DST-gap midnights, and a date-only start is treated as all-day even when the flag is missing.

Unparseable starts are stable again: when either side fails Date.parse, the comparator falls back to the text comparison, so the order stays total instead of depending on sort internals.

The comparison now lives once in packages/domain as compareEventStart, used by all three workspace.ts sites and both app screens. zonedInstant and startOfZonedDay moved into the domain package so both sides share them.

Tests: the two new service-level cases (all-day placement, unparseable fallback) failed on the previous code and pass now, plus comparator table cases for both zone directions and the date-only shape. 4/4 calendar tests and 32/32 mobile tests pass; tsc --noEmit is clean for the server and the app; biome is clean. I did not run the app UI itself.

This branch has not been deployed

No deployments
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