// ── CalDAV Sync ── import { $, esc, api, getCsrf } from './util.js'; const CAL_ICONS = [ { id: 'calendar-dots', label: 'calendar' }, { id: 'briefcase', label: 'work' }, { id: 'house', label: 'home' }, { id: 'heart', label: 'personal' }, { id: 'cake', label: 'birthday' }, { id: 'graduation-cap', label: 'education' }, { id: 'airplane', label: 'travel' }, { id: 'basketball', label: 'sport' }, { id: 'first-aid', label: 'health' }, { id: 'star', label: 'important' }, { id: 'users', label: 'team' }, { id: 'phone', label: 'calls' }, ]; const CAL_COLORS = ['#38bdf8', '#8b5cf6', '#fb7185', '#fbbf24', '#4ade80', '#fb923c', '#22d3ee', '#f472b6', '#a3e635', '#94a3b8']; let calendarConfigs = {}; // slug → { color, icon } const CAL_CONFIG_KEY = 'hyprcrd-cal-configs'; const overlay = $('caldav-settings'); const urlInput = $('caldav-url'); const userInput = $('caldav-user'); const passInput = $('caldav-pass'); const testStatus = $('caldav-test-status'); const syncStatus = $('caldav-sync-status'); const logEl = $('caldav-log'); export function openCaldavSettings() { overlay?.classList.add('open'); loadSettings(); } export function closeCaldavSettings() { overlay?.classList.remove('open'); } function loadCalConfigs() { try { calendarConfigs = JSON.parse(localStorage.getItem(CAL_CONFIG_KEY) || '{}'); } catch { calendarConfigs = {}; } } function saveCalConfigs() { localStorage.setItem(CAL_CONFIG_KEY, JSON.stringify(calendarConfigs)); } export function getCalConfig(slug) { loadCalConfigs(); return calendarConfigs[slug] || null; } export function getAllCalConfigs() { loadCalConfigs(); return calendarConfigs; } function renderCalList(calendars) { const container = $('caldav-cal-list'); const wrap = $('caldav-calendars'); if (!container || !wrap || !calendars?.length) return; loadCalConfigs(); wrap.classList.remove('hidden'); container.innerHTML = calendars.map(cal => { const slug = cal.name.toLowerCase().replace(/[^a-z0-9_-]/g, ''); const cfg = calendarConfigs[slug] || {}; const activeColor = cfg.color || '#94a3b8'; const activeIcon = cfg.icon || 'calendar-dots'; return `
${esc(cal.name)}
${CAL_COLORS.map(c => ``).join('')}
${CAL_ICONS.slice(0, 6).map(ic => ``).join('')}
`; }).join(''); container.querySelectorAll('.cal-cfg-color').forEach(btn => { btn.addEventListener('click', () => { const slug = btn.closest('[data-cal-slug]').dataset.calSlug; if (!calendarConfigs[slug]) calendarConfigs[slug] = {}; calendarConfigs[slug].color = btn.dataset.color; saveCalConfigs(); renderCalList(calendars); }); }); container.querySelectorAll('.cal-cfg-icon').forEach(btn => { btn.addEventListener('click', () => { const slug = btn.closest('[data-cal-slug]').dataset.calSlug; if (!calendarConfigs[slug]) calendarConfigs[slug] = {}; calendarConfigs[slug].icon = btn.dataset.icon; saveCalConfigs(); renderCalList(calendars); }); }); } async function loadSettings() { const s = await api('api/caldav/settings'); if (!s) return; if (urlInput) urlInput.value = s.url || ''; if (userInput) userInput.value = s.username || ''; if (passInput) passInput.value = ''; if (passInput && s.has_password) passInput.placeholder = '(saved)'; if (syncStatus) syncStatus.textContent = s.last_sync ? `last sync: ${s.last_sync}` : ''; // Load calendar list if configured if (s.url && s.has_password) { const test = await fetch('api/caldav/test', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() }, body: '{}', }).then(r => r.json()).catch(() => null); if (test?.calendars) renderCalList(test.calendars); } } $('caldav-close')?.addEventListener('click', closeCaldavSettings); overlay?.addEventListener('click', e => { if (e.target === overlay) closeCaldavSettings(); }); $('caldav-test')?.addEventListener('click', async () => { testStatus.textContent = 'testing...'; testStatus.style.color = 'var(--t-dim)'; const body = { url: urlInput.value, username: userInput.value }; if (passInput.value) body.password = passInput.value; const res = await fetch('api/caldav/test', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-Token': getCsrf() }, body: JSON.stringify(body), }).then(r => r.json()).catch(() => ({ ok: false, error: 'network error' })); if (res.ok) { const names = (res.calendars || []).map(c => c.name).join(', '); testStatus.textContent = `connected (${names || 'no calendars'})`; testStatus.style.color = 'var(--t-accent)'; if (res.calendars) renderCalList(res.calendars); } else { testStatus.textContent = res.error || 'failed'; testStatus.style.color = 'var(--t-danger)'; } showConnectionLog(res.log); }); $('caldav-save')?.addEventListener('click', async () => { const body = { url: urlInput.value, username: userInput.value, enabled: true }; if (passInput.value) body.password = passInput.value; await api('api/caldav/settings', { method: 'POST', body }); testStatus.textContent = 'saved'; testStatus.style.color = 'var(--t-accent)'; }); $('caldav-sync-now')?.addEventListener('click', () => triggerSync()); export async function triggerSync() { const btn = $('cal-sync'); if (btn) btn.style.opacity = '0.5'; if (syncStatus) { syncStatus.textContent = 'syncing...'; syncStatus.style.color = 'var(--t-dim)'; } const res = await fetch('api/caldav/sync', { method: 'POST', headers: { 'X-CSRF-Token': getCsrf() }, }).then(r => r.json()).catch(() => ({ ok: false, error: 'network error' })); if (btn) btn.style.opacity = ''; if (res.ok) { const s = res.stats || {}; const hasErrors = (res.log || []).some(l => l.status === 'error'); const msg = `pushed:${s.pushed || 0} pulled:${s.pulled || 0} del:${s.deleted || 0}${s.conflicts ? ' conflicts:' + s.conflicts : ''}`; if (syncStatus) { syncStatus.textContent = msg; syncStatus.style.color = hasErrors ? 'var(--t-danger)' : 'var(--t-accent)'; } await window._reloadAll?.(); showLog(res.log); showSyncNotification(s); } else { if (syncStatus) { syncStatus.textContent = res.error || 'sync failed'; syncStatus.style.color = 'var(--t-danger)'; } } } function showLog(log) { if (!logEl || !log?.length) return; logEl.classList.remove('hidden'); logEl.innerHTML = log.map(l => `
${l.dir} ${l.action} ${l.status}${l.detail ? ' — ' + l.detail : ''}
` ).join(''); } function showConnectionLog(lines) { if (!logEl || !lines?.length) return; logEl.classList.remove('hidden'); logEl.innerHTML = lines.map(l => `
${l.replace(/` ).join(''); } $('cal-caldav-settings')?.addEventListener('click', openCaldavSettings); $('cal-sync')?.addEventListener('click', () => triggerSync()); // Auto-sync throttle let lastAutoSync = 0; export function autoSync() { const now = Date.now(); if (now - lastAutoSync < 60000) return; lastAutoSync = now; triggerSync(); } // Background sync every 2 minutes function showSyncNotification(stats) { const s = stats || {}; const parts = []; if (s.pulled) parts.push(`${s.pulled} received`); if (s.pushed) parts.push(`${s.pushed} sent`); if (s.deleted) parts.push(`${s.deleted} deleted`); if (!parts.length) return; const note = document.createElement('div'); note.style.cssText = 'position:fixed;bottom:60px;right:16px;z-index:200;background:var(--t-surface);border:1px solid var(--t-accent);border-radius:4px;padding:10px 14px;box-shadow:0 4px 20px rgba(0,0,0,0.4);max-width:300px;'; note.innerHTML = `
CalDAV: ${parts.join(', ')}
`; document.body.appendChild(note); setTimeout(() => { note.style.transition = 'opacity 0.3s'; note.style.opacity = '0'; setTimeout(() => note.remove(), 350); }, 4000); } async function backgroundSync() { try { const res = await fetch('api/caldav/sync', { method: 'POST', headers: { 'X-CSRF-Token': getCsrf() }, }).then(r => r.json()).catch(() => null); if (res?.ok) { const s = res.stats || {}; if (s.pulled > 0 || s.pushed > 0 || s.deleted > 0) { await window._reloadAll?.(); showSyncNotification(s); } } } catch {} } // Start background sync after app is ready let bgSyncInterval = null; export function startBackgroundSync() { if (bgSyncInterval) return; backgroundSync(); // first sync immediately bgSyncInterval = setInterval(backgroundSync, 120000); // then every 2 minutes }