// Topbar component functionality class TopbarComponent { constructor() { this.init(); } getCurrentPage() { const path = window.location.pathname; //// // console.log('๐Ÿ” Topbar: Detecting page from path:', path); // PRIMARY: Use path-based detection only (most reliable) // This avoids confusion from query parameters like tab=, app=, etc. if (path.startsWith('/app') || path === '/app') { return 'app'; } if (path.startsWith('/apps') || path === '/apps') { return 'apps'; } if (path.startsWith('/config') || path === '/config') { return 'config'; } if (path.startsWith('/tasks') || path === '/tasks') { return 'tasks'; } if (path.startsWith('/backup') || path === '/backup') { return 'backup'; } if (path === '/' || path === '/dashboard') { return 'dashboard'; } // Fallback to filename extraction for backward compatibility const filename = path.split('/').pop() || 'dashboard.html'; const pageType = filename.replace('.html', ''); return pageType; } // Static method to load topbar (only once in SPA) static async loadTopbar() { const container = document.getElementById('topbar-container'); if (!container) { console.error('Topbar container not found'); return; } // Load fresh topbar HTML try { //// // console.log('Loading topbar HTML (SPA mode)'); const response = await fetch('html/topbar.html'); if (!response.ok) { throw new Error(`Failed to load topbar: ${response.status}`); } const html = await response.text(); container.innerHTML = html; // Initialize component new TopbarComponent(); } catch (error) { console.error('Error loading topbar:', error); } } init() { this.setupNavigation(); this.setActiveNav(); this.setupThemeManager(); this.setupLogout(); this.setupConfigUpdateLockout(); this.setupSetupGate(); // Mount the "out of date" badge now that .topbar-controls exists. window.updateNotifier?.onTopbarReady(); } // Disable nav items entirely until the Setup Wizard has been completed. // The wizard itself runs as a full-screen overlay that blocks interaction; // this is a belt-and-braces guard for the brief window before the wizard // mounts, and for any nav rendering that happens while it's open. async setupSetupGate() { try { const res = await fetch('/api/setup/status'); if (!res.ok) return; const { complete } = await res.json(); const nav = document.querySelector('.topbar-nav'); if (!nav) return; if (!complete) { nav.classList.add('setup-needed'); } else { nav.classList.remove('setup-needed'); } } catch { /* leave nav enabled if status check fails */ } } // Disable App Center / Config nav while a config_update task runs. setupConfigUpdateLockout() { const setLocked = (locked) => { ['nav-app-center', 'nav-config'].forEach((id) => { const el = document.getElementById(id); if (!el) return; if (locked) { el.classList.add('nav-item-disabled'); el.setAttribute('aria-disabled', 'true'); el.title = 'Disabled while configuration is being applied'; } else { el.classList.remove('nav-item-disabled'); el.removeAttribute('aria-disabled'); el.title = ''; } }); }; window.addEventListener('taskCreated', (event) => { if (event.detail?.action === 'config_update') setLocked(true); }); window.addEventListener('taskCompleted', (event) => { if (event.detail?.action === 'config_update') setLocked(false); }); } setupLogout() { const btn = document.getElementById('logout-btn'); if (btn) { btn.addEventListener('click', () => window.authManager?.logout()); } } setupNavigation() { // Add click handlers to navigation items const navItems = document.querySelectorAll('.nav-item'); navItems.forEach(item => { item.addEventListener('click', (e) => { e.preventDefault(); if (item.classList.contains('nav-item-disabled')) return; const href = item.getAttribute('href'); if (href) { // Special handling for tasks navigation if (item.id === 'nav-tasks' && window.tasksManager) { // console.log('๐Ÿ”„ Tasks button clicked - forcing clean state and reloading...'); // Force clean state window.tasksManager.highlightedTaskId = null; window.tasksManager.currentCategory = 'all'; window.tasksManager.tasks = []; // Clear the task list // Clear URL parameters window.history.pushState({ category: 'all', taskId: null }, '', '/tasks?=all'); // Clear any localStorage filters localStorage.removeItem('tasksDefaultFilter'); // Force reload all tasks window.tasksManager.loadTasks().catch(error => { console.warn('โš ๏ธ Error refreshing tasks:', error); }); } // Use shared navigation utility navigateToRoute(href); } }); }); // Setup mobile menu this.setupMobileMenu(); } setupMobileMenu() { const mobileMenuBtn = document.getElementById('mobile-menu-btn'); const mobileOverlay = document.getElementById('mobile-overlay'); const sidebar = document.getElementById('sidebar'); if (mobileMenuBtn && mobileOverlay) { mobileMenuBtn.addEventListener('click', () => { sidebar.classList.toggle('mobile-open'); mobileOverlay.classList.toggle('active'); document.body.style.overflow = sidebar.classList.contains('mobile-open') ? 'hidden' : ''; }); mobileOverlay.addEventListener('click', () => { sidebar.classList.remove('mobile-open'); mobileOverlay.classList.remove('active'); document.body.style.overflow = ''; }); } } setupThemeManager() { const themeSelector = document.getElementById('theme-selector'); if (!themeSelector) return; const savedTheme = TopbarComponent.resolveSavedTheme(); this.setTheme(savedTheme); // Populate the dropdown from ThemeRegistry. Called twice โ€” once // synchronously with the built-in fallback list, then again after // the API discovery resolves with any custom themes. const renderOptions = (themes) => { const current = localStorage.getItem('theme') || savedTheme; themeSelector.innerHTML = ''; themes.forEach((t) => { const opt = document.createElement('option'); opt.value = t.name; opt.textContent = t.displayName || t.name; themeSelector.appendChild(opt); }); // If the saved theme is now in the list, select it; otherwise // leave whatever the browser chose as the default selection. if (themes.some((t) => t.name === current)) { themeSelector.value = current; } }; if (window.ThemeRegistry && typeof window.ThemeRegistry.onChange === 'function') { window.ThemeRegistry.onChange(renderOptions); } else { // ThemeRegistry didn't load โ€” fall back to the built-in three. renderOptions([ { name: 'nebula', displayName: 'Nebula' }, { name: 'dark-blue', displayName: 'Dark Blue' }, { name: 'light', displayName: 'Light' }, ]); } themeSelector.addEventListener('change', (e) => { this.setTheme(e.target.value); }); } // Reads localStorage, migrates legacy values, and returns the canonical // theme name. Old "dark" / "blue" both map to the new "dark-blue". // An earlier dead initializer wrote to "selectedTheme" โ€” fold that in // and drop it. We don't validate against a fixed list anymore because // custom themes are discovered at runtime โ€” any string is accepted. static resolveSavedTheme() { const legacy = localStorage.getItem('selectedTheme'); if (legacy && !localStorage.getItem('theme')) { localStorage.setItem('theme', legacy); } if (legacy) localStorage.removeItem('selectedTheme'); let theme = localStorage.getItem('theme'); if (theme === 'dark' || theme === 'blue') { theme = 'dark-blue'; localStorage.setItem('theme', theme); } if (!theme) { theme = 'nebula'; localStorage.setItem('theme', theme); } return theme; } setActiveNav() { // Remove active class from all nav items document.querySelectorAll('.nav-item').forEach(item => { item.classList.remove('active'); }); // Use path-based detection only for consistency const path = window.location.pathname; let activeNavId; // PRIMARY: Use path-based detection only (most reliable) if (path.startsWith('/app') || path.startsWith('/apps')) { activeNavId = 'nav-app-center'; } else if (path.startsWith('/config')) { activeNavId = 'nav-config'; } else if (path.startsWith('/tasks')) { activeNavId = 'nav-tasks'; } else if (path.startsWith('/backup')) { activeNavId = 'nav-backup'; } else if (path === '/' || path === '/dashboard') { activeNavId = 'nav-dashboard'; } else { // Fallback to page detection if (this.currentPage === 'dashboard') { activeNavId = 'nav-dashboard'; } else if (this.currentPage === 'index' || this.currentPage === 'app' || this.currentPage === 'apps') { activeNavId = 'nav-app-center'; } else if (this.currentPage === 'config') { activeNavId = 'nav-config'; } else if (this.currentPage === 'tasks') { activeNavId = 'nav-tasks'; } else if (this.currentPage === 'backup') { activeNavId = 'nav-backup'; } else { activeNavId = 'nav-dashboard'; // default } } // Add active class to current page nav if (activeNavId) { const activeNav = document.getElementById(activeNavId); if (activeNav) { activeNav.classList.add('nav-active'); } else { console.warn(`โŒ Nav element not found: ${activeNavId}`); } } else { console.warn(`โŒ Could not determine active nav for path: ${path}`); } } setTheme(theme) { localStorage.setItem('theme', theme); document.documentElement.setAttribute('data-theme', theme); document.body.setAttribute('data-theme', theme); } static clearAllNavigationHighlighting() { //// // console.log('๐Ÿงน Aggressively clearing all navigation highlighting'); // Remove ALL active classes from ALL navigation items document.querySelectorAll('.nav-item').forEach(item => { //// // console.log('๐Ÿงน Clearing nav item:', item.id, item.textContent.trim()); item.classList.remove('active'); item.classList.remove('nav-active'); // Also remove any other possible active states item.removeAttribute('aria-current'); item.blur(); // Remove focus }); // Also clear other active classes that might interfere document.querySelectorAll('.category.active').forEach(item => { item.classList.remove('active'); }); document.querySelectorAll('.tab-button.active').forEach(item => { item.classList.remove('active'); }); //// // console.log('๐Ÿงน Cleared all navigation highlighting'); } static createNavigationHighlighting() { // Define the single function that handles navigation highlighting window.topbarNavigationHighlighting = function() { // Always clear all existing navigation first to prevent sticky highlighting TopbarComponent.clearAllNavigationHighlighting(); // PRIMARY: Use path-based detection only (most reliable) // This avoids confusion from query parameters like tab=, app=, etc. const path = window.location.pathname; let activeNavId; if (path.startsWith('/app') || path.startsWith('/apps')) { activeNavId = 'nav-app-center'; } else if (path.startsWith('/config')) { activeNavId = 'nav-config'; } else if (path.startsWith('/tasks')) { activeNavId = 'nav-tasks'; } else if (path.startsWith('/backup')) { activeNavId = 'nav-backup'; } else if (path === '/' || path === '/dashboard') { activeNavId = 'nav-dashboard'; } else { // Fallback: use currentPage detection const currentPage = new TopbarComponent().getCurrentPage(); switch (currentPage) { case 'index.html': case '': case 'index': case 'app': case 'apps': activeNavId = 'nav-app-center'; break; case 'config': activeNavId = 'nav-config'; break; case 'tasks': activeNavId = 'nav-tasks'; break; case 'backup': activeNavId = 'nav-backup'; break; case 'dashboard': activeNavId = 'nav-dashboard'; break; default: activeNavId = 'nav-dashboard'; } } // Add active class to current page nav if (activeNavId) { const activeNav = document.getElementById(activeNavId); if (activeNav) { activeNav.classList.add('nav-active'); } else { console.warn(`โŒ Topbar: Nav element not found: ${activeNavId}`); } } else { console.warn(`โŒ Topbar: Could not determine active nav for path: ${path}`); } }; //// // console.log('๐ŸŒ Topbar navigation highlighting function created'); } }