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' }); }