// Loading UI - Manages the visual loading screen and user feedback class LoadingUI { constructor() { this.container = null; this.progressBar = null; this.systemStatusContainer = null; this.errorMessage = null; this.retryButton = null; this.continueButton = null; this.isVisible = false; this.systemCards = new Map(); } // Initialize loading screen initialize() { this.createLoadingScreen(); this.attachEventListeners(); } // Create the loading screen DOM createLoadingScreen() { this.container = document.createElement('div'); this.container.id = 'libreportal-loading-screen'; this.container.className = 'loading-screen aurora-bg aurora-static'; this.container.innerHTML = `

Drifting softly into your private universe...

0% Starting up...
`; // Add to page document.body.appendChild(this.container); this.isVisible = true; // Get references to elements this.progressBar = document.getElementById('progress-fill'); this.systemStatusContainer = document.getElementById('system-status-container'); this.errorMessage = null; this.retryButton = document.getElementById('retry-button'); this.continueButton = document.getElementById('continue-button'); // Initialize system cards this.initializeSystemCards(); } // Initialize system status cards initializeSystemCards() { const systems = [ { id: 'core', name: 'Core System', icon: 'πŸͺ' }, { id: 'data', name: 'Data Loading', icon: 'πŸ“Š' }, { id: 'components', name: 'UI Components', icon: '🎨' }, { id: 'task', name: 'Task System', icon: 'πŸ“‹' }, { id: 'managers', name: 'System Managers', icon: 'πŸ”§' }, { id: 'backend', name: 'Backend Services', icon: '🌐' }, { id: 'config-validation', name: 'Config Files', icon: 'πŸ”' }, { id: 'update-lock', name: 'Update Lock', icon: 'πŸ”„' }, { id: 'pause-lock', name: 'Pause Lock', icon: '⏸️' } ]; systems.forEach(system => { const card = document.createElement('div'); card.className = 'system-card'; card.id = `system-card-${system.id}`; card.innerHTML = `
${system.icon}
${system.name}
Waiting...
⏳
`; this.systemStatusContainer.appendChild(card); this.systemCards.set(system.id, card); }); } // Update progress updateProgress(progress, details = '') { if (this.progressBar) { this.progressBar.style.width = `${progress}%`; } const percentageElement = document.getElementById('progress-percentage'); if (percentageElement) { percentageElement.textContent = `${Math.round(progress)}%`; } const detailsElement = document.getElementById('progress-details'); if (detailsElement) { detailsElement.textContent = details || `${Math.round(progress)}% complete`; } // Update loading tip this.updateLoadingTip(progress); } // Process message to convert backticks to styled code blocks processMessage(message) { if (!message) return message; // Convert backtick-wrapped text to styled code blocks return message.replace(/`([^`]+)`/g, '$1'); } // Update system check status updateSystemCheck(systemId, checkName, status, error = null, message = null) { const card = this.systemCards.get(systemId); if (!card) return; const statusElement = card.querySelector('.system-status'); const indicatorElement = card.querySelector('.status-icon'); if (!statusElement || !indicatorElement) return; switch (status) { case 'checking': statusElement.textContent = `Checking: ${checkName}`; indicatorElement.textContent = '⏳'; card.className = 'system-card checking'; // Auto-scroll to this card when checking starts this.scrollToCard(card); break; case 'retrying': statusElement.innerHTML = `Retrying: ${checkName}`; indicatorElement.textContent = 'πŸ”„'; card.className = 'system-card retrying'; // Don't auto-scroll during retry to prevent jumping around if (message) { statusElement.title = this.processMessage(message); } break; case 'waiting': statusElement.textContent = `Waiting: ${checkName}`; indicatorElement.textContent = '⏰'; card.className = 'system-card waiting'; break; case 'passed': statusElement.textContent = 'Operational'; indicatorElement.textContent = 'βœ…'; card.className = 'system-card passed'; break; case 'failed': statusElement.textContent = `Failed: ${checkName}`; indicatorElement.textContent = '❌'; card.className = 'system-card failed'; if (error) { statusElement.title = error; } break; case 'skipped': statusElement.textContent = 'Skipped'; indicatorElement.textContent = '⏭️'; card.className = 'system-card skipped'; break; } } // Scroll to specific card smoothly scrollToCard(card) { if (!card || !this.systemStatusContainer) return; const containerHeight = this.systemStatusContainer.clientHeight; const cardHeight = card.offsetHeight; const cardOffsetTop = card.offsetTop; const containerScrollHeight = this.systemStatusContainer.scrollHeight; // Calculate target scroll position to center the card let targetScrollTop = cardOffsetTop - (containerHeight / 2) + (cardHeight / 2); // Ensure we don't scroll past the bottom const maxScrollTop = containerScrollHeight - containerHeight; if (targetScrollTop > maxScrollTop) { targetScrollTop = maxScrollTop; } // Ensure we don't scroll before the top if (targetScrollTop < 0) { targetScrollTop = 0; } // Smooth scroll to the target position this.systemStatusContainer.scrollTo({ top: targetScrollTop, behavior: 'smooth' }); } // Update loading tip based on progress updateLoadingTip(progress) { const tipElement = document.getElementById('loading-tip'); if (!tipElement) return; const tips = [ 'Loading essential components...', 'Preparing your workspace...', 'Checking system dependencies...', 'Initializing data connections...', 'Almost ready...', 'Finalizing launch...' ]; const tipIndex = Math.floor((progress / 100) * tips.length); const tip = tips[Math.min(tipIndex, tips.length - 1)]; if (tipElement.textContent !== tip) { tipElement.textContent = tip; } } // Show error message showError(errors) { // console.log('πŸ” LoadingUI.showError called with errors:', errors.length); // console.log('πŸ” Existing error details elements:', document.querySelectorAll('.error-details').length); // console.log('πŸ” Call stack:', new Error().stack); const actionsContainer = document.getElementById('loading-actions'); // Check if this is a missing data files issue const hasMissingDataFiles = errors.some(error => error.error && (error.error.includes('Missing required data file') || error.error.includes('Critical configuration files are missing or empty')) ); // Create error details const errorDetails = document.createElement('div'); errorDetails.className = 'error-details'; if (hasMissingDataFiles) { // Special handling for missing data files errorDetails.innerHTML = `

