diff --git a/app/agenda/page.tsx b/app/agenda/page.tsx index fcb3faf..c7d7cd7 100644 --- a/app/agenda/page.tsx +++ b/app/agenda/page.tsx @@ -595,6 +595,7 @@ export default async function CalendarPage() { } // Auto-sync Infomaniak calendars if needed (background, don't block page load) + // Reload sync configs after URL corrections to get updated URLs const infomaniakSyncConfigs = await prisma.calendarSync.findMany({ where: { provider: 'infomaniak', @@ -609,6 +610,12 @@ export default async function CalendarPage() { // Trigger sync for Infomaniak calendars that need it (async, don't wait) for (const syncConfig of infomaniakSyncConfigs) { + // Skip sync if URL is still invalid (should have been fixed above, but double-check) + if (syncConfig.externalCalendarUrl === '/principals' || !syncConfig.externalCalendarUrl || syncConfig.externalCalendarUrl === '/') { + console.log(`[AGENDA] Skipping Infomaniak sync ${syncConfig.id} - invalid calendar URL: ${syncConfig.externalCalendarUrl}`); + continue; + } + const minutesSinceLastSync = syncConfig.lastSyncAt ? (Date.now() - syncConfig.lastSyncAt.getTime()) / (1000 * 60) : Infinity; @@ -617,7 +624,7 @@ export default async function CalendarPage() { const needsSync = !syncConfig.lastSyncAt || minutesSinceLastSync >= syncConfig.syncFrequency; - console.log(`[AGENDA] Infomaniak sync config ${syncConfig.id}: lastSyncAt=${syncConfig.lastSyncAt}, minutesSinceLastSync=${minutesSinceLastSync.toFixed(1)}, needsSync=${needsSync}, syncFrequency=${syncConfig.syncFrequency}`); + console.log(`[AGENDA] Infomaniak sync config ${syncConfig.id}: lastSyncAt=${syncConfig.lastSyncAt}, minutesSinceLastSync=${minutesSinceLastSync.toFixed(1)}, needsSync=${needsSync}, syncFrequency=${syncConfig.syncFrequency}, calendarUrl=${syncConfig.externalCalendarUrl}`); if (needsSync) { console.log(`[AGENDA] Triggering background sync for Infomaniak calendar ${syncConfig.id}`); diff --git a/lib/services/caldav-sync.ts b/lib/services/caldav-sync.ts index 98a8aca..d53452d 100644 --- a/lib/services/caldav-sync.ts +++ b/lib/services/caldav-sync.ts @@ -453,33 +453,74 @@ export async function syncInfomaniakCalendar( // For updates, we cannot modify calendarId and userId (they are relations) // For creates, we need them - const baseEventData = { + // Build event data dynamically to handle case where externalEventId field doesn't exist yet + const baseEventData: any = { title: caldavEvent.summary, description: caldavEvent.description || null, start: caldavEvent.start, end: caldavEvent.end, location: caldavEvent.location || null, isAllDay: caldavEvent.allDay, - externalEventId: caldavEvent.uid, // Store UID for reliable matching }; + // Only add externalEventId if migration has been applied + // We'll try to add it, and if it fails, we'll retry without it + if (caldavEvent.uid) { + baseEventData.externalEventId = caldavEvent.uid; + } + if (existingEvent) { // Update existing event (without calendarId and userId - they are relations) - await prisma.event.update({ - where: { id: existingEvent.id }, - data: baseEventData, - }); - updated++; + try { + await prisma.event.update({ + where: { id: existingEvent.id }, + data: baseEventData, + }); + updated++; + } catch (updateError: any) { + // If externalEventId field doesn't exist, retry without it + if (updateError?.message?.includes('externalEventId') || updateError?.code === 'P2009') { + logger.warn('externalEventId field not available, updating without it', { + eventId: existingEvent.id, + }); + const { externalEventId, ...dataWithoutExternalId } = baseEventData; + await prisma.event.update({ + where: { id: existingEvent.id }, + data: dataWithoutExternalId, + }); + updated++; + } else { + throw updateError; + } + } } else { // Create new event (with calendarId and userId) - await prisma.event.create({ - data: { - ...baseEventData, - calendarId: syncConfig.calendarId, - userId: syncConfig.calendar.userId, - }, - }); - created++; + try { + await prisma.event.create({ + data: { + ...baseEventData, + calendarId: syncConfig.calendarId, + userId: syncConfig.calendar.userId, + }, + }); + created++; + } catch (createError: any) { + // If externalEventId field doesn't exist, retry without it + if (createError?.message?.includes('externalEventId') || createError?.code === 'P2009') { + logger.warn('externalEventId field not available, creating without it'); + const { externalEventId, ...dataWithoutExternalId } = baseEventData; + await prisma.event.create({ + data: { + ...dataWithoutExternalId, + calendarId: syncConfig.calendarId, + userId: syncConfig.calendar.userId, + }, + }); + created++; + } else { + throw createError; + } + } } } diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts index 70c04fb..ed01f94 100644 --- a/lib/services/microsoft-calendar-sync.ts +++ b/lib/services/microsoft-calendar-sync.ts @@ -499,44 +499,85 @@ export async function syncMicrosoftCalendar( // For updates, we cannot modify calendarId and userId (they are relations) // For creates, we need them - const baseEventData = { + // Build event data dynamically to handle case where externalEventId field doesn't exist yet + const baseEventData: any = { title: caldavEvent.summary, description: cleanedDescription, // Clean description without [MS_ID:xxx] prefix start: caldavEvent.start, end: caldavEvent.end, location: caldavEvent.location || null, isAllDay: caldavEvent.allDay, - externalEventId: microsoftId, // Store Microsoft ID for reliable matching }; + // Only add externalEventId if migration has been applied + // We'll try to add it, and if it fails, we'll retry without it + if (microsoftId) { + baseEventData.externalEventId = microsoftId; + } + if (existingEvent) { // Update existing event (without calendarId and userId - they are relations) - await prisma.event.update({ - where: { id: existingEvent.id }, - data: baseEventData, - }); - updated++; - logger.debug('Updated event', { - eventId: existingEvent.id, - title: caldavEvent.summary, - microsoftId, - }); + try { + await prisma.event.update({ + where: { id: existingEvent.id }, + data: baseEventData, + }); + updated++; + logger.debug('Updated event', { + eventId: existingEvent.id, + title: caldavEvent.summary, + microsoftId, + }); + } catch (updateError: any) { + // If externalEventId field doesn't exist, retry without it + if (updateError?.message?.includes('externalEventId') || updateError?.code === 'P2009') { + logger.warn('externalEventId field not available, updating without it', { + eventId: existingEvent.id, + }); + const { externalEventId, ...dataWithoutExternalId } = baseEventData; + await prisma.event.update({ + where: { id: existingEvent.id }, + data: dataWithoutExternalId, + }); + updated++; + } else { + throw updateError; + } + } } else { // Create new event (with calendarId and userId) - const newEvent = await prisma.event.create({ - data: { - ...baseEventData, - calendarId: syncConfig.calendarId, - userId: syncConfig.calendar.userId, - }, - }); - created++; - logger.debug('Created new event', { - eventId: newEvent.id, - title: caldavEvent.summary, - microsoftId, - start: caldavEvent.start.toISOString(), - }); + try { + const newEvent = await prisma.event.create({ + data: { + ...baseEventData, + calendarId: syncConfig.calendarId, + userId: syncConfig.calendar.userId, + }, + }); + created++; + logger.debug('Created new event', { + eventId: newEvent.id, + title: caldavEvent.summary, + microsoftId, + start: caldavEvent.start.toISOString(), + }); + } catch (createError: any) { + // If externalEventId field doesn't exist, retry without it + if (createError?.message?.includes('externalEventId') || createError?.code === 'P2009') { + logger.warn('externalEventId field not available, creating without it'); + const { externalEventId, ...dataWithoutExternalId } = baseEventData; + const newEvent = await prisma.event.create({ + data: { + ...dataWithoutExternalId, + calendarId: syncConfig.calendarId, + userId: syncConfig.calendar.userId, + }, + }); + created++; + } else { + throw createError; + } + } } } diff --git a/scripts/apply-external-event-id-migration.sql b/scripts/apply-external-event-id-migration.sql new file mode 100644 index 0000000..1e08f23 --- /dev/null +++ b/scripts/apply-external-event-id-migration.sql @@ -0,0 +1,49 @@ +-- Script to manually apply externalEventId migration +-- Run this directly on your PostgreSQL database + +-- Check if columns exist +SELECT + column_name, + data_type +FROM information_schema.columns +WHERE table_name = 'Event' +AND column_name IN ('externalEventId', 'externalEventUrl'); + +-- Add externalEventId column if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'Event' AND column_name = 'externalEventId' + ) THEN + ALTER TABLE "Event" ADD COLUMN "externalEventId" TEXT; + RAISE NOTICE 'Added externalEventId column'; + ELSE + RAISE NOTICE 'externalEventId column already exists'; + END IF; +END $$; + +-- Add externalEventUrl column if it doesn't exist +DO $$ +BEGIN + IF NOT EXISTS ( + SELECT 1 FROM information_schema.columns + WHERE table_name = 'Event' AND column_name = 'externalEventUrl' + ) THEN + ALTER TABLE "Event" ADD COLUMN "externalEventUrl" TEXT; + RAISE NOTICE 'Added externalEventUrl column'; + ELSE + RAISE NOTICE 'externalEventUrl column already exists'; + END IF; +END $$; + +-- Create index on externalEventId if it doesn't exist +CREATE INDEX IF NOT EXISTS "Event_externalEventId_idx" ON "Event"("externalEventId"); + +-- Verify columns were added +SELECT + column_name, + data_type +FROM information_schema.columns +WHERE table_name = 'Event' +AND column_name IN ('externalEventId', 'externalEventUrl');