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.
This commit is contained in:
@@ -300,6 +300,7 @@ def get_events(
|
|||||||
end_dt = end_dt.replace(tzinfo=timezone.utc)
|
end_dt = end_dt.replace(tzinfo=timezone.utc)
|
||||||
|
|
||||||
all_events = []
|
all_events = []
|
||||||
|
sync_errors = []
|
||||||
accounts = (
|
accounts = (
|
||||||
db.query(models.CalDAVAccount)
|
db.query(models.CalDAVAccount)
|
||||||
.filter(
|
.filter(
|
||||||
@@ -333,6 +334,11 @@ def get_events(
|
|||||||
logger.error(
|
logger.error(
|
||||||
"Error fetching calendar %s: %s", calendar.id, exc
|
"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) ─────────────
|
# ── Local calendar events (own + shared + group calendars) ─────────────
|
||||||
readable_ids = permissions.readable_local_calendar_ids(db, current_user)
|
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)
|
.filter(models.GoogleAccount.user_id == current_user.id)
|
||||||
.all()
|
.all()
|
||||||
)
|
)
|
||||||
google_errors = []
|
|
||||||
for g_acc in google_accounts:
|
for g_acc in google_accounts:
|
||||||
try:
|
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:
|
except Exception as exc:
|
||||||
logger.error("Error fetching Google Calendar for %s: %s", g_acc.email, 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 ─────────────────────────────
|
# ── Home Assistant events ─────────────────────────────
|
||||||
from routers.homeassistant_router import get_ha_events
|
from routers.homeassistant_router import get_ha_events
|
||||||
@@ -431,11 +442,18 @@ def get_events(
|
|||||||
)
|
)
|
||||||
for ha_acc in ha_accounts:
|
for ha_acc in ha_accounts:
|
||||||
try:
|
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:
|
except Exception as exc:
|
||||||
logger.error("Error fetching HA events for %s: %s", ha_acc.name, 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")
|
@router.post("/events")
|
||||||
|
|||||||
@@ -382,8 +382,13 @@ def update_calendar(
|
|||||||
|
|
||||||
# ── Events ───────────────────────────────────────────────
|
# ── Events ───────────────────────────────────────────────
|
||||||
|
|
||||||
def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt: datetime, db: Session) -> list:
|
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."""
|
"""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:
|
try:
|
||||||
token = _refresh_access_token(account, db)
|
token = _refresh_access_token(account, db)
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
@@ -391,6 +396,7 @@ def get_google_events(account: models.GoogleAccount, start_dt: datetime, end_dt:
|
|||||||
raise
|
raise
|
||||||
|
|
||||||
all_events = []
|
all_events = []
|
||||||
|
errors = []
|
||||||
for gcal in account.calendars:
|
for gcal in account.calendars:
|
||||||
if not gcal.enabled or gcal.sidebar_hidden:
|
if not gcal.enabled or gcal.sidebar_hidden:
|
||||||
continue
|
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"))
|
all_events.append(_parse_google_event(ev, gcal.id, gcal.name, gcal.color or "#4285f4"))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("Error fetching events for calendar %s (%s): %s", gcal.name, gcal.cal_id, 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):
|
class GoogleEventCreate(BaseModel):
|
||||||
|
|||||||
@@ -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 = []
|
all_events = []
|
||||||
|
errors = []
|
||||||
try:
|
try:
|
||||||
token = _get_valid_token(account, db)
|
token = _get_valid_token(account, db)
|
||||||
except Exception as exc:
|
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))
|
all_events.append(_parse_ha_event(ev, cal.id, cal.name, color))
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
logger.error("HA event fetch error %s (%s): %s", cal.entity_id, account.name, 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 ─────────────────────────────────────────
|
# ── Serialization ─────────────────────────────────────────
|
||||||
|
|||||||
@@ -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 resp = await api.get(`/caldav/events?start=${fetchStart.toISOString()}&end=${fetchEnd.toISOString()}`);
|
||||||
const events = resp.events || resp;
|
const events = resp.events || resp;
|
||||||
if (resp.errors && resp.errors.length) {
|
if (resp.errors && resp.errors.length) {
|
||||||
|
const sourceLabels = { caldav: 'CalDAV', homeassistant: 'Home Assistant', google: 'Google' };
|
||||||
for (const err of resp.errors) {
|
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;
|
eventCache.start = fetchStart;
|
||||||
|
|||||||
Reference in New Issue
Block a user