Agenda refactor

This commit is contained in:
alma 2026-01-15 13:26:37 +01:00
parent 631524b8a8
commit 2ef0717e9c
2 changed files with 149 additions and 2 deletions

View File

@ -507,7 +507,8 @@ export async function syncMicrosoftCalendar(
// Priority 3: Fallback to title + date matching for events without externalEventId
// IMPORTANT: Only match if the event doesn't have an externalEventId (to avoid false matches)
if (!existingEvent) {
// This helps migrate old events that were created before externalEventId was added
if (!existingEvent && microsoftId) {
existingEvent = existingEvents.find(
(e) => {
// Access externalEventId safely (may not be in Prisma type if client not regenerated)
@ -521,10 +522,11 @@ export async function syncMicrosoftCalendar(
if (e.title === caldavEvent.summary) {
const timeDiff = Math.abs(new Date(e.start).getTime() - caldavEvent.start.getTime());
if (timeDiff < 60000) { // Within 1 minute
logger.debug('Matched event by title + date (no externalEventId)', {
logger.debug('Matched event by title + date (no externalEventId) - will update with externalEventId', {
eventId: e.id,
title: caldavEvent.summary,
timeDiff,
microsoftId,
});
return true;
}

View File

@ -0,0 +1,145 @@
#!/usr/bin/env ts-node
/**
* Script to clean up duplicate events in the database
* Keeps events with externalEventId, removes duplicates without externalEventId
*/
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
interface DuplicateGroup {
title: string;
start: Date;
end: Date;
calendarId: string;
events: Array<{
id: string;
externalEventId: string | null;
}>;
}
async function cleanupDuplicateEvents() {
try {
console.log('🔍 Searching for duplicate events...\n');
// Find all events grouped by title, start, end, and calendarId
const allEvents = await prisma.event.findMany({
select: {
id: true,
title: true,
start: true,
end: true,
calendarId: true,
externalEventId: true,
},
orderBy: [
{ calendarId: 'asc' },
{ title: 'asc' },
{ start: 'asc' },
],
});
// Group events by title + start + end + calendarId
const eventGroups = new Map<string, DuplicateGroup>();
for (const event of allEvents) {
const key = `${event.calendarId}|${event.title}|${event.start.toISOString()}|${event.end.toISOString()}`;
if (!eventGroups.has(key)) {
eventGroups.set(key, {
title: event.title,
start: event.start,
end: event.end,
calendarId: event.calendarId,
events: [],
});
}
eventGroups.get(key)!.events.push({
id: event.id,
externalEventId: event.externalEventId,
});
}
// Find duplicates (groups with more than 1 event)
const duplicates: DuplicateGroup[] = [];
for (const group of eventGroups.values()) {
if (group.events.length > 1) {
duplicates.push(group);
}
}
console.log(`Found ${duplicates.length} groups of duplicate events\n`);
if (duplicates.length === 0) {
console.log('✅ No duplicates found!');
return;
}
let totalDeleted = 0;
let totalKept = 0;
for (const group of duplicates) {
console.log(`\n📋 Processing: "${group.title}" (${group.events.length} duplicates)`);
console.log(` Calendar: ${group.calendarId}`);
console.log(` Date: ${group.start.toISOString()}`);
// Separate events with and without externalEventId
const withExternalId = group.events.filter(e => e.externalEventId);
const withoutExternalId = group.events.filter(e => !e.externalEventId);
if (withExternalId.length > 1) {
// Multiple events with externalEventId - keep the first one, delete others
console.log(` ⚠️ Warning: Multiple events with externalEventId found!`);
const toKeep = withExternalId[0];
const toDelete = [...withExternalId.slice(1), ...withoutExternalId];
console.log(` ✅ Keeping: ${toKeep.id} (has externalEventId)`);
for (const event of toDelete) {
console.log(` 🗑️ Deleting: ${event.id}`);
await prisma.event.delete({ where: { id: event.id } });
totalDeleted++;
}
totalKept++;
} else if (withExternalId.length === 1) {
// One event with externalEventId - keep it, delete others
const toKeep = withExternalId[0];
const toDelete = withoutExternalId;
console.log(` ✅ Keeping: ${toKeep.id} (has externalEventId)`);
for (const event of toDelete) {
console.log(` 🗑️ Deleting: ${event.id}`);
await prisma.event.delete({ where: { id: event.id } });
totalDeleted++;
}
totalKept++;
} else {
// No events with externalEventId - keep the first one, delete others
const toKeep = group.events[0];
const toDelete = group.events.slice(1);
console.log(` ✅ Keeping: ${toKeep.id} (no externalEventId, keeping first)`);
for (const event of toDelete) {
console.log(` 🗑️ Deleting: ${event.id}`);
await prisma.event.delete({ where: { id: event.id } });
totalDeleted++;
}
totalKept++;
}
}
console.log(`\n✅ Cleanup completed!`);
console.log(` Kept: ${totalKept} events`);
console.log(` Deleted: ${totalDeleted} duplicate events`);
} catch (error) {
console.error('❌ Error cleaning up duplicates:', error);
throw error;
} finally {
await prisma.$disconnect();
}
}
cleanupDuplicateEvents();