Server-side batched /api/v1/services/status endpoint replaces N individual browser probes with a single API call (HEAD-first with GET fallback, concurrency-limited, CA-aware HTTPS agent). Frontend: clock reuses DOM instead of rebuilding innerHTML every second with drift-correcting timer that pauses on hidden tabs. Card animations use CSS transitionDelay + requestAnimationFrame. Internet dot blink moved from JS intervals to CSS keyframes with prefers-reduced-motion support. Service worker rewritten with network-first navigation, stale-while-revalidate assets, and navigation preload. Font faces drop TTF fallbacks, use font-display swap. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
227 lines
7.9 KiB
JavaScript
227 lines
7.9 KiB
JavaScript
// ========== 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();
|
|
})();
|
|
|
|
})();
|