Agenda refactor
This commit is contained in:
parent
e50af5450d
commit
1d2a72ce8c
@ -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) {
|
||||
|
||||
@ -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: `<?xml version="1.0" encoding="utf-8" ?>
|
||||
// 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: `<?xml version="1.0" encoding="utf-8" ?>
|
||||
<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
|
||||
<d:prop>
|
||||
<d:displayname />
|
||||
<c:calendar-color />
|
||||
<d:resourcetype />
|
||||
</d:prop>
|
||||
</d:propfind>`,
|
||||
});
|
||||
});
|
||||
|
||||
// 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 <c:calendar> in resourcetype)
|
||||
const isCalendar = props.data && props.data.includes('<c:calendar');
|
||||
|
||||
if (!isCalendar) {
|
||||
logger.debug('Skipping non-calendar directory', {
|
||||
filename: item.filename,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
// 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),
|
||||
});
|
||||
// Don't add calendars that fail property fetch - they might not be calendars
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,6 +163,11 @@ export async function fetchCalDAVEvents(
|
||||
endDate?: Date
|
||||
): Promise<CalDAVEvent[]> {
|
||||
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<string, typeof existingEvents[0]>();
|
||||
type EventType = typeof existingEvents[number];
|
||||
const existingEventsByExternalId = new Map<string, EventType>();
|
||||
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++;
|
||||
}
|
||||
|
||||
@ -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', {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user