// Clean SPA Router - Unified routing system for LibrePortal class LibrePortalSPAClean { constructor() { this.routes = new Map(); this.currentRoute = null; this.isLoading = false; this.dataLoaded = false; this.apps = []; this.categories = []; this.init(); } async init() { //console.log('🚀 Clean SPA: Initializing...'); // Setup routes immediately this.setupRoutes(); // Wait for DOM to be ready if (document.readyState === 'loading') { await new Promise(resolve => { document.addEventListener('DOMContentLoaded', resolve); }); } // Wait for topbar to load first await this.waitForTopbar(); // Load data first await this.loadCoreData(); // Handle initial route this.handleInitialRoute(); //console.log('✅ Clean SPA: Initialization complete'); } async waitForTopbar() { //console.log('⏳ Waiting for topbar to load...'); // Wait for topbar component to be available and loaded let attempts = 0; const maxAttempts = 20; // 2 seconds max (reduced from 5 seconds) while (attempts < maxAttempts) { if (typeof TopbarComponent !== 'undefined' && TopbarComponent.loadTopbar) { try { await TopbarComponent.loadTopbar(); //console.log('✅ Topbar loaded successfully'); return; } catch (error) { console.warn('⚠️ Topbar loading failed, retrying...', error); } } await new Promise(resolve => setTimeout(resolve, 100)); attempts++; } console.warn('⚠️ Topbar failed to load after 2 seconds, continuing without topbar'); // Don't block the entire app if topbar fails } setupRoutes() { // Clean route definitions with explicit handlers this.routes.set('/', () => this.handleDashboard()); this.routes.set('/dashboard', () => this.handleDashboard()); this.routes.set('/apps', () => this.handleApps()); this.routes.set('/app', () => this.handleAppDetail()); // Handle /app without query this.routes.set('/app*', () => this.handleAppDetail()); // Handle /app with query this.routes.set('/config', () => this.handleConfig()); // Handle /config without query this.routes.set('/config*', () => this.handleConfig()); // Handle /config with query this.routes.set('/tasks', () => this.handleTasks()); // Handle /tasks without query this.routes.set('/tasks*', () => this.handleTasks()); // Handle /tasks with query this.routes.set('/backup', () => this.handleBackup()); this.routes.set('/backup*', () => this.handleBackup()); //console.log('📍 Routes registered:', Array.from(this.routes.keys())); } async loadCoreData() { //console.log('📊 Loading core data...'); try { // Load apps if (typeof DataLoader !== 'undefined' && DataLoader.loadApps) { this.apps = await DataLoader.loadApps(); window.apps = this.apps; //console.log(`📱 Loaded ${this.apps.length} apps`); } // Load categories if (typeof DataLoader !== 'undefined' && DataLoader.loadCategories) { this.categories = await DataLoader.loadCategories(); window.categories = this.categories; window.sidebarCategories = this.categories; // Ensure this is always available //console.log(`📂 Loaded ${Object.keys(this.categories).length} categories`); } this.dataLoaded = true; //console.log('✅ Core data loaded successfully'); } catch (error) { console.error('❌ Failed to load core data:', error); this.showError('Failed to load application data'); } } handleInitialRoute() { const path = window.location.pathname + window.location.search; // console.log('🎯 SPA: Handling initial route:', path); // Handle root path - redirect to dashboard if (path === '/' || path === '') { // console.log('🏠 SPA: Redirecting to dashboard'); this.navigate('/dashboard', false); } else { // console.log('🔀 SPA: Navigating to:', path); this.navigate(path, false); // Don't add to history for initial load } } async navigate(path, addToHistory = true) { // console.log('🚀 SPA: navigate called with:', path, 'addToHistory:', addToHistory); if (this.isLoading) { // console.log('⏳ Navigation already in progress, ignoring:', path); return; } if (this.currentRoute === path && addToHistory) { //console.log('🔄 Same route, skipping navigation:', path); return; } // Unsaved-config guard — an app's config panel registers // window.__appConfigNavGuard while it has unsaved changes, so it can // intercept navigation away (Apply / Discard / Stay). if (typeof window.__appConfigNavGuard === 'function') { try { const decision = await window.__appConfigNavGuard(path); if (decision === 'stay') return; } catch (e) { console.error('Nav guard error:', e); } } // Force data reload when navigating to dashboard, even if same route if (path === '/dashboard' || path === '/') { // console.log('🔄 Dashboard navigation detected, forcing data reload'); // Trigger dashboard data reload after a short delay to ensure DOM is ready setTimeout(() => { if (typeof loadDashboardData === 'function') { loadDashboardData(); } }, 100); } this.isLoading = true; //console.log('🧭 Navigating to:', path); try { // Update browser history if (addToHistory) { history.pushState({ route: path }, '', path); } this.currentRoute = path; // Find and execute route handler const handler = this.findRouteHandler(path); if (handler) { await handler(); } else { console.warn('❌ No handler found for:', path); this.showError('Page not found'); } // Update navigation highlighting after content loads if (typeof window.topbarNavigationHighlighting === 'function') { //console.log('🔗 SPA: Updating navigation highlighting after navigation'); window.topbarNavigationHighlighting(); } } catch (error) { console.error('❌ Navigation error:', error); this.showError('Navigation failed'); } finally { this.isLoading = false; } } findRouteHandler(path) { //console.log('🔍 Finding handler for path:', path); // Exact match first if (this.routes.has(path)) { //console.log('✅ Exact match found:', path); return this.routes.get(path); } // Handle query parameters - strip them for matching let basePath = path; if (path.includes('?')) { basePath = path.split('?')[0]; } //console.log('🔍 Checking base path:', basePath, 'for full path:', path); // Check base path against routes if (this.routes.has(basePath)) { //console.log('✅ Base path match found:', basePath); return this.routes.get(basePath); } // Wildcard matching for remaining cases for (const [route, handler] of this.routes) { if (route.includes('*')) { const pattern = route.replace('*', ''); if (basePath.startsWith(pattern)) { //console.log('✅ Wildcard match:', pattern, 'for base path:', basePath); return handler; } } } //console.log('❌ No handler found for:', path); //console.log('❌ Base path:', basePath); //console.log('❌ Available routes:', Array.from(this.routes.keys())); return null; } async handleDashboard() { // console.log('🏠 SPA: Loading dashboard...'); try { // console.log('📄 SPA: Fetching dashboard content...'); const html = await this.fetchContent('/html/dashboard-content.html'); // console.log('📄 SPA: Dashboard content fetched, loading...'); this.loadContent(html, 'Dashboard'); // console.log('📄 SPA: Dashboard content loaded'); // Dashboard should already be initialized by SystemLoader if (typeof loadInstalledApps === 'function') { // console.log('📱 SPA: Loading installed apps...'); loadInstalledApps(); } } catch (error) { console.error('❌ Dashboard load error:', error); this.showError('Failed to load dashboard'); } } async handleBackup() { try { const html = await this.fetchContent('/html/backup-content.html'); this.loadContent(html, 'Backups'); if (typeof BackupPage !== 'undefined') { window.backupPage = new BackupPage(); await window.backupPage.init(); } else { console.error('BackupPage class not loaded'); } } catch (error) { console.error('❌ Backup page load error:', error); this.showError('Failed to load backup page'); } } async handleApps() { //console.log('📱 Loading apps...'); // Handle query parameters for apps const path = window.location.pathname + window.location.search; if (path.includes('?=')) { const [basePath, query] = path.split('?='); window.appsCategory = query || 'all'; } else if (path.includes('?')) { const url = new URL(path, window.location.origin); const searchParams = url.searchParams; window.appsCategory = searchParams.get('apps') || 'all'; } else { window.appsCategory = 'all'; } try { // Ensure unified layout is loaded (like the old SPA) if (!document.querySelector('.apps-layout')) { //console.log('📄 Loading apps layout HTML...'); const html = await this.fetchContent('/html/apps-unified-layout.html'); this.loadContent(html, 'Applications'); } else { //console.log('📄 Apps layout already exists, skipping HTML load'); } // Apps manager should already be initialized by SystemLoader if (window.appsManager) { //console.log('✅ AppsManager already initialized by SystemLoader'); await window.appsManager.initialize(); //console.log('✅ Apps loaded successfully'); } else { console.error('AppsManager not available - SystemLoader should have initialized it'); throw new Error('AppsManager not initialized by SystemLoader'); } } catch (error) { console.error('❌ Apps load error:', error); this.showError('Failed to load applications: ' + error.message); } } async handleAppDetail() { //console.log('🔍 Loading app detail...'); // Extract app name from URL const url = new URL(window.location); let appName = url.searchParams.get('app'); // Handle old format ?=appname&tab=tabname if (!appName && url.search.includes('?=')) { const queryPart = url.search.replace('?', ''); const parts = queryPart.split('&'); for (const part of parts) { if (part.startsWith('=')) { appName = part.substring(1); // Remove the '=' break; } } } //console.log('🔍 Parsed app name:', appName, 'from URL:', url.search); if (!appName) { this.navigate('/apps', false); return; } try { const html = await this.fetchContent('/html/apps-unified-layout.html'); this.loadContent(html, appName); // Will be updated after app data loads // AppTabbedManager should already be initialized by SystemLoader if (window.appTabbedManager) { // console.log('✅ AppTabbedManager already initialized by SystemLoader'); await window.appTabbedManager.initialize(); } else { console.error('AppTabbedManager not available - SystemLoader should have initialized it'); throw new Error('AppTabbedManager not initialized by SystemLoader'); } //console.log('✅ App detail loaded:', appName); } catch (error) { console.error('❌ App detail load error:', error); this.showError('Failed to load application details'); } } async handleConfig() { //console.log('⚙️ Loading config...'); // Handle query parameters for config const path = window.location.pathname + window.location.search; if (path.includes('?=')) { const [basePath, query] = path.split('?='); window.configCategory = query || 'general'; } else if (path.includes('?')) { const url = new URL(path, window.location.origin); const searchParams = url.searchParams; window.configCategory = searchParams.get('config') || 'general'; } else { window.configCategory = 'general'; } try { const html = await this.fetchContent('/html/config-content.html'); this.loadContent(html, 'Configuration'); // Config manager should already be initialized by SystemLoader if (window.configManager) { // Render the actual configuration if (typeof window.configManager.renderConfig === 'function') { await window.configManager.renderConfig(window.configCategory || 'general'); } //console.log('✅ Config loaded'); } else { console.error('ConfigManager not available - SystemLoader should have initialized it'); throw new Error('ConfigManager not initialized by SystemLoader'); } } catch (error) { console.error('❌ Config load error:', error); this.showError('Failed to load configuration'); } } async handleTasks() { //console.log('📋 Loading tasks...'); try { const html = await this.fetchContent('/html/tasks-content.html'); this.loadContent(html, 'Tasks'); // Tasks manager should already be initialized by SystemLoader if (window.tasksManager) { //console.log('✅ TasksManager already initialized by SystemLoader'); await window.tasksManager.init(); } else { console.warn('⚠️ TasksManager not available yet, task functionality will be limited'); // Don't throw error - just show warning and continue // The task system will be available when the user actually interacts with tasks } } catch (error) { console.error('❌ Tasks load error:', error); this.showError('Failed to load tasks'); } } async fetchContent(url) { //console.log('📥 Fetching:', url); const response = await fetch(url); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } return await response.text(); } async loadScript(src) { const scriptId = src.replace(/[^a-zA-Z0-9]/g, '_'); if (document.getElementById(scriptId)) { return; // Already loaded } return new Promise((resolve, reject) => { const script = document.createElement('script'); script.src = src; script.id = scriptId; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } loadContent(html, title) { const container = document.getElementById('main-content') || document.querySelector('.main'); if (!container) { throw new Error('Content container not found'); } container.innerHTML = html; document.title = `${title} - LibrePortal`; // Update navigation highlighting this.updateNavigation(); } updateNavigation() { //console.log('🔗 SPA: Using fallback navigation logic'); // Remove ALL active classes document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); item.classList.remove('nav-active'); }); // Use path-based detection only for consistency const path = window.location.pathname; let activeId = 'nav-dashboard'; // default if (path.startsWith('/app') || path.startsWith('/apps')) { activeId = 'nav-app-center'; } else if (path.startsWith('/config')) { activeId = 'nav-config'; } else if (path.startsWith('/tasks')) { activeId = 'nav-tasks'; } else if (path.startsWith('/backup')) { activeId = 'nav-backup'; } else if (path === '/' || path === '/dashboard') { activeId = 'nav-dashboard'; } //console.log('🔗 SPA: Setting active nav:', activeId); const activeElement = document.getElementById(activeId); if (activeElement) { activeElement.classList.add('nav-active'); } } showError(message) { const container = document.getElementById('main-content') || document.querySelector('.main'); if (container) { container.innerHTML = `

