From 9fb350eb29c34bdb1725b88bb390e1a020eb0c75 Mon Sep 17 00:00:00 2001 From: Scarriffle Date: Thu, 2 Jul 2026 18:22:11 +0200 Subject: [PATCH] fix: surface all calendar sync failures, not just Google CalDAV and Home Assistant sync failures were previously only logged server-side, leaving clients unable to distinguish an empty calendar from a broken sync. Unify error reporting across CalDAV, Home Assistant, and Google into a single errors list on GET /api/caldav/events, shaped as {source, name, message}. Messages are fixed generic strings, never raw exception text, to avoid leaking URLs or credential fragments. get_ha_events and get_google_events now return (events, errors) tuples so per-calendar failures propagate to the caller in addition to account-level failures. Frontend toast now picks its label from err.source instead of assuming Google/err.email. --- backend/routers/caldav_router.py | 28 ++++++++++++++++++++----- backend/routers/google_router.py | 17 ++++++++++++--- backend/routers/homeassistant_router.py | 16 ++++++++++++-- frontend/js/calendar.js | 4 +++- 4 files changed, 54 insertions(+), 11 deletions(-) diff --git a/backend/routers/caldav_router.py b/backend/routers/caldav_router.py index 4427706..bc8e147 100644 --- a/backend/routers/caldav_router.py +++ b/backend/routers/caldav_router.py @@ -300,6 +300,7 @@ def get_events( end_dt = end_dt.replace(tzinfo=timezone.utc) all_events = [] + sync_errors = [] accounts = ( db.query(models.CalDAVAccount) .filter( @@ -333,6 +334,11 @@ def get_events( logger.error( "Error fetching calendar %s: %s", calendar.id, exc ) + sync_errors.append({ + "source": "caldav", + "name": f"{account.username} – {calendar.name}", + "message": "Sync fehlgeschlagen", + }) # ── Local calendar events (own + shared + group calendars) ───────────── readable_ids = permissions.readable_local_calendar_ids(db, current_user) @@ -414,13 +420,18 @@ def get_events( .filter(models.GoogleAccount.user_id == current_user.id) .all() ) - google_errors = [] for g_acc in google_accounts: try: - all_events.extend(get_google_events(g_acc, start_dt, end_dt, db)) + g_events, g_errors = get_google_events(g_acc, start_dt, end_dt, db) + all_events.extend(g_events) + sync_errors.extend(g_errors) except Exception as exc: logger.error("Error fetching Google Calendar for %s: %s", g_acc.email, exc) - google_errors.append({"email": g_acc.email}) + sync_errors.append({ + "source": "google", + "name": g_acc.email, + "message": "Sync fehlgeschlagen", + }) # ── Home Assistant events ───────────────────────────── from routers.homeassistant_router import get_ha_events @@ -431,11 +442,18 @@ def get_events( ) for ha_acc in ha_accounts: try: - all_events.extend(get_ha_events(ha_acc, start_dt, end_dt, db)) + ha_events, ha_errors = get_ha_events(ha_acc, start_dt, end_dt, db) + all_events.extend(ha_events) + sync_errors.extend(ha_errors) except Exception as exc: logger.error("Error fetching HA events for %s: %s", ha_acc.name, exc) + sync_errors.append({ + "source": "homeassistant", + "name": ha_acc.name, + "message": "Sync fehlgeschlagen", + }) - return {"events": all_events, "errors": google_errors} + return {"events": all_events, "errors": sync_errors} @router.post("/events") diff --git a/backend/routers/google_router.py b/backend/routers/google_router.py index aa6a7fb..2d7d631 100644 --- a/backend/routers/google_router.py +++ b/backend/routers/google_router.py @@ -382,8 +382,13 @@ def update_calendar( # ── Events ─────────────────────────────────────────────── -def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: datetime, db: Session) -> list: - """Fetch events from all enabled Google calendars for an account.""" +def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: datetime, db: Session) -> tuple: + """Fetch events from all enabled Google calendars for an account. + + Returns (events, errors) — errors is a list of + {"source": "google", "name": ..., "message": ...} dicts for any + calendar that failed to sync. Never includes raw exception text. + """ try: token = _refresh_access_token(account, db) except Exception as exc: @@ -391,6 +396,7 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: raise all_events = [] + errors = [] for gcal in account.calendars: if not gcal.enabled or gcal.sidebar_hidden: continue @@ -409,8 +415,13 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: all_events.append(_parse_google_event(ev, gcal.id, gcal.name, gcal.color or "#4285f4")) except Exception as exc: logger.error("Error fetching events for calendar %s (%s): %s", gcal.name, gcal.cal_id, exc) + errors.append({ + "source": "google", + "name": f"{account.email} – {gcal.name}", + "message": "Sync fehlgeschlagen", + }) - return all_events + return all_events, errors class GoogleEventCreate(BaseModel): diff --git a/backend/routers/homeassistant_router.py b/backend/routers/homeassistant_router.py index 7b5bc97..3e3398f 100644 --- a/backend/routers/homeassistant_router.py +++ b/backend/routers/homeassistant_router.py @@ -313,8 +313,15 @@ def _parse_ha_event(ev: dict, cal_db_id: int, cal_name: str, cal_color: str) -> } -def get_ha_events(account: models.HomeAssistantAccount, start_dt: datetime, end_dt: datetime, db: Session) -> list: +def get_ha_events(account: models.HomeAssistantAccount, start_dt: datetime, end_dt: datetime, db: Session) -> tuple: + """Fetch events from all enabled HA calendars for an account. + + Returns (events, errors) — errors is a list of + {"source": "homeassistant", "name": ..., "message": ...} dicts for any + calendar that failed to sync. Never includes raw exception text. + """ all_events = [] + errors = [] try: token = _get_valid_token(account, db) except Exception as exc: @@ -330,7 +337,12 @@ def get_ha_events(account: models.HomeAssistantAccount, start_dt: datetime, end_ all_events.append(_parse_ha_event(ev, cal.id, cal.name, color)) except Exception as exc: logger.error("HA event fetch error %s (%s): %s", cal.entity_id, account.name, exc) - return all_events + errors.append({ + "source": "homeassistant", + "name": f"{account.name} – {cal.name}", + "message": "Sync fehlgeschlagen", + }) + return all_events, errors # ── Serialization ───────────────────────────────────────── diff --git a/frontend/js/calendar.js b/frontend/js/calendar.js index 3c71acc..22b57ae 100644 --- a/frontend/js/calendar.js +++ b/frontend/js/calendar.js @@ -377,8 +377,10 @@ async function fetchAndRender(force = false, silent = false) { const resp = await api.get(`/caldav/events?start=${fetchStart.toISOString()}&end=${fetchEnd.toISOString()}`); const events = resp.events || resp; if (resp.errors && resp.errors.length) { + const sourceLabels = { caldav: 'CalDAV', homeassistant: 'Home Assistant', google: 'Google' }; for (const err of resp.errors) { - showToast(`Google (${err.email}): Token abgelaufen – bitte Konto trennen und neu verbinden`, true); + const label = sourceLabels[err.source] || err.source || 'Kalender'; + showToast(`${label} (${err.name}): ${err.message}`, true); } } eventCache.start = fetchStart;