Initial commit: DashCaddy v1.0
Full codebase including API server (32 modules + routes), dashboard frontend, DashCA certificate distribution, installer script, and deployment skills.
This commit is contained in:
204
status/js/core/init.js
Normal file
204
status/js/core/init.js
Normal file
@@ -0,0 +1,204 @@
|
||||
// ========== 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.opacity = '0';
|
||||
card.style.transform = 'translateY(20px)';
|
||||
setTimeout(() => {
|
||||
card.style.transition = 'opacity 0.3s cubic-bezier(0.4, 0, 0.2, 1), transform 0.3s cubic-bezier(0.4, 0, 0.2, 1)';
|
||||
card.style.opacity = '1';
|
||||
card.style.transform = 'translateY(0)';
|
||||
}, index * 150); // 150ms delay between top cards
|
||||
});
|
||||
}
|
||||
|
||||
// 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 */ }
|
||||
}
|
||||
// Lazy-load onboarding for first-time users, 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() {
|
||||
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;
|
||||
|
||||
// Check if tour has been completed before
|
||||
let tourDone = false;
|
||||
try {
|
||||
const data = JSON.parse(localStorage.getItem('dashcaddy_onboarding'));
|
||||
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;
|
||||
|
||||
// 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();
|
||||
})();
|
||||
|
||||
})();
|
||||
Reference in New Issue
Block a user