From db1206f91c59a80dadd59102b5b79082778e158d Mon Sep 17 00:00:00 2001 From: root Date: Thu, 23 Oct 2025 18:46:49 +0000 Subject: [PATCH] Fix recurring event duplication - handle recurringEventId properly in all phases --- bitbylaw/delete_all_calendars.py | 39 +++++++++------------- bitbylaw/steps/advoware_cal_sync/README.md | 36 ++++++++++++++++++++ 2 files changed, 51 insertions(+), 24 deletions(-) diff --git a/bitbylaw/delete_all_calendars.py b/bitbylaw/delete_all_calendars.py index afd4edcf..5a97c44c 100644 --- a/bitbylaw/delete_all_calendars.py +++ b/bitbylaw/delete_all_calendars.py @@ -1,6 +1,6 @@ #!/usr/bin/env python3 """ -Script to clear all events from all calendars in Google Calendar account +Script to delete all calendars from Google Calendar account """ import asyncio import sys @@ -9,8 +9,8 @@ sys.path.append('.') from steps.advoware_cal_sync.calendar_sync_event_step import get_google_service -async def clear_all_calendars(): - """Clear all events from all calendars""" +async def delete_all_calendars(): + """Delete all calendars from the Google account""" try: service = await get_google_service() @@ -22,7 +22,7 @@ async def clear_all_calendars(): print(f"Raw calendars result: {calendars_result}") print(f"Calendars list: {calendars}") - print(f"Found {len(calendars)} calendars to clear:") + print(f"Found {len(calendars)} calendars to delete:") for calendar in calendars: calendar_id = calendar['id'] @@ -31,31 +31,22 @@ async def clear_all_calendars(): print(f" - {summary} (ID: {calendar_id}, Primary: {primary})") + # Skip primary calendar if you want to keep it + if primary: + print(f" Skipping primary calendar: {summary}") + continue + try: - # Get all events in this calendar - events_result = service.events().list(calendarId=calendar_id, singleEvents=True).execute() - events = events_result.get('items', []) - - print(f" Found {len(events)} events to delete") - - # Delete each event - deleted_count = 0 - for event in events: - try: - service.events().delete(calendarId=calendar_id, eventId=event['id']).execute() - deleted_count += 1 - except Exception as e: - print(f" Failed to delete event {event['id']}: {e}") - - print(f" ✓ Deleted {deleted_count} events from: {summary}") - + # Delete the calendar + service.calendars().delete(calendarId=calendar_id).execute() + print(f" ✓ Deleted calendar: {summary}") except Exception as e: - print(f" ✗ Failed to clear {summary}: {e}") + print(f" ✗ Failed to delete calendar {summary}: {e}") - print("\nAll calendars have been cleared of events.") + print("\nAll non-primary calendars have been deleted.") except Exception as e: print(f"Error: {e}") if __name__ == "__main__": - asyncio.run(clear_all_calendars()) \ No newline at end of file + asyncio.run(delete_all_calendars()) \ No newline at end of file diff --git a/bitbylaw/steps/advoware_cal_sync/README.md b/bitbylaw/steps/advoware_cal_sync/README.md index 67d14b25..4e343aaf 100644 --- a/bitbylaw/steps/advoware_cal_sync/README.md +++ b/bitbylaw/steps/advoware_cal_sync/README.md @@ -255,6 +255,42 @@ Cron-Step für regelmäßige Ausführung. - Recurring-Events: Begrenzte Unterstützung; Advoware hat keine RRULE. - Timestamps: Fehlende in Google können zu Fallback führen. - Performance: Bei vielen Terminen könnte Paginierung helfen. +- **Single Events Expansion**: `singleEvents=true` in `fetch_google_events()` expandiert wiederkehrende Events in einzelne Instanzen, was zu Duplizierungsproblemen führt, wenn nicht korrekt behandelt. + +## Kritischer Bugfix: Duplizierung wiederkehrender Termine + +### Problemstellung +Bei wiederkehrenden Terminen (`dauertermin=1`) wurden Termine bei jedem Sync dupliziert, weil `fetch_google_events()` mit `singleEvents=true` arbeitet: + +1. **Google Calendar erstellt Master-Event** mit RRULE und `event_id` (z.B. `"abc123"`) +2. **`fetch_google_events()` expandiert** das Event in einzelne Instanzen mit IDs wie `"abc123_20251024"`, `"abc123_20251031"`, etc. +3. **Jede Instanz wird als "neu" behandelt** und erstellt einen separaten Advoware-Termin +4. **Ergebnis:** 1 wiederkehrender Advoware-Termin → N duplizierte Advoware-Termine + +### Lösung +**RecurringEventId-basierte Erkennung** in allen Phasen: + +- **DB-Indizes:** Verwenden weiterhin die gespeicherten `event_id` (Master-ID) +- **Phase 2:** Prüfe sowohl `event_id` als auch `recurringEventId` gegen DB-Index +- **Phase 3:** Berücksichtige `recurringEventId` bei Existenzprüfungen +- **Phase 4:** Verarbeite nur Master-Events einmal, nicht jede Instanz + +**Code-Änderungen:** +```python +# Phase 2: Prüfe Master-Event +recurring_master_id = evt.get('recurringEventId') +is_already_synced = event_id in db_google_index or (recurring_master_id and recurring_master_id in db_google_index) + +# Phase 4: Verarbeite nur Master-Events einmal +master_event_id = google_data.get('recurringEventId') or event_id +if master_event_id in processed_master_events: + continue +``` + +### Auswirkung +- Wiederkehrende Termine werden nicht mehr dupliziert +- Bidirektionale Sync funktioniert korrekt für alle Event-Typen +- Performance-Verbesserung durch weniger redundante Verarbeitung ## Korrekter Umgang mit Advoware-Timestamps