πŸ”§ LibrePortal Setup Required

❌ Required data files are missing!

LibrePortal needs to generate JSON configuration files before it can run properly.

libreportal webui generate all

Run this command in your terminal to fix the issue.

After running the setup command, refresh this page to continue.

`; } else { // Regular error handling errorDetails.innerHTML = `

⚠️ Loading Issues Detected

You can retry loading or continue with limited functionality

`; } // Insert before actions if (actionsContainer && actionsContainer.parentNode) { actionsContainer.parentNode.insertBefore(errorDetails, actionsContainer); actionsContainer.style.display = 'flex'; this.retryButton.style.display = 'inline-flex'; this.continueButton.style.display = 'inline-flex'; } } // Hide loading screen hide() { if (this.container && this.isVisible) { // Show success message and trigger animations const successMessage = document.getElementById('success-message'); if (successMessage) { successMessage.style.display = 'block'; } // Add success class to trigger animations this.container.classList.add('success'); // Wait for success animations to play, then hide setTimeout(() => { this.container.classList.add('hiding'); setTimeout(() => { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.isVisible = false; }, 500); }, 200); } } // Show loading screen show() { if (!this.isVisible && this.container) { if (!this.container.parentNode) { document.body.appendChild(this.container); } this.container.classList.remove('hiding'); this.isVisible = true; } } // Update status text updateStatus(text) { const statusElement = document.getElementById('loading-status-text'); if (statusElement) { statusElement.textContent = text; } } // Attach event listeners attachEventListeners() { if (this.retryButton) { this.retryButton.addEventListener('click', () => { // console.log('πŸ”„ Retry button clicked - refreshing page'); window.location.reload(); }); } if (this.continueButton) { this.continueButton.addEventListener('click', () => { this.hide(); // Trigger continue event window.dispatchEvent(new CustomEvent('loadingContinue')); }); } } // Cleanup destroy() { if (this.container && this.container.parentNode) { this.container.parentNode.removeChild(this.container); } this.isVisible = false; this.systemCards.clear(); } }