From 69c72f3abdb45626ff41bb6833b1bd1519ef256e Mon Sep 17 00:00:00 2001 From: "Konrad M." Date: Tue, 21 Apr 2026 21:51:00 +0200 Subject: [PATCH] feat(calendar): track external calendar name and color through Google/Apple sync Google and Apple sync services now fetch calendar metadata and persist it via upsertExternalCalendar(). The /calendar and /upcoming endpoints JOIN on external_calendars to return cal_name and cal_color with every event. --- server/routes/calendar.js | 10 ++++-- server/services/apple-calendar.js | 47 ++++++++++++++++++++++--- server/services/google-calendar.js | 56 +++++++++++++++++------------- 3 files changed, 81 insertions(+), 32 deletions(-) diff --git a/server/routes/calendar.js b/server/routes/calendar.js index 60cbf31..b499af2 100644 --- a/server/routes/calendar.js +++ b/server/routes/calendar.js @@ -125,10 +125,13 @@ router.get('/', (req, res) => { SELECT e.*, u_assigned.display_name AS assigned_name, u_assigned.avatar_color AS assigned_color, - u_created.display_name AS creator_name + u_created.display_name AS creator_name, + ec.name AS cal_name, + ec.color AS cal_color FROM calendar_events e LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to LEFT JOIN users u_created ON u_created.id = e.created_by + LEFT JOIN external_calendars ec ON ec.id = e.calendar_ref_id WHERE ( (e.recurrence_rule IS NULL AND DATE(e.start_datetime) <= ? AND @@ -182,9 +185,12 @@ router.get('/upcoming', (req, res) => { const rawEvents = db.get().prepare(` SELECT e.*, u_assigned.display_name AS assigned_name, - u_assigned.avatar_color AS assigned_color + u_assigned.avatar_color AS assigned_color, + ec.name AS cal_name, + ec.color AS cal_color FROM calendar_events e LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to + LEFT JOIN external_calendars ec ON ec.id = e.calendar_ref_id WHERE ( (e.recurrence_rule IS NULL AND DATE(e.start_datetime) BETWEEN ? AND ?) OR diff --git a/server/services/apple-calendar.js b/server/services/apple-calendar.js index a21c66a..b16d317 100644 --- a/server/services/apple-calendar.js +++ b/server/services/apple-calendar.js @@ -20,6 +20,29 @@ import { unfoldLines, parseICS, formatICSDate, tzLocalToUTC, applyDuration } fro const APPLE_COLOR = '#FC3C44'; +// -------------------------------------------------------- +// Externe Kalender-Metadaten upserten +// -------------------------------------------------------- + +function normalizeCalColor(c) { + if (!c) return null; + if (/^#[0-9a-fA-F]{8}$/.test(c)) return c.slice(0, 7); // strip alpha + if (/^#[0-9a-fA-F]{6}$/.test(c)) return c; + return null; +} + +function upsertExternalCalendar(source, externalId, name, color) { + const row = db.get().prepare(` + INSERT INTO external_calendars (source, external_id, name, color) + VALUES (?, ?, ?, ?) + ON CONFLICT(source, external_id) DO UPDATE SET + name = excluded.name, + color = excluded.color + RETURNING id + `).get(source, externalId, name, color); + return row.id; +} + // -------------------------------------------------------- // sync_config Helfer // -------------------------------------------------------- @@ -152,6 +175,15 @@ function escapeICS(str) { return String(str).replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n'); } +function unescapeICS(str) { + if (!str) return str; + return str + .replace(/\\[Nn]/g, '\n') + .replace(/\\,/g, ',') + .replace(/\\;/g, ';') + .replace(/\\\\/g, '\\'); +} + // -------------------------------------------------------- // Sync // -------------------------------------------------------- @@ -207,6 +239,11 @@ async function sync() { totalObjects += calObjects.length; + // Kalender-Metadaten in external_calendars upserten + const calColor = normalizeCalColor(cal.calendarColor) || APPLE_COLOR; + const calName = cal.displayName || 'Apple Calendar'; + const calRefId = upsertExternalCalendar('apple', cal.url, calName, calColor); + // -------------------------------------------------------- // Inbound: iCloud → lokal // -------------------------------------------------------- @@ -222,21 +259,21 @@ async function sync() { db.get().prepare(` UPDATE calendar_events SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, - all_day = ?, location = ?, recurrence_rule = ? + all_day = ?, location = ?, recurrence_rule = ?, color = ?, calendar_ref_id = ? WHERE id = ? `).run( ev.summary, ev.description, ev.dtstart, ev.dtend, - ev.allDay ? 1 : 0, ev.location, ev.rrule, existing.id + ev.allDay ? 1 : 0, ev.location, ev.rrule, calColor, calRefId, 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', ?, ?) + location, color, external_calendar_id, external_source, recurrence_rule, calendar_ref_id, 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 + ev.allDay ? 1 : 0, ev.location, calColor, ev.uid, ev.rrule, calRefId, createdBy ); } } catch (err) { diff --git a/server/services/google-calendar.js b/server/services/google-calendar.js index 2dc6fb3..1f9d2a7 100644 --- a/server/services/google-calendar.js +++ b/server/services/google-calendar.js @@ -20,6 +20,18 @@ import * as db from '../db.js'; const GOOGLE_COLOR = '#4285F4'; +function upsertExternalCalendar(source, externalId, name, color) { + const row = db.get().prepare(` + INSERT INTO external_calendars (source, external_id, name, color) + VALUES (?, ?, ?, ?) + ON CONFLICT(source, external_id) DO UPDATE SET + name = excluded.name, + color = excluded.color + RETURNING id + `).get(source, externalId, name, color); + return row.id; +} + // -------------------------------------------------------- // OAuth2-Client (lazy initialisiert) // -------------------------------------------------------- @@ -160,6 +172,18 @@ async function sync() { const client = loadAuthorizedClient(); const calendar = google.calendar({ version: 'v3', auth: client }); + // Kalender-Metadaten holen und in external_calendars upserten + let calRefId = null; + let calColor = GOOGLE_COLOR; + try { + const meta = await calendar.calendarList.get({ calendarId: 'primary' }); + calColor = meta.data.backgroundColor || GOOGLE_COLOR; + const calName = meta.data.summary || 'Google Calendar'; + calRefId = upsertExternalCalendar('google', 'primary', calName, calColor); + } catch (err) { + log.warn('Kalender-Metadaten nicht abrufbar:', err.message); + } + // -------------------------------------------------------- // Inbound: Google → lokal // -------------------------------------------------------- @@ -199,7 +223,7 @@ async function sync() { } const items = response.data.items || []; - upsertGoogleEvents(items); + upsertGoogleEvents(items, calRefId, calColor); pageToken = response.data.nextPageToken; newSyncToken = response.data.nextSyncToken || newSyncToken; @@ -238,29 +262,11 @@ async function sync() { // Helfer: Google-Event in lokale DB upserten // -------------------------------------------------------- -function upsertGoogleEvents(items) { - const upsert = 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 (?, ?, ?, ?, ?, ?, ?, ?, 'google', ?, 1) - ON CONFLICT(external_calendar_id) DO UPDATE SET - title = excluded.title, - description = excluded.description, - start_datetime = excluded.start_datetime, - end_datetime = excluded.end_datetime, - all_day = excluded.all_day, - location = excluded.location, - recurrence_rule = excluded.recurrence_rule - `); - +function upsertGoogleEvents(items, calRefId = null, calColor = GOOGLE_COLOR) { const del = db.get().prepare(` DELETE FROM calendar_events WHERE external_calendar_id = ? AND external_source = 'google' `); - // Erst external_calendar_id UNIQUE index anlegen falls noch nicht vorhanden - // (Migration 2 legt idx_calendar_external_id an, aber kein UNIQUE constraint) - // Wir nutzen stattdessen manuelles Upsert mit SELECT + INSERT/UPDATE const insertOrUpdate = db.transaction((item) => { if (item.status === 'cancelled') { del.run(item.id); @@ -283,16 +289,16 @@ function upsertGoogleEvents(items) { db.get().prepare(` UPDATE calendar_events SET title = ?, description = ?, start_datetime = ?, end_datetime = ?, - all_day = ?, location = ?, recurrence_rule = ? + all_day = ?, location = ?, recurrence_rule = ?, color = ?, calendar_ref_id = ? WHERE id = ? - `).run(title, description, startDt, endDt, allDay ? 1 : 0, location, rrule, existing.id); + `).run(title, description, startDt, endDt, allDay ? 1 : 0, location, rrule, calColor, calRefId, 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 (?, ?, ?, ?, ?, ?, ?, ?, 'google', ?, 1) - `).run(title, description, startDt, endDt, allDay ? 1 : 0, location, GOOGLE_COLOR, item.id, rrule); + location, color, external_calendar_id, external_source, recurrence_rule, calendar_ref_id, created_by) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'google', ?, ?, 1) + `).run(title, description, startDt, endDt, allDay ? 1 : 0, location, calColor, item.id, rrule, calRefId); } });