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:
2026-03-05 02:26:12 -08:00
commit f61e85d9a7
337 changed files with 75282 additions and 0 deletions

204
status/js/core/init.js Normal file
View 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();
})();
})();