/** * Error Handler * Handles errors gracefully without breaking the onboarding tour */ (function(window) { 'use strict'; class ErrorHandler { constructor() { this.errors = []; this.maxErrors = 50; // Keep last 50 errors } /** * Log an error without breaking the tour * @param {string} context - Context where error occurred * @param {Error|string} error - The error object or message * @param {Object} metadata - Additional metadata */ logError(context, error, metadata = {}) { const errorEntry = { timestamp: new Date().toISOString(), context, message: error instanceof Error ? error.message : error, stack: error instanceof Error ? error.stack : null, metadata }; // Add to errors array this.errors.push(errorEntry); // Keep only last maxErrors if (this.errors.length > this.maxErrors) { this.errors.shift(); } // Log to console console.error(`[Onboarding Error] ${context}:`, error, metadata); // Optionally send to error tracking service // this.sendToErrorTracking(errorEntry); } /** * Attempt to recover from an error and continue tour * @param {Error} error - The error object * @param {number} currentStep - Current step index * @returns {Object} Recovery action */ recoverFromError(error, currentStep) { const errorType = this.classifyError(error); switch (errorType) { case 'ELEMENT_NOT_FOUND': this.logError('Element Not Found', error, { currentStep }); return { action: 'SKIP_STEP', nextStep: currentStep + 1, message: 'Target element not found, skipping to next step' }; case 'STORAGE_UNAVAILABLE': this.logError('Storage Unavailable', error); return { action: 'USE_MEMORY_STORAGE', message: 'Local storage unavailable, using in-memory storage' }; case 'DRIVER_NOT_LOADED': this.logError('Driver.js Not Loaded', error); return { action: 'ABORT_TOUR', message: 'Driver.js library not loaded, cannot start tour' }; case 'INVALID_TOOLTIP': this.logError('Invalid Tooltip Configuration', error, { currentStep }); return { action: 'SKIP_STEP', nextStep: currentStep + 1, message: 'Invalid tooltip configuration, skipping' }; case 'THEME_DETECTION_FAILED': this.logError('Theme Detection Failed', error); return { action: 'USE_DEFAULT_THEME', message: 'Using default dark theme' }; default: this.logError('Unknown Error', error, { currentStep }); return { action: 'ABORT_TOUR', message: 'Unexpected error occurred, aborting tour' }; } } /** * Classify error type * @private * @param {Error} error - The error object * @returns {string} Error type */ classifyError(error) { const message = error.message || error.toString(); if (message.includes('element') && message.includes('not found')) { return 'ELEMENT_NOT_FOUND'; } if (message.includes('storage') || message.includes('quota')) { return 'STORAGE_UNAVAILABLE'; } if (message.includes('driver') || message.includes('undefined')) { return 'DRIVER_NOT_LOADED'; } if (message.includes('invalid') || message.includes('validation')) { return 'INVALID_TOOLTIP'; } if (message.includes('theme')) { return 'THEME_DETECTION_FAILED'; } return 'UNKNOWN'; } /** * Get all logged errors * @returns {Array} Array of error entries */ getErrors() { return [...this.errors]; } /** * Clear all logged errors */ clearErrors() { this.errors = []; } /** * Get error statistics * @returns {Object} Error statistics */ getStatistics() { const stats = { total: this.errors.length, byContext: {}, byType: {}, recent: this.errors.slice(-10) }; this.errors.forEach(error => { // Count by context stats.byContext[error.context] = (stats.byContext[error.context] || 0) + 1; // Count by type const type = this.classifyError({ message: error.message }); stats.byType[type] = (stats.byType[type] || 0) + 1; }); return stats; } /** * Handle graceful degradation when Driver.js fails to load * @returns {boolean} Whether fallback was successful */ handleDriverLoadFailure() { this.logError('Driver.js Load Failure', 'Driver.js library failed to load'); // Show fallback message const fallbackMessage = document.createElement('div'); fallbackMessage.id = 'onboarding-fallback'; fallbackMessage.style.cssText = ` position: fixed; bottom: 20px; right: 20px; background: var(--card-base, #2a2a2a); color: var(--fg, #ffffff); padding: 15px 20px; border-radius: 8px; box-shadow: 0 4px 12px rgba(0,0,0,0.3); z-index: 9999; max-width: 300px; font-size: 14px; `; fallbackMessage.innerHTML = ` Welcome to DashCaddy!

The interactive tour is unavailable, but you can explore the dashboard freely. Check the documentation for help getting started.

`; document.body.appendChild(fallbackMessage); // Auto-remove after 10 seconds setTimeout(() => { if (fallbackMessage.parentNode) { fallbackMessage.parentNode.removeChild(fallbackMessage); } }, 10000); return true; } /** * Handle storage unavailable scenario * @returns {Object} In-memory storage fallback */ handleStorageUnavailable() { this.logError('Storage Unavailable', 'Local storage is not available'); // Create in-memory storage const memoryStorage = { data: {}, getItem(key) { return this.data[key] || null; }, setItem(key, value) { this.data[key] = value; }, removeItem(key) { delete this.data[key]; }, clear() { this.data = {}; } }; console.warn('[ErrorHandler] Using in-memory storage - progress will not persist'); return memoryStorage; } /** * Send error to tracking service (placeholder) * @private * @param {Object} errorEntry - Error entry to send */ sendToErrorTracking(errorEntry) { // Placeholder for error tracking integration // Could integrate with Sentry, LogRocket, etc. // Example: // if (window.Sentry) { // Sentry.captureException(new Error(errorEntry.message), { // extra: errorEntry.metadata // }); // } } } window.ErrorHandler = ErrorHandler; console.log('[ErrorHandler] Module loaded'); })(window);