Files
dashcaddy/status/js/core/init.js
Hermes 84edb035e3
CI / Test & Lint (push) Canceled after 0s
CI / Security audit (push) Canceled after 0s
[grade=B] feat(auth): onboard missing credentials into encrypted vault
2026-08-22 05:41:38 -07:00

297 lines
11 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();
await loadTemplateCategories();
window.buildGrid();
if (typeof window.openRequestedCredentialForm === 'function') {
window.openRequestedCredentialForm();
}
animateTopCards();
window.refreshAll();
setInterval(() => {
// Stop polling if the session has been invalidated (e.g. TOTP gate
// now active, or user logged out). Avoids relentless 401/403 noise.
if (window._dcAuthLost) return;
window.refreshAll();
}, DC.POLL.DASHBOARD);
if (typeof window.refreshCredsButtons === 'function') window.refreshCredsButtons();
if (typeof window.refreshMonitoringWidgets === 'function') window.refreshMonitoringWidgets();
// 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();
}
// DC-048: inject the "Admin" trigger button into the top bar. The
// button only renders when /me returns isAdmin=true; the module
// re-checks every 60s so a permission downgrade takes effect.
if (window.AdminPanel && typeof window.AdminPanel.attachTrigger === 'function') {
try {
await window.AdminPanel.attachTrigger(document.body);
} catch (e) {
console.warn('[init] AdminPanel attachTrigger failed:', e);
}
}
}
// 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();
// ===== TEMPLATE CATEGORIES =====
// Cached template categories from /api/v1/templates for use across the UI
// (service create/edit, filter dropdown, category badges, etc.)
async function loadTemplateCategories() {
try {
const r = await fetch('/api/v1/templates', { cache: 'no-store' });
if (!r.ok) return;
const data = await r.json();
if (data && data.categories) {
window.DC_CATEGORIES = data.categories;
// Also expose via globals.js constant for convenience
if (typeof DC !== 'undefined') DC.CATEGORIES = data.categories;
// Populate any category <select> that's already in the DOM
populateCategorySelects();
}
} catch (e) {
console.warn('[init] Failed to load template categories:', e);
}
}
function populateCategorySelects() {
const cats = window.DC_CATEGORIES || (typeof DC !== 'undefined' && DC.CATEGORIES);
if (!cats) return;
document.querySelectorAll('select[data-role="service-category"]').forEach(select => {
const current = select.dataset.current || '';
// Clear options but keep the first (placeholder)
const placeholder = select.querySelector('option[value=""]');
select.innerHTML = '';
if (placeholder) select.appendChild(placeholder);
else {
const ph = document.createElement('option');
ph.value = '';
ph.textContent = '— Select category —';
select.appendChild(ph);
}
Object.entries(cats).forEach(([name, info]) => {
const opt = document.createElement('option');
opt.value = name;
opt.textContent = `${info.icon || ''} ${name}`.trim();
if (name === current) opt.selected = true;
select.appendChild(opt);
});
});
}
// Allow other modules to re-run population after they (re)inject selects
window.populateCategorySelects = populateCategorySelects;
window.loadTemplateCategories = loadTemplateCategories;
// 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();
})();
})();