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 <noreply@anthropic.com>
This commit is contained in:
@@ -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
|
||||
|
||||
+1
-1
@@ -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": {
|
||||
|
||||
@@ -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 };
|
||||
|
||||
Reference in New Issue
Block a user