fix(calendar): render all-day events on the date the calendar published - #159
fix(calendar): render all-day events on the date the calendar published#159mrramam wants to merge 1 commit into
Conversation
All-day events show one day early for any viewer west of UTC: a Friday
"NO SCHOOL" appears on Thursday in America/Los_Angeles.
An all-day event is a floating date. Google sends {"date": "2026-09-11"}
with no timezone. calendarSync parses that with new Date(), which reads a
date-only string as UTC midnight, and stores it via toISOString(), so the
API serves "2026-09-11T00:00:00.000Z" — asserting a timezone the source
date never had.
The client is then correct given what it is told: moment(event.start)
converts the asserted instant to local time, landing on 2026-09-10 17:00
in PDT, which satisfies the Thursday bucket and fails the Friday one.
Nothing mishandles UTC; something claims UTC for a value that had no zone.
The all_day flag is the only surviving record of the original intent, so
use it. eventDates() takes the UTC date part — the real datum — and
reinterprets it as a local date, applied at the widget's single event
normalisation point so every downstream comparison, sort and format
follows. Correct in any viewer timezone, no data migration.
This is a read-side correction. Storage still asserts a zone the source
date did not have, and every other consumer of /api/calendar-events
inherits the same trap; that is tracked separately.
|
I thought that we are using the node-ical dateOnly property when we parse ical events as described in their project page. Can you tell me more about why none of that is mentioned in this PR and if there is a more targeted fix for this not getting used instead of a workaround extra property? We definitely want to ensure that you can have events that start at midnight without being coerced into all-day activities, glad to see that's not being offered at all as a solution. |
|
<Updated: line numbers are wrong, Not correcting the> Current tip of main behavior:
So a minimal fix is to make Making them consistent ties correctness to the server's time zone such that a viewer west of it still sees the previous day. I read HG from a laptop and travel, so that one's real for me. The fundamental issue is that the date for an all-day event is most correctly stored as a date sans time while today ICS invents "midnight where the server is" while Google invents "midnight in UTC". A remedy for travelers is a viewer-side follow-up: interpret an all-day instant in the server's time zone rather than the browser's ie getServerTimezoneSync() already exists and is cached. So while I don't recommend accepting this PR, I do think there is a bug to be fixed here. It can be made TZ friendly by agreeing to use server local as the definitive time and teaching the clients to work with that or by changing the storage to accept dates without times. |
|
So if it fair to state that the issue is that when server and client timezone are different, all day events aren't working correctly? Does that apply only to google or to all 4 event sources? I just changed the TZ of the demo instance to NY and my pacific timezone (-7) is still working right for events and all day items (using the ICS/ical provider). I'd rather make it so that the system displays things in the client local timezone and so that it handles all day events just fine instead of doing something like forcing everything to be in server timezone. |
|
Not really, there are two different failure modes... Google is broken even when server and client agree. It stores The other three are the mismatch case. They store midnight in the server's zone, so they are right when the viewer shares that offset and wrong when the viewer is behind it. The demo has the bug, look at Thanksgiving this year which is Nov 26, 2026 - it shows as Wednesday the 25th.
Agreed, and I think that means not storing an all-day date as an instant at all. Two notes on doing it that way:
The |
|
When I look at Thanksgiving on my pacific time computer, I see it on thursday like you'd expect, so I'm not seeing the same that you are. Hmmmm |
|
Confirmed Safari and Firefox show the same as Chrome (screenshot above is Chrome) |
Is your device normally Pacific Time or did you change it for the test? |
|
WHOA. My UTC-7 is actually from Arizona time. If I swap to PDT directly, I see it on wed too. |


All-day calendar events render one day early for any viewer west of UTC. On a display in
America/Los_Angeles, a Friday "No School" shows on Thursday, and a Thursday bin-day event shows on Wednesday.Why
An all-day event is a floating date. Google sends it with no timezone:
server/services/calendarSync.js:241parses that withnew Date(start.date)— and a bareYYYY-MM-DDis parsed as UTC midnight — then:97stores it viatoISOString(). There is noall_daybranch on the insert, so the Google, ICS and Apple CalDAV paths all funnel through it. The API then serves:which asserts a timezone the source date never had.
Given that, the client is behaving correctly.
eventSpansDaydoesmoment(event.start), converting the asserted instant to local time —2026-09-10 17:00in PDT — which satisfies the Thursday bucket and fails the Friday one:Nothing mishandles UTC. Something claims UTC for a value that had no zone. East of UTC the same storage lands on the correct calendar day, which is why this is an Americas-only symptom.
What this changes
all_dayis the only surviving record of the original intent, so this uses it: take the UTC date part — the real datum — and reinterpret it as a local date.It is applied at
CalendarWidget's single event-normalisation point infetchCalendarEvents, so every downstream comparison, sort and format follows without touching the other ~15moment(event.start)call sites. Timed events are untouched: they carry a genuine instant and must keep converting normally.No server change, no data migration, no resync — existing cached rows are read differently, not rewritten.
This is a workaround, and I would rather say so
The underlying problem is that there is no timeless-day representation.
calendar_events_cache.start_timeisTEXTand would hold"2026-09-11"quite happily, andall_day INTEGERsits right beside it — but every writer goes throughtoISOString()and the reader rehydrates throughnew Date(), so the flag is carried end to end and never consulted when interpreting the date.That means every other consumer of
/api/calendar-eventsinherits the same trap with no signal that theZis fictional. This PR fixes the one consumer in this repo.A full fix branches on
all_dayat write and read across all three provider paths and needs a migration for cached rows. I kept that separate rather than folding it in, so a small and obviously-correct change does not turn into a risky one — happy to follow up with it if you would like it.Verified
New
calendarAllDay.test.js, 7 cases, run in four timezones —America/Los_Angeles,America/New_York,UTC,Australia/Sydney— since timezone is the whole failure mode. Full client suite passes (167), i18n parity unchanged, build clean.Replayed against a live production cache of 1,462 events in
America/Los_Angeles:Running in production on a family install.