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:
Ulas
2026-04-03 12:10:58 +02:00
parent cd963540cf
commit eafe2b964d
3 changed files with 75 additions and 39 deletions
+8
View File
@@ -7,6 +7,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased] ## [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 ## [0.5.4] - 2026-04-03
### Fixed ### Fixed
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"name": "oikos", "name": "oikos",
"version": "0.5.4", "version": "0.5.5",
"description": "Selbstgehosteter Familienplaner — Kalender, Aufgaben, Einkauf, Essensplan, Budget und mehr. Privat, offen, ohne Abo.", "description": "Selbstgehosteter Familienplaner — Kalender, Aufgaben, Einkauf, Essensplan, Budget und mehr. Privat, offen, ohne Abo.",
"main": "server/index.js", "main": "server/index.js",
"engines": { "engines": {
+36 -8
View File
@@ -36,6 +36,10 @@ function cfgSet(key, value) {
`).run(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 // Credentials: sync_config hat Vorrang vor .env
// -------------------------------------------------------- // --------------------------------------------------------
@@ -253,10 +257,32 @@ async function sync() {
return; return;
} }
// Standard-Kalender: erster nicht-Geburtstags-Kalender // created_by: ersten existierenden User verwenden (nicht hardcoded ID 1)
const cal = calendars.find((c) => !c.displayName?.toLowerCase().includes('geburts')) || calendars[0]; 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')
);
let totalObjects = 0;
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 // Inbound: iCloud → lokal
@@ -284,10 +310,10 @@ async function sync() {
INSERT INTO calendar_events INSERT INTO calendar_events
(title, description, start_datetime, end_datetime, all_day, (title, description, start_datetime, end_datetime, all_day,
location, color, external_calendar_id, external_source, recurrence_rule, created_by) location, color, external_calendar_id, external_source, recurrence_rule, created_by)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'apple', ?, 1) VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'apple', ?, ?)
`).run( `).run(
ev.summary, ev.description, ev.dtstart, ev.dtend, ev.summary, ev.description, ev.dtstart, ev.dtend,
ev.allDay ? 1 : 0, ev.location, APPLE_COLOR, ev.uid, ev.rrule ev.allDay ? 1 : 0, ev.location, APPLE_COLOR, ev.uid, ev.rrule, createdBy
); );
} }
} catch (err) { } catch (err) {
@@ -295,10 +321,12 @@ async function sync() {
} }
} }
} }
}
// -------------------------------------------------------- // --------------------------------------------------------
// Outbound: lokal → iCloud // Outbound: lokal → iCloud (erster verfügbarer Kalender)
// -------------------------------------------------------- // --------------------------------------------------------
const defaultCal = syncCalendars[0];
const localEvents = db.get().prepare(` const localEvents = db.get().prepare(`
SELECT * FROM calendar_events SELECT * FROM calendar_events
WHERE external_source = 'local' AND external_calendar_id IS NULL WHERE external_source = 'local' AND external_calendar_id IS NULL
@@ -311,7 +339,7 @@ async function sync() {
const filename = `${uid}.ics`; const filename = `${uid}.ics`;
await client.createCalendarObject({ await client.createCalendarObject({
calendar: cal, calendar: defaultCal,
filename, filename,
iCalString: icsData, iCalString: icsData,
}); });
@@ -325,7 +353,7 @@ async function sync() {
} }
cfgSet('apple_last_sync', new Date().toISOString()); 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 }; module.exports = { sync, getStatus, saveCredentials, clearCredentials, testConnection };