diff --git a/app/agenda/page.tsx b/app/agenda/page.tsx
index 43da248..fcb3faf 100644
--- a/app/agenda/page.tsx
+++ b/app/agenda/page.tsx
@@ -235,7 +235,44 @@ export default async function CalendarPage() {
// If sync exists but is disabled, check if it's due to invalid credentials
// Don't re-enable if the last error was 401 (invalid credentials)
if (existingSync) {
- console.log(`[AGENDA] Found existing sync for Infomaniak account ${account.email}: syncId=${existingSync.id}, calendarId=${existingSync.calendarId}, syncEnabled=${existingSync.syncEnabled}, hasCalendar=${!!existingSync.calendar}`);
+ console.log(`[AGENDA] Found existing sync for Infomaniak account ${account.email}: syncId=${existingSync.id}, calendarId=${existingSync.calendarId}, syncEnabled=${existingSync.syncEnabled}, hasCalendar=${!!existingSync.calendar}, externalCalendarUrl=${existingSync.externalCalendarUrl}`);
+
+ // Fix invalid calendar URLs (like /principals)
+ if (existingSync.externalCalendarUrl === '/principals' || !existingSync.externalCalendarUrl || existingSync.externalCalendarUrl === '/') {
+ console.log(`[AGENDA] Invalid calendar URL detected (${existingSync.externalCalendarUrl}), attempting to rediscover...`);
+ try {
+ const { discoverInfomaniakCalendars } = await import('@/lib/services/caldav-sync');
+ const externalCalendars = await discoverInfomaniakCalendars(
+ account.email,
+ account.password!
+ );
+
+ if (externalCalendars.length > 0) {
+ const mainCalendar = externalCalendars[0];
+ console.log(`[AGENDA] Updating sync with correct calendar URL: ${mainCalendar.url}`);
+ await prisma.calendarSync.update({
+ where: { id: existingSync.id },
+ data: {
+ externalCalendarId: mainCalendar.id,
+ externalCalendarUrl: mainCalendar.url,
+ lastSyncError: null, // Clear error since we're fixing the URL
+ }
+ });
+ // Reload the sync config
+ const updatedSync = await prisma.calendarSync.findUnique({
+ where: { id: existingSync.id },
+ include: { calendar: true }
+ });
+ if (updatedSync) {
+ existingSync = updatedSync;
+ }
+ } else {
+ console.log(`[AGENDA] No calendars found during rediscovery for ${account.email}`);
+ }
+ } catch (rediscoverError) {
+ console.error(`[AGENDA] Error rediscovering calendars:`, rediscoverError);
+ }
+ }
// Check if calendar still exists
if (!existingSync.calendar) {
diff --git a/lib/services/caldav-sync.ts b/lib/services/caldav-sync.ts
index bae5362..98a8aca 100644
--- a/lib/services/caldav-sync.ts
+++ b/lib/services/caldav-sync.ts
@@ -53,46 +53,55 @@ export async function discoverInfomaniakCalendars(
const calendars: CalDAVCalendar[] = [];
for (const item of items) {
- if (item.type === 'directory' && item.filename !== '/') {
- // Get calendar properties
- try {
- const props = await client.customRequest(item.filename, {
- method: 'PROPFIND',
- headers: {
- Depth: '0',
- 'Content-Type': 'application/xml',
- },
- data: `
+ // Skip non-directories, root, and special directories like /principals
+ if (item.type !== 'directory' || item.filename === '/' || item.filename === '/principals') {
+ continue;
+ }
+
+ // Get calendar properties to verify it's actually a calendar
+ try {
+ const props = await client.customRequest(item.filename, {
+ method: 'PROPFIND',
+ headers: {
+ Depth: '0',
+ 'Content-Type': 'application/xml',
+ },
+ data: `
+
`,
- });
+ });
- // Parse XML response to extract calendar name and color
- const displayName = extractDisplayName(props.data);
- const color = extractCalendarColor(props.data);
-
- calendars.push({
- id: item.filename.replace(/^\//, '').replace(/\/$/, ''),
- name: displayName || item.basename || 'Calendrier',
- url: item.filename,
- color: color,
- });
- } catch (error) {
- logger.error('Error fetching calendar properties', {
- calendar: item.filename,
- error: error instanceof Error ? error.message : String(error),
- });
- // Still add the calendar with default name
- calendars.push({
- id: item.filename.replace(/^\//, '').replace(/\/$/, ''),
- name: item.basename || 'Calendrier',
- url: item.filename,
+ // Check if this is actually a calendar (has in resourcetype)
+ const isCalendar = props.data && props.data.includes(' {
try {
+ // Validate calendar URL - must not be /principals or other non-calendar paths
+ if (!calendarUrl || calendarUrl === '/principals' || calendarUrl === '/') {
+ throw new Error(`Invalid calendar URL: ${calendarUrl}. This is not a calendar directory.`);
+ }
+
const client = await getInfomaniakCalDAVClient(email, password);
// Build calendar query URL
@@ -188,6 +202,11 @@ export async function fetchCalDAVEvents(
data: queryXml,
});
+ // Validate response data exists
+ if (!response.data || typeof response.data !== 'string') {
+ throw new Error(`Invalid response from CalDAV server: expected string data, got ${typeof response.data}`);
+ }
+
// Parse iCalendar data from response
const events = parseICalendarEvents(response.data);
@@ -399,7 +418,8 @@ export async function syncInfomaniakCalendar(
});
// Create a map of existing events by externalEventId (UID) for fast lookup
- const existingEventsByExternalId = new Map();
+ type EventType = typeof existingEvents[number];
+ const existingEventsByExternalId = new Map();
for (const event of existingEvents) {
if (event.externalEventId) {
existingEventsByExternalId.set(event.externalEventId, event);
@@ -413,14 +433,14 @@ export async function syncInfomaniakCalendar(
// Sync events: create or update
for (const caldavEvent of caldavEvents) {
// Priority 1: Match by externalEventId (UID) - most reliable
- let existingEvent = caldavEvent.uid
+ let existingEvent: EventType | undefined = caldavEvent.uid
? existingEventsByExternalId.get(caldavEvent.uid)
: undefined;
// Priority 2: Fallback to title + date matching for events without externalEventId (backward compatibility)
if (!existingEvent) {
existingEvent = existingEvents.find(
- (e) => {
+ (e: EventType) => {
if (!e.externalEventId && // Only match events that don't have externalEventId yet
e.title === caldavEvent.summary) {
const timeDiff = Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime());
@@ -431,29 +451,33 @@ export async function syncInfomaniakCalendar(
);
}
- const eventData = {
+ // For updates, we cannot modify calendarId and userId (they are relations)
+ // For creates, we need them
+ const baseEventData = {
title: caldavEvent.summary,
description: caldavEvent.description || null,
start: caldavEvent.start,
end: caldavEvent.end,
location: caldavEvent.location || null,
isAllDay: caldavEvent.allDay,
- calendarId: syncConfig.calendarId,
- userId: syncConfig.calendar.userId,
externalEventId: caldavEvent.uid, // Store UID for reliable matching
};
if (existingEvent) {
- // Update existing event
+ // Update existing event (without calendarId and userId - they are relations)
await prisma.event.update({
where: { id: existingEvent.id },
- data: eventData,
+ data: baseEventData,
});
updated++;
} else {
- // Create new event
+ // Create new event (with calendarId and userId)
await prisma.event.create({
- data: eventData,
+ data: {
+ ...baseEventData,
+ calendarId: syncConfig.calendarId,
+ userId: syncConfig.calendar.userId,
+ },
});
created++;
}
diff --git a/lib/services/microsoft-calendar-sync.ts b/lib/services/microsoft-calendar-sync.ts
index 736e3d5..70c04fb 100644
--- a/lib/services/microsoft-calendar-sync.ts
+++ b/lib/services/microsoft-calendar-sync.ts
@@ -497,23 +497,23 @@ export async function syncMicrosoftCalendar(
// Clean description (remove [MS_ID:xxx] prefix if present from previous syncs)
const cleanedDescription = cleanDescription(caldavEvent.description);
- const eventData = {
+ // For updates, we cannot modify calendarId and userId (they are relations)
+ // For creates, we need them
+ const baseEventData = {
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,
- calendarId: syncConfig.calendarId,
- userId: syncConfig.calendar.userId,
externalEventId: microsoftId, // Store Microsoft ID for reliable matching
};
if (existingEvent) {
- // Update existing event
+ // Update existing event (without calendarId and userId - they are relations)
await prisma.event.update({
where: { id: existingEvent.id },
- data: eventData,
+ data: baseEventData,
});
updated++;
logger.debug('Updated event', {
@@ -522,9 +522,13 @@ export async function syncMicrosoftCalendar(
microsoftId,
});
} else {
- // Create new event
+ // Create new event (with calendarId and userId)
const newEvent = await prisma.event.create({
- data: eventData,
+ data: {
+ ...baseEventData,
+ calendarId: syncConfig.calendarId,
+ userId: syncConfig.calendar.userId,
+ },
});
created++;
logger.debug('Created new event', {