Error

${message}

`; } } } // Global navigation function for click handlers window.navigateToRoute = function(href) { if (window.spaClean) { //console.log('🔗 Converting href:', href); // Convert href to clean path - handle various formats let route = href.replace('.html', '').replace('./', '').replace(/^\//, ''); // Handle special cases if (route === '' || route === 'index') { route = '/apps'; // index goes to apps (main app center) } else if (route === 'dashboard') { route = '/dashboard'; } else if (route === 'apps') { route = '/apps'; } else if (route === 'config') { route = '/config?=general'; } else if (route === 'tasks') { route = '/tasks'; } else if (!route.startsWith('/')) { route = '/' + route; } //console.log('🎯 Final route:', route); window.spaClean.navigate(route); } }; // Handle browser back/forward window.addEventListener('popstate', (e) => { if (e.state && e.state.route && window.spaClean) { window.spaClean.navigate(e.state.route, false); } }); // Handle internal link clicks document.addEventListener('click', (e) => { const target = e.target.closest('a'); if (target && target.href) { const href = target.getAttribute('href'); if (href && (href.startsWith('/') || href.startsWith('./'))) { e.preventDefault(); window.navigateToRoute(href); } } }); // SPA initialization is now handled by SystemLoader // LibrePortalSPAClean instance will be created centrally