Skip to content

fix(calendar): correct day-of-week for full-day recurring events across all timezones#4004

Merged
sdetweil merged 1 commit intoMagicMirrorOrg:developfrom
KristjanESPERANTO:calendar
Jan 4, 2026
Merged

fix(calendar): correct day-of-week for full-day recurring events across all timezones#4004
sdetweil merged 1 commit intoMagicMirrorOrg:developfrom
KristjanESPERANTO:calendar

Conversation

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator

@KristjanESPERANTO KristjanESPERANTO commented Jan 3, 2026

Fixes full-day recurring events showing on wrong day in timezones west of UTC (reported in #4003).

Root cause: moment.tz(date, eventTimezone).startOf("day") interprets UTC midnight as local time:

  • 2025-11-03T00:00:00.000Z in America/Chicago (UTC-6)
  • → Converts to 2025-11-02 18:00:00 (6 hours back)
  • .startOf("day")2025-11-02 00:00:00Wrong day!

Impact: The bug affects:

  • All timezones west of UTC (UTC-1 through UTC-12): Americas, Pacific
  • Timezones east of UTC (UTC+1 through UTC+12): Europe, Asia, Africa - work correctly
  • UTC itself - works correctly

The issue was introduced with commit c2ec6fc (#3976), which fixed the time but broke the date. This PR fixes both.

Result Day Time Notes
Before c2ec6fc 2025-11-03 05:00:00 Monday Wrong time, but correct day
Current (c2ec6fc) 2025-11-02 00:00:00 Sunday ❌ (west of UTC)
✅ (east of UTC)
Wrong day - visible bug!
This fix 2025-11-03 00:00:00 Monday Correct in all timezones

Note: While the old logic had incorrect timing, it produced the correct calendar day due to how it handled UTC offsets. The current logic fixed the timing issue but introduced the more visible calendar day bug.

Solution

Extract UTC date components and interpret as local calendar dates:

const utcYear = date.getUTCFullYear();
const utcMonth = date.getUTCMonth();
const utcDate = date.getUTCDate();
return moment.tz([utcYear, utcMonth, utcDate], eventTimezone);

Testing

To prevent this from happening again in future refactorings, I wrote a test for it.

npm test -- tests/unit/modules/default/calendar/calendar_fetcher_utils_spec.js

@sdetweil
Copy link
Copy Markdown
Collaborator

sdetweil commented Jan 3, 2026

Is the a matching edge case with one hour close to
DST which should move forward , but doesn’t

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator Author

I initially thought this was DST-related too, but after testing I just updated the PR text - it's not about DST.

The bug affects all full-day events in western timezones (UTC-1 to UTC-12), regardless of DST:

// Western timezones: Always wrong
moment.tz(new Date("2025-11-03T00:00:00Z"), "America/Chicago")
// → 2025-11-02 18:00 → .startOf("day") → 2025-11-02 ❌

// Eastern timezones: Always correct
moment.tz(new Date("2025-11-03T00:00:00Z"), "Asia/Tokyo")  
// → 2025-11-03 09:00 → .startOf("day") → 2025-11-03 ✅

No matching edge case exists - the bug is asymmetric. UTC midnight converts to the previous day in western timezones, but same day in eastern timezones. That's why eastern timezones work correctly by design.

…ss DST

Full-day recurring events were showing on wrong day after DST transitions.
rrule.js returns UTC dates that shift calendar days when offset changes.

Fix: Extract UTC date components and interpret as local calendar dates,
preserving the intended day regardless of DST offset changes.

Added test for weekly Monday event crossing DST boundary.

Fixes: MagicMirrorOrg#4003
Regression from: c2ec6fc
@KristjanESPERANTO KristjanESPERANTO changed the title fix(calendar): correct day-of-week for full-day recurring events across DST fix(calendar): correct day-of-week for full-day recurring events across all timezones Jan 3, 2026
@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator Author

KristjanESPERANTO commented Jan 3, 2026

I just fixed a comment that still referred to DST. No changes in the logic.

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator Author

KristjanESPERANTO commented Jan 4, 2026

I'm too tired to continue researching right now, but after further investigation, it seems to me that the old logic before c2ec6fc is not as wrong as I described it above 🤯. But that doesn't change the fact that we need a fix (this PR) now.

I hope that we switch to Temporal at some point, then all the time zone stuff will be a little less confusing.

@sdetweil
Copy link
Copy Markdown
Collaborator

sdetweil commented Jan 4, 2026

No rush. Not shipping til April 1

@sdetweil
Copy link
Copy Markdown
Collaborator

sdetweil commented Jan 4, 2026

User reports PR fixed issue

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator Author

Great 🙂 Then we can merge it, right?

@sdetweil sdetweil merged commit 40301f2 into MagicMirrorOrg:develop Jan 4, 2026
9 checks passed
@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator Author

@sdetweil Is the problem serious enough that we should do a fix release? 🤔

@sdetweil
Copy link
Copy Markdown
Collaborator

sdetweil commented Jan 4, 2026

no, we can teach them how to get the PR

@KristjanESPERANTO
Copy link
Copy Markdown
Collaborator Author

Okay, I think that's a good call 👍

@KristjanESPERANTO KristjanESPERANTO deleted the calendar branch January 5, 2026 13:53
@khassel khassel mentioned this pull request Apr 1, 2026
khassel added a commit that referenced this pull request Apr 1, 2026
## Release Notes
Thanks to: @angeldeejay, @in-voker, @JHWelch, @khassel,
@KristjanESPERANTO, @rejas, @sdetweil
> ⚠️ This release needs nodejs version >=22.21.1 <23 || >=24 (no change
to previous release)

[Compare to previous Release
v2.34.0](v2.34.0...v2.25.0)

> ⚠️ We introduced some internal changes with this release, please read
[this forum
post](https://forum.magicmirror.builders/topic/20138/upcoming-release-april-1-2026-breaking-changes-some-operational-changes)
before upgrading!

### [core]
- Prepare Release 2.35.0 (#4071)
- docs: add security policy and vulnerability reporting guidelines
(#4069)
- refactor: simplify internal `require()` calls (#4056)
- allow environment variables in cors urls (#4033)
- fix cors proxy getting binary data (e.g. png, webp) (#4030)
- fix: correct secret redaction and optimize loadConfig (#4031)
- change loading config.js, allow variables in config.js and try to
protect sensitive data (#4029)
- remove kioskmode (#4027)
- Add dark theme logo (#4026)
- move custom.css from css to config (#4020)
- move default modules from /modules/default to /defaultmodules (#4019)
- update node versions in workflows (#4018)
- [core] refactor: extract and centralize HTTP fetcher (#4016)
- fix systeminformation not displaying electron version (#4012)
- Update node-ical and support it's rrule-temporal changes (#4010)
- Change default start scripts from X11 to Wayland (#4011)
- refactor: unify favicon for index.html and Electron (#4006)
- [core] run systeminformation in subprocess so the info is always
displayed (#4002)
- set next release dev number (#4000)

### [dependencies]
- update dependencies (#4068)
- update dependencies incl. electron to v41 (#4058)
- chore: upgrade ESLint to v10 and fix newly surfaced issues (#4057)
- chore: update ESLint and plugins, simplify config, apply new rules
(#4052)
- chore: update dependencies + add exports, files, and sideEffects
fields to package.json (#4040)
- [core] refactor: enable ESLint rule require-await and handle detected
issues (#4038)
- Update node-ical and other deps (#4025)
- chore: update dependencies (#4021)
- chore(eslint): migrate from eslint-plugin-vitest to
@vitest/eslint-plugin and run rules only on test files (#4014)
- Update deps as requested by dependabot (#4008)
- update Collaboration.md and dependencies (#4001)

### [logging]
- refactor: further logger clean-up (#4050)
- Fix Node.js v25 logging prefix and modernize logger (#4049)

### [modules/calendar]
- fix(calendar): make showEnd behavior more consistent across time
formats (#4059)
- test(calendar): fix hardcoded date in event shape test (#4055)
- [calendar] refactor: delegate event expansion to node-ical's
expandRecurringEvent (#4047)
- calendar.js: remove useless hasCalendarURL function (#4028)
- fix(calendar): update to node-ical 0.23.1 and fix full-day recurrence
lookup (#4013)
- fix(calendar): correct day-of-week for full-day recurring events
across all timezones (#4004)

### [modules/newsfeed]
- fix(newsfeed): fix full article view and add framing check (#4039)
- [newsfeed] refactor: migrate to centralized HTTPFetcher (#4023)

### [modules/weather]
- fix(weather): fix openmeteo forecast stuck in the past (#4064)
- fix(weather): fix weathergov forecast day labels off by one (#4065)
- weather: fixes for templates (#4054)
- weather: add possibility to override njk's and css (#4051)
- Use getDateString in openmeteo (#4046)
- [weather] refactor: migrate to server-side providers with centralized
HTTPFetcher (#4032)
- [weather] feat: add Weather API Provider  (#4036)

### [testing]
- chore: remove obsolete Jest config and unit test global setup (#4044)
- replace template_spec test with config_variables test (#4034)
- refactor(clientonly): modernize code structure and add comprehensive
tests (#4022)
- Switch to undici Agent for HTTPS requests (#4015)
- chore: migrate CI workflows to ubuntu-slim for faster startup times
(#4007)

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: Kristjan ESPERANTO <35647502+KristjanESPERANTO@users.noreply.github.com>
Co-authored-by: Bugsounet - Cédric <github@bugsounet.fr>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: sam detweiler <sdetweil@gmail.com>
Co-authored-by: Veeck <github@veeck.de>
Co-authored-by: veeck <gitkraken@veeck.de>
Co-authored-by: Magnus <34011212+MagMar94@users.noreply.github.com>
Co-authored-by: Ikko Eltociear Ashimine <eltociear@gmail.com>
Co-authored-by: DevIncomin <56730075+Developer-Incoming@users.noreply.github.com>
Co-authored-by: Nathan <n8nyoung@gmail.com>
Co-authored-by: mixasgr <mixasgr@users.noreply.github.com>
Co-authored-by: Savvas Adamtziloglou <savvas-gr@greeklug.gr>
Co-authored-by: Konstantinos <geraki@gmail.com>
Co-authored-by: OWL4C <124401812+OWL4C@users.noreply.github.com>
Co-authored-by: BugHaver <43462320+bughaver@users.noreply.github.com>
Co-authored-by: BugHaver <43462320+lsaadeh@users.noreply.github.com>
Co-authored-by: Koen Konst <koenspero@gmail.com>
Co-authored-by: Koen Konst <c.h.konst@avisi.nl>
Co-authored-by: dathbe <github@beffa.us>
Co-authored-by: Marcel <m-idler@users.noreply.github.com>
Co-authored-by: Kevin G. <crazylegstoo@gmail.com>
Co-authored-by: Jboucly <33218155+jboucly@users.noreply.github.com>
Co-authored-by: Jboucly <contact@jboucly.fr>
Co-authored-by: Jarno <54169345+jarnoml@users.noreply.github.com>
Co-authored-by: Jordan Welch <JordanHWelch@gmail.com>
Co-authored-by: Blackspirits <blackspirits@gmail.com>
Co-authored-by: Samed Ozdemir <samed@xsor.io>
Co-authored-by: in-voker <58696565+in-voker@users.noreply.github.com>
Co-authored-by: Andrés Vanegas Jiménez <142350+angeldeejay@users.noreply.github.com>
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