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.
This commit is contained in:
@@ -125,10 +125,13 @@ router.get('/', (req, res) => {
|
|||||||
SELECT e.*,
|
SELECT e.*,
|
||||||
u_assigned.display_name AS assigned_name,
|
u_assigned.display_name AS assigned_name,
|
||||||
u_assigned.avatar_color AS assigned_color,
|
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
|
FROM calendar_events e
|
||||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
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 users u_created ON u_created.id = e.created_by
|
||||||
|
LEFT JOIN external_calendars ec ON ec.id = e.calendar_ref_id
|
||||||
WHERE (
|
WHERE (
|
||||||
(e.recurrence_rule IS NULL AND
|
(e.recurrence_rule IS NULL AND
|
||||||
DATE(e.start_datetime) <= ? AND
|
DATE(e.start_datetime) <= ? AND
|
||||||
@@ -182,9 +185,12 @@ router.get('/upcoming', (req, res) => {
|
|||||||
const rawEvents = db.get().prepare(`
|
const rawEvents = db.get().prepare(`
|
||||||
SELECT e.*,
|
SELECT e.*,
|
||||||
u_assigned.display_name AS assigned_name,
|
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
|
FROM calendar_events e
|
||||||
LEFT JOIN users u_assigned ON u_assigned.id = e.assigned_to
|
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 (
|
WHERE (
|
||||||
(e.recurrence_rule IS NULL AND DATE(e.start_datetime) BETWEEN ? AND ?)
|
(e.recurrence_rule IS NULL AND DATE(e.start_datetime) BETWEEN ? AND ?)
|
||||||
OR
|
OR
|
||||||
|
|||||||
@@ -20,6 +20,29 @@ import { unfoldLines, parseICS, formatICSDate, tzLocalToUTC, applyDuration } fro
|
|||||||
|
|
||||||
const APPLE_COLOR = '#FC3C44';
|
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
|
// sync_config Helfer
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -152,6 +175,15 @@ function escapeICS(str) {
|
|||||||
return String(str).replace(/\\/g, '\\\\').replace(/;/g, '\\;').replace(/,/g, '\\,').replace(/\n/g, '\\n');
|
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
|
// Sync
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -207,6 +239,11 @@ async function sync() {
|
|||||||
|
|
||||||
totalObjects += calObjects.length;
|
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
|
// Inbound: iCloud → lokal
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -222,21 +259,21 @@ async function sync() {
|
|||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
UPDATE calendar_events
|
UPDATE calendar_events
|
||||||
SET title = ?, description = ?, start_datetime = ?, end_datetime = ?,
|
SET title = ?, description = ?, start_datetime = ?, end_datetime = ?,
|
||||||
all_day = ?, location = ?, recurrence_rule = ?
|
all_day = ?, location = ?, recurrence_rule = ?, color = ?, calendar_ref_id = ?
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
`).run(
|
`).run(
|
||||||
ev.summary, ev.description, ev.dtstart, ev.dtend,
|
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 {
|
} else {
|
||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
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, calendar_ref_id, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'apple', ?, ?)
|
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, createdBy
|
ev.allDay ? 1 : 0, ev.location, calColor, ev.uid, ev.rrule, calRefId, createdBy
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
|
|||||||
@@ -20,6 +20,18 @@ import * as db from '../db.js';
|
|||||||
|
|
||||||
const GOOGLE_COLOR = '#4285F4';
|
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)
|
// OAuth2-Client (lazy initialisiert)
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -160,6 +172,18 @@ async function sync() {
|
|||||||
const client = loadAuthorizedClient();
|
const client = loadAuthorizedClient();
|
||||||
const calendar = google.calendar({ version: 'v3', auth: client });
|
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
|
// Inbound: Google → lokal
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
@@ -199,7 +223,7 @@ async function sync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const items = response.data.items || [];
|
const items = response.data.items || [];
|
||||||
upsertGoogleEvents(items);
|
upsertGoogleEvents(items, calRefId, calColor);
|
||||||
|
|
||||||
pageToken = response.data.nextPageToken;
|
pageToken = response.data.nextPageToken;
|
||||||
newSyncToken = response.data.nextSyncToken || newSyncToken;
|
newSyncToken = response.data.nextSyncToken || newSyncToken;
|
||||||
@@ -238,29 +262,11 @@ async function sync() {
|
|||||||
// Helfer: Google-Event in lokale DB upserten
|
// Helfer: Google-Event in lokale DB upserten
|
||||||
// --------------------------------------------------------
|
// --------------------------------------------------------
|
||||||
|
|
||||||
function upsertGoogleEvents(items) {
|
function upsertGoogleEvents(items, calRefId = null, calColor = GOOGLE_COLOR) {
|
||||||
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
|
|
||||||
`);
|
|
||||||
|
|
||||||
const del = db.get().prepare(`
|
const del = db.get().prepare(`
|
||||||
DELETE FROM calendar_events WHERE external_calendar_id = ? AND external_source = 'google'
|
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) => {
|
const insertOrUpdate = db.transaction((item) => {
|
||||||
if (item.status === 'cancelled') {
|
if (item.status === 'cancelled') {
|
||||||
del.run(item.id);
|
del.run(item.id);
|
||||||
@@ -283,16 +289,16 @@ function upsertGoogleEvents(items) {
|
|||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
UPDATE calendar_events
|
UPDATE calendar_events
|
||||||
SET title = ?, description = ?, start_datetime = ?, end_datetime = ?,
|
SET title = ?, description = ?, start_datetime = ?, end_datetime = ?,
|
||||||
all_day = ?, location = ?, recurrence_rule = ?
|
all_day = ?, location = ?, recurrence_rule = ?, color = ?, calendar_ref_id = ?
|
||||||
WHERE 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 {
|
} else {
|
||||||
db.get().prepare(`
|
db.get().prepare(`
|
||||||
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, calendar_ref_id, created_by)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'google', ?, 1)
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, 'google', ?, ?, 1)
|
||||||
`).run(title, description, startDt, endDt, allDay ? 1 : 0, location, GOOGLE_COLOR, item.id, rrule);
|
`).run(title, description, startDt, endDt, allDay ? 1 : 0, location, calColor, item.id, rrule, calRefId);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user