// ========== DASHBOARD INITIALIZATION ========== (function () { function loadCustomServices() { const customServices = safeGet('custom-services'); if (customServices) { try { const services = JSON.parse(customServices); // Merge with default APPS, avoiding duplicates services.forEach(service => { if (!window.APPS.find(app => app.id === service.id)) { window.APPS.push(service); } }); } catch (e) { console.warn('Failed to load custom services:', e); } } } // Initialize custom services immediately so window.APPS is populated before buildGrid runs loadCustomServices(); // Staggered animation for top cards too function animateTopCards() { const topCards = document.querySelectorAll('.top .card'); topCards.forEach((card, index) => { card.style.transitionDelay = `${Math.min(index * 60, 300)}ms`; }); requestAnimationFrame(() => { topCards.forEach(card => card.classList.add('loaded')); }); } function registerServiceWorker() { if (!('serviceWorker' in navigator)) return; if (!window.isSecureContext && location.hostname !== 'localhost' && location.hostname !== '127.0.0.1') return; const register = () => { navigator.serviceWorker.register('/sw.js', { updateViaCache: 'none' }).catch((error) => { console.warn('[init] Service worker registration failed:', error); }); }; if (document.readyState === 'complete') { register(); } else { window.addEventListener('load', register, { once: true }); } } // Initialize dashboard (called after TOTP gate check or directly if TOTP disabled) // NOTE: loadServices comes from window.loadServices (exported by grid.js) let _dashboardInitialized = false; async function initializeDashboard() { if (_dashboardInitialized) { console.warn('[init] initializeDashboard called again, skipping duplicate'); return; } _dashboardInitialized = true; await window.loadServices(); window.buildGrid(); animateTopCards(); window.refreshAll(); setInterval(window.refreshAll, DC.POLL.DASHBOARD); if (typeof window.refreshCredsButtons === 'function') window.refreshCredsButtons(); // Update auth card (may have already been updated by the auto-load IIFE but ensure it's correct) if (typeof window._updateAuthCard === 'function') { try { const r = await fetch('/api/v1/totp/config', { cache: 'no-store' }); const d = await r.json(); if (d.success) window._updateAuthCard(d.config.enabled && d.config.isSetUp, d.config.sessionDuration); } catch (e) { /* ignore */ } } if (window.__dashcaddySiteConfigLoaded) { try { await window.__dashcaddySiteConfigLoaded; } catch (_) { /* ignore */ } } // Lazy-load onboarding only once per install, otherwise just add the tour button addTourButton(); if (shouldLoadOnboarding()) { loadOnboarding(); } } // Lazy-load onboarding bundle (52 KB) — only loaded when needed function loadOnboarding() { if (document.querySelector('script[src="/dist/onboarding.js"]')) return; // already loading/loaded const s = document.createElement('script'); s.src = '/dist/onboarding.js'; s.defer = true; document.head.appendChild(s); // Also load onboarding CSS if not already present if (!document.querySelector('link[href="/css/driver.min.css"]')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/css/driver.min.css'; document.head.appendChild(link); } if (!document.querySelector('link[href="/css/onboarding.css"]')) { const link = document.createElement('link'); link.rel = 'stylesheet'; link.href = '/css/onboarding.css'; document.head.appendChild(link); } } // Check if onboarding should auto-start (first-time user) function shouldLoadOnboarding() { if (typeof SITE !== 'undefined' && SITE.onboardingCompleted) { return false; } try { const data = JSON.parse(localStorage.getItem('dashcaddy_onboarding')); return !data || (!data.tourCompleted && data.currentStep === 0); } catch (_) { return true; // No data means first-time user } } // ===== Collapsible toolbar sections ===== function initToolbarSections() { const sections = document.querySelectorAll('.tools-section'); if (!sections.length) return; // Restore saved state from localStorage let saved = {}; try { saved = JSON.parse(localStorage.getItem('toolbar-sections') || '{}'); } catch (_) {} sections.forEach(section => { const key = section.dataset.section; const header = section.querySelector('.tools-section-header'); if (!header) return; // Restore state (default: collapsed) if (saved[key]) { section.classList.add('open'); header.setAttribute('aria-expanded', 'true'); } header.addEventListener('click', (e) => { e.preventDefault(); const isOpen = section.classList.toggle('open'); header.setAttribute('aria-expanded', isOpen ? 'true' : 'false'); // Save state const state = {}; document.querySelectorAll('.tools-section').forEach(s => { state[s.dataset.section] = s.classList.contains('open'); }); localStorage.setItem('toolbar-sections', JSON.stringify(state)); }); }); } // Initialize toolbar sections on DOM ready initToolbarSections(); // Add restart tour button (loads bundle on click if not loaded) // Visible in primary toolbar until tour completed once, then moves to Admin section function addTourButton() { if (document.getElementById('restart-tour-btn')) return; let tourDone = typeof SITE !== 'undefined' && SITE.onboardingCompleted; try { const data = JSON.parse(localStorage.getItem('dashcaddy_onboarding')); tourDone = tourDone || !!(data && data.tourCompleted); } catch (_) {} // Before first completion: show in primary toolbar. After: tuck into Admin section. const target = tourDone ? document.querySelector('.tools-section[data-section="admin"] .tools-section-items') : document.querySelector('.tools-primary'); if (!target) return; const button = document.createElement('button'); button.id = 'restart-tour-btn'; button.textContent = tourDone ? 'Help Tour' : '🎓 Help Tour'; button.title = 'Restart the onboarding tour'; button.onclick = () => { if (window.DashCaddyOnboarding) { window.DashCaddyOnboarding.restartTour(); } else { loadOnboarding(); // Wait for bundle to load, then start const check = setInterval(() => { if (window.DashCaddyOnboarding) { clearInterval(check); window.DashCaddyOnboarding.restartTour(); } }, 100); setTimeout(() => clearInterval(check), 5000); // give up after 5s } }; target.appendChild(button); } window.initializeDashboard = initializeDashboard; window.loadCustomServices = loadCustomServices; registerServiceWorker(); // TOTP-gated initialization (async () => { try { const totpRes = await fetch('/api/v1/totp/config', { cache: 'no-store' }); const totpData = await totpRes.json(); if (totpData.success && totpData.config.enabled) { // TOTP is enabled - check if we have a valid session const testRes = await fetch('/api/v1/totp/check-session', { cache: 'no-store' }); if (testRes.status === 401) { // Need TOTP verification - show overlay window._showTotpOverlay(); return; // initializeDashboard() will be called after successful verification } } } catch (e) { console.warn('TOTP check failed, proceeding normally:', e); } // TOTP disabled or session valid - initialize immediately initializeDashboard(); })(); })();