From 01f127881186363830b18c0d86a1d6ddb49f27b1 Mon Sep 17 00:00:00 2001 From: Greg Stevenson Date: Fri, 5 Jun 2026 15:11:08 +0100 Subject: [PATCH] fix: Settings now correctly displays CalDAV integrations when more than one isconfigured (#2901) * fix(calendar): expose source in calendar list and add per-calendar delete - GET /api/calendar/calendars now includes source field so the frontend can distinguish CalDAV collections from local calendars - Add DELETE /api/calendar/calendars/{cal_id} to remove a specific calendar and its events by owner-scoped ID * fix(settings): show all CalDAV calendars in integrations list Previously one card was shown for the CalDAV server connection regardless of how many calendar collections had been synced. The Calendars page showed them all; Settings did not. - Fetch /api/calendar/calendars alongside existing requests and render one card per source=caldav collection, falling back to the single server-level card if nothing has synced yet - Delete now targets the specific calendar by ID rather than clearing the whole server config - Confirm dialog shows the calendar name so the user can verify before removing --- routes/calendar_routes.py | 24 +++++++++++++++++++++++- static/js/settings.js | 27 +++++++++++++++++++++------ 2 files changed, 44 insertions(+), 7 deletions(-) diff --git a/routes/calendar_routes.py b/routes/calendar_routes.py index d1b621ad1..b8bb1e9f6 100644 --- a/routes/calendar_routes.py +++ b/routes/calendar_routes.py @@ -729,6 +729,28 @@ def setup_calendar_routes() -> APIRouter: from src.caldav_sync import sync_caldav return await sync_caldav(owner) + @router.delete("/calendars/{cal_id}") + async def delete_calendar(cal_id: str, request: Request): + owner = _require_user(request) + db = SessionLocal() + try: + cal = db.query(CalendarCal).filter( + CalendarCal.id == cal_id, + CalendarCal.owner == owner, + ).first() + if not cal: + raise HTTPException(404, "Calendar not found") + db.delete(cal) + db.commit() + return {"ok": True} + except HTTPException: + raise + except Exception as e: + logger.error("Failed to delete calendar %s: %s", cal_id, e) + raise HTTPException(500, "Failed to delete calendar") + finally: + db.close() + @router.get("/calendars") async def list_calendars(request: Request): owner = _require_user(request) @@ -737,7 +759,7 @@ def setup_calendar_routes() -> APIRouter: _ensure_default_calendar(db, owner) cals = db.query(CalendarCal).filter(CalendarCal.owner == owner).all() return {"calendars": [ - {"name": c.name, "href": c.id, "color": c.color} + {"name": c.name, "href": c.id, "color": c.color, "source": c.source} for c in cals ]} except HTTPException: diff --git a/static/js/settings.js b/static/js/settings.js index 8269bb65e..068cd80e2 100644 --- a/static/js/settings.js +++ b/static/js/settings.js @@ -3197,7 +3197,7 @@ async function initUnifiedIntegrations() { } async function fetchAll() { - const [apiRes, calRes, cardRes, contactsRes, emailAccountsRes, mcpRes, vaultRes, tokenRes] = await Promise.all([ + const [apiRes, calRes, cardRes, contactsRes, emailAccountsRes, mcpRes, vaultRes, tokenRes, calendarsRes] = await Promise.all([ fetch('/api/auth/integrations', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : { integrations: [] }).catch(() => ({ integrations: [] })), fetch('/api/calendar/config', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : {}).catch(() => ({})), fetch('/api/contacts/config', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : {}).catch(() => ({})), @@ -3206,14 +3206,21 @@ async function initUnifiedIntegrations() { fetch('/api/mcp/servers', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : []).catch(() => []), fetch('/api/vault/config', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : {}).catch(() => ({})), fetch('/api/tokens', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : []).catch(() => []), + fetch('/api/calendar/calendars', { credentials: 'same-origin' }).then(r => r.ok ? r.json() : { calendars: [] }).catch(() => ({ calendars: [] })), ]); const items = []; // API integrations for (const intg of (apiRes.integrations || [])) { items.push({ type: 'api', id: intg.id, name: intg.name || 'Unnamed', detail: intg.base_url || '', enabled: intg.enabled !== false, data: intg }); } - // CalDAV - if (calRes.url) { + // CalDAV — one card per synced calendar collection; fall back to the + // server-level entry if calendars haven't been synced yet. + const caldavCals = (calendarsRes.calendars || []).filter(c => c.source === 'caldav'); + if (caldavCals.length > 0) { + for (const cal of caldavCals) { + items.push({ type: 'caldav', id: cal.href, name: cal.name, detail: calRes.url || 'CalDAV', enabled: true, data: { ...cal, serverData: calRes } }); + } + } else if (calRes.url) { items.push({ type: 'caldav', id: '__caldav__', name: 'Calendar (CalDAV)', detail: calRes.url, enabled: true, data: calRes }); } // Contacts import first, then the optional CardDAV sync account. @@ -3283,7 +3290,7 @@ async function initUnifiedIntegrations() {
${item.detail || ''}
${statusDot} - `; @@ -3321,12 +3328,20 @@ async function initUnifiedIntegrations() { listEl.querySelectorAll('.intg-del-btn').forEach(btn => { btn.addEventListener('click', async (e) => { e.stopPropagation(); - if (!await window.styledConfirm('Remove this integration?', { confirmText: 'Remove', danger: true })) return; + const intgName = btn.dataset.intgName || 'this integration'; + if (!await window.styledConfirm(`Remove "${intgName}"?`, { confirmText: 'Remove', danger: true })) return; const type = btn.dataset.intgType; const id = btn.dataset.intgId; try { if (type === 'api') await fetch(`/api/auth/integrations/${id}`, { method: 'DELETE', credentials: 'same-origin' }); - else if (type === 'caldav') await fetch('/api/calendar/config', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: '', username: '', password: '' }) }); + else if (type === 'caldav') { + if (id === '__caldav__') { + // Fallback card: server configured but never synced — clear credentials + await fetch('/api/calendar/config', { method: 'POST', credentials: 'same-origin', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: '', username: '', password: '' }) }); + } else { + await fetch(`/api/calendar/calendars/${id}`, { method: 'DELETE', credentials: 'same-origin' }); + } + } else if (type === 'contacts') { await fetch('/api/contacts/clear', { method: 'DELETE', credentials: 'same-origin' }); }