fix(calendar): update to node-ical 0.23.1 and fix full-day recurrence lookup (#4013)

Adapts calendar module to node-ical changes and fixes a bug with moved
full-day recurring events in eastern timezones.

## Changes

### 1. Update node-ical to 0.23.1
- Includes upstream fixes for UNTIL UTC validation errors from CalDAV
servers (reported by @rejas in PR #4010)
- Changes to `getDateKey()` behavior for VALUE=DATE events (now uses
local date components)
- Fixes issue with malformed DURATION values (reported by MagicMirror
user here: https://github.com/jens-maus/node-ical/issues/381)

### 2. Remove dead code
- Removed ineffective UNTIL modification code (rule.options is read-only
in rrule-temporal)
- The code attempted to extend UNTIL for all-day events but had no
effect

### 3. Fix recurrence lookup for full-day events
node-ical changed the behavior of `getDateKey()` - it now uses local
date components for VALUE=DATE events instead of UTC. This broke
recurrence override lookups for full-day events in eastern timezones.

**Why it broke:**
- **before node-ical update:** Both node-ical and MagicMirror used UTC →
keys matched 
- **after node-ical update:** node-ical uses local date (RFC 5545
conform), MagicMirror still used UTC → **mismatch** 

**Example:**
- Full-day recurring event on October 12 in Europe/Berlin (UTC+2)
- node-ical 0.23.1 stores override with key: `"2024-10-12"` (local date)
- MagicMirror looked for key: `"2024-10-11"` (from UTC: Oct 11 22:00)
- **Result:** Moved event not found, appears on wrong date

**Solution:** Adapt to node-ical's new behavior by using local date
components for full-day events, UTC for timed events.

**Note:** This is different from previous timezone fixes - those
addressed event generation, this fixes the lookup of recurrence
overrides.

## Background

node-ical 0.23.0 switched from `rrule` to `rrule-temporal`, introducing
breaking changes. Version 0.23.1 fixed the UNTIL validation issue and
formalized the `getDateKey()` behavior for DATE vs DATE-TIME values,
following RFC 5545 specification that DATE values represent local
calendar dates without timezone context.
This commit is contained in:
Kristjan ESPERANTO
2026-01-12 04:27:52 +01:00
committed by GitHub
parent 82e39a2476
commit 2d3a557864
3 changed files with 17 additions and 17 deletions

View File

@@ -66,11 +66,6 @@ const CalendarFetcherUtils = {
const searchFromDate = pastLocalMoment.clone().subtract(Math.max(durationInMs, oneDayInMs), "milliseconds").toDate();
const searchToDate = futureLocalMoment.clone().add(1, "days").toDate();
// For all-day events, extend "until" to end of day to include the final occurrence
if (isFullDayEvent && rule.options?.until) {
rule.options.until = moment(rule.options.until).endOf("day").toDate();
}
const dates = rule.between(searchFromDate, searchToDate, true) || [];
// Convert dates to moments in the event's timezone.
@@ -313,7 +308,12 @@ const CalendarFetcherUtils = {
let recurringEventStartMoment = startMoment.clone().tz(CalendarFetcherUtils.getLocalTimezone());
let recurringEventEndMoment = recurringEventStartMoment.clone().add(durationMs, "ms");
const dateKey = recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD");
// For full-day events, use local date components to match node-ical's getDateKey behavior
// For timed events, use UTC to match ISO string slice
const isFullDay = CalendarFetcherUtils.isFullDayEvent(event);
const dateKey = isFullDay
? recurringEventStartMoment.format("YYYY-MM-DD")
: recurringEventStartMoment.tz("UTC").format("YYYY-MM-DD");
// Check for overrides
if (curEvent.recurrences !== undefined) {

20
package-lock.json generated
View File

@@ -27,7 +27,7 @@
"ipaddr.js": "^2.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"node-ical": "^0.23.0",
"node-ical": "^0.23.1",
"nunjucks": "^3.2.4",
"pm2": "^6.0.14",
"socket.io": "^4.8.3",
@@ -950,9 +950,9 @@
}
},
"node_modules/@csstools/css-syntax-patches-for-csstree": {
"version": "1.0.24",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.24.tgz",
"integrity": "sha512-T0pSTcd9eYEHV+llVPSkZU7URdVGu87BpSvozMwRoLJYXmLXvEHgYfv0yDsQH9+DIdLzkJCOJBABqWWnwTGPvg==",
"version": "1.0.25",
"resolved": "https://registry.npmjs.org/@csstools/css-syntax-patches-for-csstree/-/css-syntax-patches-for-csstree-1.0.25.tgz",
"integrity": "sha512-g0Kw9W3vjx5BEBAF8c5Fm2NcB/Fs8jJXh85aXqwEXiL+tqtOut07TWgyaGzAAfTM+gKckrrncyeGEZPcaRgm2Q==",
"dev": true,
"funding": [
{
@@ -9426,9 +9426,9 @@
}
},
"node_modules/node-ical": {
"version": "0.23.0",
"resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.23.0.tgz",
"integrity": "sha512-1XMNdOVH1j1JgHdCrIzJ3MPYhPn0LZidrF65tjzGGJYgps1id8+bNd8qs8SuQMjxoKM5VPpfT5D8ro5y1MsBkw==",
"version": "0.23.1",
"resolved": "https://registry.npmjs.org/node-ical/-/node-ical-0.23.1.tgz",
"integrity": "sha512-4+FON2u++r7vU9VfTCris9hgxS5darN4P7aQZOJkzDazXLRS7cExb0G5OWO3CzRs8gHd5um1fdSMF0e45f7xAg==",
"license": "Apache-2.0",
"dependencies": {
"@js-temporal/polyfill": "^0.5.1",
@@ -12070,9 +12070,9 @@
"license": "MIT"
},
"node_modules/systeminformation": {
"version": "5.30.2",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.2.tgz",
"integrity": "sha512-Rrt5oFTWluUVuPlbtn3o9ja+nvjdF3Um4DG0KxqfYvpzcx7Q9plZBTjJiJy9mAouua4+OI7IUGBaG9Zyt9NgxA==",
"version": "5.30.3",
"resolved": "https://registry.npmjs.org/systeminformation/-/systeminformation-5.30.3.tgz",
"integrity": "sha512-NgHJUpA+y7j4asLQa9jgBt+Eb2piyQIXQ+YjOyd2K0cHNwbNJ6I06F5afOqOiaCuV/wrEyGrb0olg4aFLlJD+A==",
"license": "MIT",
"os": [
"darwin",

View File

@@ -92,7 +92,7 @@
"ipaddr.js": "^2.3.0",
"moment": "^2.30.1",
"moment-timezone": "^0.6.0",
"node-ical": "^0.23.0",
"node-ical": "^0.23.1",
"nunjucks": "^3.2.4",
"pm2": "^6.0.14",
"socket.io": "^4.8.3",