// Main admin entry point - loads all modules and initializes the application // Load order: config -> state -> utils -> core -> notifications -> ws-client -> ui modules -> main init // Redirect if hash is 'domains' on initial load (should use no hash for default) if (location.hash === '#domains') { // Replace state to remove hash without triggering hashchange history.replaceState(null, '', location.pathname + location.search); } // Initialize when DOM is ready async function initializeApp() { const SCROLLABLE_TABS = ['stats', 'plugins', 'settings']; function updateAdminContentMode(tabId) { const contentEl = document.getElementById('admin-content'); if (!contentEl) return; if (SCROLLABLE_TABS.includes(tabId)) { contentEl.classList.remove('admin-content--fill'); contentEl.classList.add('admin-content--scroll'); } else { contentEl.classList.remove('admin-content--scroll'); contentEl.classList.add('admin-content--fill'); } } function updateAdminNavActive(tabId) { document.querySelectorAll('.admin-nav-link[data-tab-id]').forEach((btn) => { const isActive = btn.dataset.tabId === tabId; btn.classList.toggle('admin-nav-link--active', isActive); btn.setAttribute('aria-current', isActive ? 'page' : 'false'); }); } function closeAdminSidebar() { const sidebar = document.getElementById('admin-sidebar'); const overlay = document.getElementById('admin-sidebar-overlay'); const toggle = document.getElementById('admin-sidebar-toggle'); if (sidebar) sidebar.classList.remove('admin-sidebar--open'); if (overlay) overlay.classList.remove('admin-sidebar-overlay--visible'); if (toggle) toggle.setAttribute('aria-expanded', 'false'); } function toggleAdminSidebar() { const sidebar = document.getElementById('admin-sidebar'); const overlay = document.getElementById('admin-sidebar-overlay'); const toggle = document.getElementById('admin-sidebar-toggle'); if (!sidebar) return; const willOpen = !sidebar.classList.contains('admin-sidebar--open'); sidebar.classList.toggle('admin-sidebar--open', willOpen); if (overlay) overlay.classList.toggle('admin-sidebar-overlay--visible', willOpen); if (toggle) toggle.setAttribute('aria-expanded', willOpen ? 'true' : 'false'); } window.closeAdminSidebar = closeAdminSidebar; window.toggleAdminSidebar = toggleAdminSidebar; window.updateAdminNavActive = updateAdminNavActive; // showTab function - must be defined after all modules are loaded function showTab(tabId) { // If we're already on this tab and it's visible, don't do anything to avoid breaking the page const tabEl = document.getElementById(tabId); if (window.activeTab === tabId && tabEl && !tabEl.classList.contains('hidden')) { return; } document.querySelectorAll('.tab-content').forEach(el => el.classList.add('hidden')); if (tabEl) tabEl.classList.remove('hidden'); window.activeTab = tabId; updateAdminContentMode(tabId); updateAdminNavActive(tabId); closeAdminSidebar(); // Enable/disable body scrolling based on tab const scrollableTabs = SCROLLABLE_TABS; if (scrollableTabs.includes(tabId)) { document.body.classList.remove('no-scroll'); } else { document.body.classList.add('no-scroll'); } if (window.tabs && window.tabs[tabId] && window.genericFetch) { if (tabId === 'backups' && window.renderBackups) { window.renderBackups(); } else if (tabId === 'peers' && window.renderPeers) { window.renderPeers(); } else { window.genericFetch(tabId, true); } // Load saved filter settings for domains tab if (tabId === 'domains' && window.loadDomainFilterSettings) { window.loadDomainFilterSettings(); } } if (tabId === 'host') { // Show servers sub-tab by default if (window.showSubTab) { window.showSubTab('host', 'servers'); } if (!window.wsConnected) { if (window.startPollingFallback) window.startPollingFallback(); } else { if (window.stopPollingFallback) window.stopPollingFallback(); } if (window.stopInterfacesPollingFallback) window.stopInterfacesPollingFallback(); } else if (tabId === 'interfaces') { if (window.stopPollingFallback) window.stopPollingFallback(); if (!window.wsConnected) { if (window.startInterfacesPollingFallback) window.startInterfacesPollingFallback(); } else if (window.stopInterfacesPollingFallback) { window.stopInterfacesPollingFallback(); } } else if (tabId === 'local-dns') { // Show records sub-tab by default if (window.showSubTab) { window.showSubTab('local-dns', 'records'); } } else { if (window.stopPollingFallback) window.stopPollingFallback(); if (window.stopInterfacesPollingFallback) window.stopInterfacesPollingFallback(); } if (tabId === 'logs') { if (window.renderLogs) window.renderLogs(); } if (tabId === 'stats') { if (window.latestStatsSnapshot && window.applyStatsSnapshot) { window.applyStatsSnapshot(window.latestStatsSnapshot); } if (window.renderStats) window.renderStats(); if (window.renderHealth) window.renderHealth(); if (window.renderDiagnostics) window.renderDiagnostics(); if (window.startStatsUpdates) { window.startStatsUpdates(); } if (window.startHealthUpdates) { window.startHealthUpdates(); } } else { if (window.stopStatsPollingFallback) window.stopStatsPollingFallback(); if (window.stopHealthUpdates) window.stopHealthUpdates(); } if (tabId === 'settings') { if (window.renderSettings) { // Fetch settings and render fetch('/api/settings') .then(res => res.json()) .then(async data => { // Set metadata for renderSettings to use if (data.metadata) { window.settingsMetadata = data.metadata; } if (window.renderSettings) await window.renderSettings(data); }) .catch(err => { console.error('Failed to fetch settings:', err); if (window.showNotification) window.showNotification('Failed to load settings', 'error'); }); } } if (tabId === 'plugins') { if (window.renderPlugins) { window.renderPlugins(); } } else { // Clean up plugin terminals when leaving plugins tab if (window.pluginTerminals) { window.pluginTerminals.forEach((term, domain) => { if (window.cleanupPluginTerminal) { window.cleanupPluginTerminal(domain); } }); } } // Note: fetchSubnets() is now called from renderSettings() after the subnet configurator HTML is created // This ensures the DOM elements exist before attempting to populate them } window.showTab = showTab; // Show sub-tab function for tabs with sub-sections function showSubTab(mainTabId, subTabId) { // Hide all sub-tabs in this main tab const mainTab = document.getElementById(mainTabId); if (!mainTab) return; mainTab.querySelectorAll('.sub-tab-content').forEach(el => el.classList.add('hidden')); // Show the selected sub-tab const subTab = document.getElementById(`${mainTabId}-${subTabId}`); if (subTab) { subTab.classList.remove('hidden'); } // Update button styles mainTab.querySelectorAll(`[id^="${mainTabId}-subtab-"]`).forEach(btn => { if (btn.id === `${mainTabId}-subtab-${subTabId}`) { btn.className = 'admin-subtab-btn admin-subtab-btn--active px-4 py-2 rounded transition-colors'; } else { btn.className = 'admin-subtab-btn px-4 py-2 rounded transition-colors'; } }); // Fetch data for the sub-tab if it has a corresponding config // Map sub-tab IDs to config keys const configKeyMap = { 'records': 'local-dns', 'conflicts': 'dns-conflicts', 'p2p-conflicts': 'p2p-domain-conflicts' }; const configKey = configKeyMap[subTabId] || subTabId; if (window.tabs && window.tabs[configKey] && window.genericFetch) { window.genericFetch(configKey, true); } // Fetch data for the sub-tab if needed if (mainTabId === 'host') { if (subTabId === 'servers' && window.genericFetch) { window.genericFetch('host-servers', true); } else if (subTabId === 'clients' && window.genericFetch) { window.genericFetch('host-clients', true); } } else if (mainTabId === 'local-dns') { if (subTabId === 'records' && window.genericFetch) { window.genericFetch('local-dns', true); } else if (subTabId === 'conflicts' && window.genericFetch) { window.genericFetch('dns-conflicts', true); } } } window.showSubTab = showSubTab; // Filter settings function function filterSettings() { const query = document.getElementById('search-settings')?.value.toLowerCase() || ''; const container = document.getElementById('settingsContainer'); if (!container) return; const categories = container.querySelectorAll('.settings-category'); categories.forEach(category => { const categoryTitle = category.querySelector('h3')?.textContent.toLowerCase() || ''; const items = category.querySelectorAll('.settings-item'); let categoryVisible = categoryTitle.includes(query); items.forEach(item => { const label = item.querySelector('label')?.textContent.toLowerCase() || ''; const description = item.querySelector('.settings-description')?.textContent.toLowerCase() || ''; const matches = label.includes(query) || description.includes(query); item.style.display = matches ? '' : 'none'; if (matches) categoryVisible = true; }); category.style.display = categoryVisible ? '' : 'none'; }); } window.filterSettings = filterSettings; // Initial load const hash = location.hash.substring(1); const tabId = hash && document.getElementById(hash) ? hash : 'domains'; // Set initial scroll state const scrollableTabs = ['stats', 'plugins', 'settings']; document.body.classList.add('admin-app'); if (!scrollableTabs.includes(tabId)) { document.body.classList.add('no-scroll'); } if (window.initStatsToolbar) { await window.initStatsToolbar(); } showTab(tabId); if (window.startStatusUpdates) window.startStatusUpdates(); // Pre-load local-dns data so it's available when the tab is accessed if (window.genericFetch) { window.genericFetch('local-dns', false); } if (window.shouldAutoRefreshStats && window.shouldAutoRefreshStats() && window.startStatsUpdates) { window.startStatsUpdates(); } } // Wait for DOM to be ready if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', initializeApp); } else { initializeApp(); } // Navigation helper function - only sets hash if not already on that tab function navigateToTab(tabId) { // Get current tab - check hash first, then fallback to activeTab const currentHash = location.hash.substring(1); let currentTab; if (currentHash && document.getElementById(currentHash)) { currentTab = currentHash; } else { // No hash or invalid hash - use activeTab or default to 'domains' currentTab = window.activeTab || 'domains'; } // If we're already on this tab, don't change the hash // This prevents breaking the page when clicking the same tab button if (currentTab === tabId) { return; } // For domains tab, navigate to index with no hash (since domains is the default) if (tabId === 'domains') { history.replaceState(null, '', location.pathname + location.search); if (window.showTab) { window.showTab('domains'); } return; } // Only set hash if we're not already on this tab location.hash = tabId; } window.navigateToTab = navigateToTab; // Handle hash changes window.addEventListener('hashchange', () => { const tabId = location.hash.substring(1); // Redirect if hash is 'domains' (should use no hash for default) if (tabId === 'domains') { history.replaceState(null, '', location.pathname + location.search); // Show domains tab without hash if (window.showTab) { window.showTab('domains'); } return; } if (tabId && document.getElementById(tabId) && window.showTab) { window.showTab(tabId); } });