From eafe2b964dd719bd479d20602e808907ac1e8d42 Mon Sep 17 00:00:00 2001 From: Ulas Date: Fri, 3 Apr 2026 12:10:58 +0200 Subject: [PATCH] fix(sync): resolve iCloud Calendar sync FOREIGN KEY crash and sync all calendars The Apple Calendar sync hardcoded created_by=1 which fails when no user with ID 1 exists, causing every single event import to fail silently. Now dynamically resolves the first available user. Also syncs all calendars instead of only the first one, adds the missing cfgDel helper, and gracefully skips unreachable calendars. Fixes #4 Co-Authored-By: Claude Opus 4.6 --- CHANGELOG.md | 8 +++ package.json | 2 +- server/services/apple-calendar.js | 104 +++++++++++++++++++----------- 3 files changed, 75 insertions(+), 39 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 00df7fa..ab6d193 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.5.5] - 2026-04-03 + +### Fixed +- Fix iCloud Calendar sync failing with FOREIGN KEY constraint error — `created_by` was hardcoded to user ID 1 instead of resolving dynamically (fixes #4) +- Sync all iCloud calendars instead of only the first one — previously only a single calendar was imported, ignoring Family, subscribed, and other calendars +- Add missing `cfgDel` helper function used by `clearCredentials` — disconnecting Apple Calendar would crash +- Skip unreachable or broken calendars gracefully instead of aborting the entire sync + ## [0.5.4] - 2026-04-03 ### Fixed diff --git a/package.json b/package.json index 9d4daf3..ede924e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oikos", - "version": "0.5.4", + "version": "0.5.5", "description": "Selbstgehosteter Familienplaner — Kalender, Aufgaben, Einkauf, Essensplan, Budget und mehr. Privat, offen, ohne Abo.", "main": "server/index.js", "engines": { diff --git a/server/services/apple-calendar.js b/server/services/apple-calendar.js index bce76bc..0255104 100644 --- a/server/services/apple-calendar.js +++ b/server/services/apple-calendar.js @@ -36,6 +36,10 @@ function cfgSet(key, value) { `).run(key, value); } +function cfgDel(key) { + db.get().prepare('DELETE FROM sync_config WHERE key = ?').run(key); +} + // -------------------------------------------------------- // Credentials: sync_config hat Vorrang vor .env // -------------------------------------------------------- @@ -253,52 +257,76 @@ async function sync() { return; } - // Standard-Kalender: erster nicht-Geburtstags-Kalender - const cal = calendars.find((c) => !c.displayName?.toLowerCase().includes('geburts')) || calendars[0]; + // created_by: ersten existierenden User verwenden (nicht hardcoded ID 1) + const owner = db.get().prepare('SELECT id FROM users ORDER BY id ASC LIMIT 1').get(); + if (!owner) { + console.warn('[Apple] Kein User in der Datenbank — Sync übersprungen.'); + return; + } + const createdBy = owner.id; - const calObjects = await client.fetchCalendarObjects({ calendar: cal }); + // Alle Kalender synchen (außer Geburtstags-Kalender) + const syncCalendars = calendars.filter( + (c) => !c.displayName?.toLowerCase().includes('geburts') && + !c.displayName?.toLowerCase().includes('birthday') + ); - // -------------------------------------------------------- - // Inbound: iCloud → lokal - // -------------------------------------------------------- - for (const obj of calObjects) { - const parsed = parseICS(obj.data || ''); - for (const ev of parsed) { - try { - const existing = db.get().prepare( - `SELECT id FROM calendar_events WHERE external_calendar_id = ? AND external_source = 'apple'` - ).get(ev.uid); + let totalObjects = 0; - if (existing) { - db.get().prepare(` - UPDATE calendar_events - SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, - all_day = ?, location = ?, recurrence_rule = ? - WHERE id = ? - `).run( - ev.summary, ev.description, ev.dtstart, ev.dtend, - ev.allDay ? 1 : 0, ev.location, ev.rrule, existing.id - ); - } else { - db.get().prepare(` - INSERT INTO calendar_events - (title, description, start_datetime, end_datetime, all_day, - location, color, external_calendar_id, external_source, recurrence_rule, created_by) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'apple', ?, 1) - `).run( - ev.summary, ev.description, ev.dtstart, ev.dtend, - ev.allDay ? 1 : 0, ev.location, APPLE_COLOR, ev.uid, ev.rrule - ); + for (const cal of syncCalendars) { + let calObjects; + try { + calObjects = await client.fetchCalendarObjects({ calendar: cal }); + } catch (err) { + console.warn(`[Apple] Kalender "${cal.displayName || '(unbenannt)'}" nicht abrufbar: ${err.message}`); + continue; + } + + totalObjects += calObjects.length; + + // -------------------------------------------------------- + // Inbound: iCloud → lokal + // -------------------------------------------------------- + for (const obj of calObjects) { + const parsed = parseICS(obj.data || ''); + for (const ev of parsed) { + try { + const existing = db.get().prepare( + `SELECT id FROM calendar_events WHERE external_calendar_id = ? AND external_source = 'apple'` + ).get(ev.uid); + + if (existing) { + db.get().prepare(` + UPDATE calendar_events + SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, + all_day = ?, location = ?, recurrence_rule = ? + WHERE id = ? + `).run( + ev.summary, ev.description, ev.dtstart, ev.dtend, + ev.allDay ? 1 : 0, ev.location, ev.rrule, existing.id + ); + } else { + db.get().prepare(` + INSERT INTO calendar_events + (title, description, start_datetime, end_datetime, all_day, + location, color, external_calendar_id, external_source, recurrence_rule, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'apple', ?, ?) + `).run( + ev.summary, ev.description, ev.dtstart, ev.dtend, + ev.allDay ? 1 : 0, ev.location, APPLE_COLOR, ev.uid, ev.rrule, createdBy + ); + } + } catch (err) { + console.error(`[Apple] Upsert-Fehler für UID ${ev.uid}:`, err.message); } - } catch (err) { - console.error(`[Apple] Upsert-Fehler für UID ${ev.uid}:`, err.message); } } } // -------------------------------------------------------- - // Outbound: lokal → iCloud + // Outbound: lokal → iCloud (erster verfügbarer Kalender) // -------------------------------------------------------- + const defaultCal = syncCalendars[0]; const localEvents = db.get().prepare(` SELECT * FROM calendar_events WHERE external_source = 'local' AND external_calendar_id IS NULL @@ -311,7 +339,7 @@ async function sync() { const filename = `${uid}.ics`; await client.createCalendarObject({ - calendar: cal, + calendar: defaultCal, filename, iCalString: icsData, }); @@ -325,7 +353,7 @@ async function sync() { } cfgSet('apple_last_sync', new Date().toISOString()); - console.log(`[Apple] Sync abgeschlossen — ${calObjects.length} Objekte inbound, ${localEvents.length} lokal → iCloud.`); + console.log(`[Apple] Sync abgeschlossen — ${totalObjects} Objekte aus ${syncCalendars.length} Kalendern inbound, ${localEvents.length} lokal → iCloud.`); } module.exports = { sync, getStatus, saveCredentials, clearCredentials, testConnection };