/** * Huntarr - New UI Implementation * Main JavaScript file for handling UI interactions and API communication */ /** * Huntarr - New UI Implementation * Main JavaScript file for handling UI interactions and API communication */ let huntarrUI = { // Current state currentSection: 'home', // Default section currentHistoryApp: 'all', // Default history app currentLogApp: 'all', // Default log app for compatibility autoScroll: true, eventSources: {}, // Event sources for compatibility isLoadingStats: false, // Flag to prevent multiple simultaneous stats requests configuredApps: { sonarr: false, radarr: false, lidarr: false, readarr: false, // Added readarr whisparr: false, // Added whisparr eros: false, // Added eros swaparr: false // Added swaparr }, originalSettings: {}, // Store the full original settings object settingsChanged: false, // Legacy flag (auto-save enabled) // Logo URL logoUrl: './static/logo/256.png', // Element references elements: {}, // Initialize the application init: function() { console.log('[huntarrUI] Initializing UI...'); // Skip initialization on login page const isLoginPage = document.querySelector('.login-container, #loginForm, .login-form'); if (isLoginPage) { console.log('[huntarrUI] Login page detected, skipping full initialization'); return; } // Cache frequently used DOM elements this.cacheElements(); // Register event handlers this.setupEventListeners(); this.setupLogoHandling(); // Auto-save enabled - no unsaved changes handler needed // Check if Low Usage Mode is enabled BEFORE loading stats to avoid race condition this.checkLowUsageMode().then(() => { // Initialize media stats after low usage mode is determined if (window.location.pathname === '/') { this.loadMediaStats(); } }).catch(() => { // If low usage mode check fails, still load stats if (window.location.pathname === '/') { this.loadMediaStats(); } }); // Check if we need to navigate to a specific section after refresh const targetSection = localStorage.getItem('huntarr-target-section'); if (targetSection) { console.log(`[huntarrUI] Found target section after refresh: ${targetSection}`); localStorage.removeItem('huntarr-target-section'); // Navigate to the target section this.switchSection(targetSection); } else { // Initial navigation based on hash this.handleHashNavigation(window.location.hash); } // Remove initial sidebar hiding style const initialSidebarStyle = document.getElementById('initial-sidebar-state'); if (initialSidebarStyle) { initialSidebarStyle.remove(); } // Check which sidebar should be shown based on current section console.log(`[huntarrUI] Initialization - current section: ${this.currentSection}`); if (this.currentSection === 'settings' || this.currentSection === 'scheduling' || this.currentSection === 'notifications' || this.currentSection === 'backup-restore' || this.currentSection === 'user') { console.log('[huntarrUI] Initialization - showing settings sidebar'); this.showSettingsSidebar(); } else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-history') { console.log('[huntarrUI] Initialization - showing requestarr sidebar'); this.showRequestarrSidebar(); } else if (this.currentSection === 'apps' || this.currentSection === 'sonarr' || this.currentSection === 'radarr' || this.currentSection === 'lidarr' || this.currentSection === 'readarr' || this.currentSection === 'whisparr' || this.currentSection === 'eros' || this.currentSection === 'prowlarr') { console.log('[huntarrUI] Initialization - showing apps sidebar'); this.showAppsSidebar(); } else { // Show main sidebar by default and clear settings sidebar preference console.log('[huntarrUI] Initialization - showing main sidebar (default)'); localStorage.removeItem('huntarr-settings-sidebar'); localStorage.removeItem('huntarr-apps-sidebar'); this.showMainSidebar(); } // Auto-save enabled - no unsaved changes handler needed // Load username this.loadUsername(); // Apply any preloaded theme immediately to avoid flashing const prefersDarkMode = localStorage.getItem('huntarr-dark-mode') === 'true'; if (prefersDarkMode) { document.body.classList.add('dark-theme'); } const resetButton = document.getElementById('reset-stats'); if (resetButton) { resetButton.addEventListener('click', (e) => { e.preventDefault(); this.resetMediaStats(); }); } // Ensure logo is visible immediately this.logoUrl = localStorage.getItem('huntarr-logo-url') || this.logoUrl; // Load current version this.loadCurrentVersion(); // Load current version // Load latest version from GitHub this.loadLatestVersion(); // Load latest version from GitHub // Load latest beta version from GitHub this.loadBetaVersion(); // Load latest beta version from GitHub // Load GitHub star count this.loadGitHubStarCount(); // Load GitHub star count // Preload stateful management info so it's ready when needed this.loadStatefulInfo(); // Ensure logo is applied if (typeof window.applyLogoToAllElements === 'function') { window.applyLogoToAllElements(); } // Initialize instance event handlers this.setupInstanceEventHandlers(); // Setup navigation for sidebars this.setupRequestarrNavigation(); this.setupAppsNavigation(); this.setupSettingsNavigation(); // Auto-save enabled - no unsaved changes handler needed // Setup Swaparr components this.setupSwaparrResetCycle(); // Setup Swaparr status polling (refresh every 30 seconds) this.setupSwaparrStatusPolling(); // Setup Prowlarr status polling (refresh every 30 seconds) this.setupProwlarrStatusPolling(); // Make dashboard visible after initialization to prevent FOUC setTimeout(() => { this.showDashboard(); // Mark as initialized after everything is set up to enable refresh on section changes this.isInitialized = true; console.log('[huntarrUI] Initialization complete - refresh on section change enabled'); }, 50); // Reduced from implicit longer delay }, // Cache DOM elements for better performance cacheElements: function() { // Navigation this.elements.navItems = document.querySelectorAll('.nav-item'); this.elements.homeNav = document.getElementById('homeNav'); this.elements.logsNav = document.getElementById('logsNav'); this.elements.huntManagerNav = document.getElementById('huntManagerNav'); this.elements.settingsNav = document.getElementById('settingsNav'); this.elements.userNav = document.getElementById('userNav'); // Sections this.elements.sections = document.querySelectorAll('.content-section'); this.elements.homeSection = document.getElementById('homeSection'); this.elements.logsSection = document.getElementById('logsSection'); this.elements.huntManagerSection = document.getElementById('huntManagerSection'); this.elements.settingsSection = document.getElementById('settingsSection'); this.elements.schedulingSection = document.getElementById('schedulingSection'); // History dropdown elements this.elements.historyOptions = document.querySelectorAll('.history-option'); // History dropdown options this.elements.currentHistoryApp = document.getElementById('current-history-app'); // Current history app text this.elements.historyDropdownBtn = document.querySelector('.history-dropdown-btn'); // History dropdown button this.elements.historyDropdownContent = document.querySelector('.history-dropdown-content'); // History dropdown content this.elements.historyPlaceholderText = document.getElementById('history-placeholder-text'); // Placeholder text for history // Settings dropdown elements this.elements.settingsOptions = document.querySelectorAll('.settings-option'); // New: settings dropdown options this.elements.currentSettingsApp = document.getElementById('current-settings-app'); // New: current settings app text this.elements.settingsDropdownBtn = document.querySelector('.settings-dropdown-btn'); // New: settings dropdown button this.elements.settingsDropdownContent = document.querySelector('.settings-dropdown-content'); // New: dropdown content this.elements.appSettingsPanels = document.querySelectorAll('.app-settings-panel'); // Settings // Save button removed for auto-save // Status elements this.elements.sonarrHomeStatus = document.getElementById('sonarrHomeStatus'); this.elements.radarrHomeStatus = document.getElementById('radarrHomeStatus'); this.elements.lidarrHomeStatus = document.getElementById('lidarrHomeStatus'); this.elements.readarrHomeStatus = document.getElementById('readarrHomeStatus'); // Added readarr this.elements.whisparrHomeStatus = document.getElementById('whisparrHomeStatus'); // Added whisparr this.elements.erosHomeStatus = document.getElementById('erosHomeStatus'); // Added eros // Actions this.elements.startHuntButton = document.getElementById('startHuntButton'); this.elements.stopHuntButton = document.getElementById('stopHuntButton'); // Logout this.elements.logoutLink = document.getElementById('logoutLink'); // Added logout link }, // Set up event listeners setupEventListeners: function() { // Navigation document.addEventListener('click', (e) => { // Navigation link handling if (e.target.matches('.nav-link') || e.target.closest('.nav-link')) { const link = e.target.matches('.nav-link') ? e.target : e.target.closest('.nav-link'); e.preventDefault(); this.handleNavigation(e); } // Handle cycle reset button clicks if (e.target.matches('.cycle-reset-button') || e.target.closest('.cycle-reset-button')) { const button = e.target.matches('.cycle-reset-button') ? e.target : e.target.closest('.cycle-reset-button'); const app = button.dataset.app; if (app) { this.resetAppCycle(app, button); } } }); // History dropdown toggle if (this.elements.historyDropdownBtn) { this.elements.historyDropdownBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Prevent event bubbling // Toggle this dropdown this.elements.historyDropdownContent.classList.toggle('show'); }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.history-dropdown') && this.elements.historyDropdownContent.classList.contains('show')) { this.elements.historyDropdownContent.classList.remove('show'); } }); } // History options this.elements.historyOptions.forEach(option => { option.addEventListener('click', (e) => this.handleHistoryOptionChange(e)); }); // Settings dropdown toggle if (this.elements.settingsDropdownBtn) { this.elements.settingsDropdownBtn.addEventListener('click', (e) => { e.preventDefault(); e.stopPropagation(); // Prevent event bubbling // Toggle this dropdown this.elements.settingsDropdownContent.classList.toggle('show'); }); // Close dropdown when clicking outside document.addEventListener('click', (e) => { if (!e.target.closest('.settings-dropdown') && this.elements.settingsDropdownContent.classList.contains('show')) { this.elements.settingsDropdownContent.classList.remove('show'); } }); } // Settings options this.elements.settingsOptions.forEach(option => { option.addEventListener('click', (e) => this.handleSettingsOptionChange(e)); }); // Save settings button // Save button removed for auto-save // Test notification button (delegated event listener for dynamic content) document.addEventListener('click', (e) => { if (e.target.id === 'testNotificationBtn' || e.target.closest('#testNotificationBtn')) { this.testNotification(); } }); // Start hunt button if (this.elements.startHuntButton) { this.elements.startHuntButton.addEventListener('click', () => this.startHunt()); } // Stop hunt button if (this.elements.stopHuntButton) { this.elements.stopHuntButton.addEventListener('click', () => this.stopHunt()); } // Logout button if (this.elements.logoutLink) { this.elements.logoutLink.addEventListener('click', (e) => this.logout(e)); } // Requestarr navigation this.setupRequestarrNavigation(); // Dark mode toggle const darkModeToggle = document.getElementById('darkModeToggle'); if (darkModeToggle) { const prefersDarkMode = localStorage.getItem('huntarr-dark-mode') === 'true'; darkModeToggle.checked = prefersDarkMode; darkModeToggle.addEventListener('change', function() { const isDarkMode = this.checked; document.body.classList.toggle('dark-theme', isDarkMode); localStorage.setItem('huntarr-dark-mode', isDarkMode); }); } // Settings now use manual save - no auto-save setup console.log('[huntarrUI] Settings using manual save - skipping auto-save setup'); // Auto-save enabled - no need to warn about unsaved changes // Stateful management reset button const resetStatefulBtn = document.getElementById('reset_stateful_btn'); if (resetStatefulBtn) { resetStatefulBtn.addEventListener('click', () => this.handleStatefulReset()); } // Stateful management hours input const statefulHoursInput = document.getElementById('stateful_management_hours'); if (statefulHoursInput) { statefulHoursInput.addEventListener('change', () => { this.updateStatefulExpirationOnUI(); }); } // Handle window hash change window.addEventListener('hashchange', () => this.handleHashNavigation(window.location.hash)); // Ensure hash is passed // Settings form delegation - now triggers auto-save const settingsFormContainer = document.querySelector('#settingsSection'); if (settingsFormContainer) { // Settings now use manual save - remove auto-save event listeners console.log('[huntarrUI] Settings section using manual save - no auto-save listeners'); } // Auto-save enabled - no need for beforeunload warnings // Initial setup based on hash or default to home const initialHash = window.location.hash || '#home'; this.handleHashNavigation(initialHash); // HISTORY: Listen for change on #historyAppSelect const historyAppSelect = document.getElementById('historyAppSelect'); if (historyAppSelect) { historyAppSelect.addEventListener('change', (e) => { const app = e.target.value; this.handleHistoryOptionChange(app); }); } }, // Setup logo handling to prevent flashing during navigation setupLogoHandling: function() { // Get the logo image const logoImg = document.querySelector('.sidebar .logo'); if (logoImg) { // Cache the source this.logoSrc = logoImg.src; // Ensure it's fully loaded if (!logoImg.complete) { logoImg.onload = () => { // Once loaded, store the source this.logoSrc = logoImg.src; }; } } // Also add event listener to ensure logo is preserved during navigation window.addEventListener('beforeunload', () => { // Store logo src in session storage to persist across page loads if (this.logoSrc) { sessionStorage.setItem('huntarr-logo-src', this.logoSrc); } }); }, // Navigation handling handleNavigation: function(e) { const targetElement = e.currentTarget; // Get the clicked nav item const href = targetElement.getAttribute('href'); const target = targetElement.getAttribute('target'); // Allow links with target="_blank" to open in a new window (return early) if (target === '_blank') { return; // Let the default click behavior happen } // For all other links, prevent default behavior and handle internally e.preventDefault(); if (!href) return; // Exit if no href let targetSection = null; let isInternalLink = href.startsWith('#'); if (isInternalLink) { targetSection = href.substring(1) || 'home'; // Get section from hash, default to 'home' if only '#' } else { // Handle external links (like /user) or non-hash links if needed // For now, assume non-hash links navigate away } // Auto-save enabled - no need to check for unsaved changes when navigating // Add special handling for apps section - clear global app module flags if (this.currentSection === 'apps' && targetSection !== 'apps') { // Reset the app module flags when navigating away if (window._appsModuleLoaded) { window._appsSuppressChangeDetection = true; if (window.appsModule && typeof window.appsModule.settingsChanged !== 'undefined') { window.appsModule.settingsChanged = false; } // Schedule ending suppression to avoid any edge case issues setTimeout(() => { window._appsSuppressChangeDetection = false; }, 1000); } } // Proceed with navigation if (isInternalLink) { window.location.hash = href; // Change hash to trigger handleHashNavigation } else { // If it's an external link (like /user), just navigate normally window.location.href = href; } }, handleHashNavigation: function(hash) { const section = hash.substring(1) || 'home'; this.switchSection(section); }, switchSection: function(section) { console.log(`[huntarrUI] *** SWITCH SECTION CALLED *** section: ${section}, current: ${this.currentSection}`); // Check for unsaved changes before allowing navigation if (this.isInitialized && this.currentSection && this.currentSection !== section) { // Check for unsaved Swaparr changes if leaving Swaparr section if (this.currentSection === 'swaparr' && window.SettingsForms && typeof window.SettingsForms.checkUnsavedChanges === 'function') { if (!window.SettingsForms.checkUnsavedChanges()) { console.log(`[huntarrUI] Navigation cancelled due to unsaved Swaparr changes`); return; // User chose to stay and save changes } } // Check for unsaved Settings changes if leaving Settings section if (this.currentSection === 'settings' && window.SettingsForms && typeof window.SettingsForms.checkUnsavedChanges === 'function') { if (!window.SettingsForms.checkUnsavedChanges()) { console.log(`[huntarrUI] Navigation cancelled due to unsaved Settings changes`); return; // User chose to stay and save changes } } // Check for unsaved Notifications changes if leaving Notifications section if (this.currentSection === 'notifications' && window.SettingsForms && typeof window.SettingsForms.checkUnsavedChanges === 'function') { if (!window.SettingsForms.checkUnsavedChanges()) { console.log(`[huntarrUI] Navigation cancelled due to unsaved Notifications changes`); return; // User chose to stay and save changes } } // Check for unsaved App instance changes if leaving Apps section const appSections = ['apps']; if (appSections.includes(this.currentSection) && window.SettingsForms && typeof window.SettingsForms.checkUnsavedChanges === 'function') { if (!window.SettingsForms.checkUnsavedChanges()) { console.log(`[huntarrUI] Navigation cancelled due to unsaved App changes`); return; // User chose to stay and save changes } } // Check for unsaved Prowlarr changes if leaving Prowlarr section if (this.currentSection === 'prowlarr' && window.SettingsForms && typeof window.SettingsForms.checkUnsavedChanges === 'function') { if (!window.SettingsForms.checkUnsavedChanges()) { console.log(`[huntarrUI] Navigation cancelled due to unsaved Prowlarr changes`); return; // User chose to stay and save changes } } console.log(`[huntarrUI] User switching from ${this.currentSection} to ${section}, refreshing page...`); // Store the target section in localStorage so we can navigate to it after refresh localStorage.setItem('huntarr-target-section', section); location.reload(); return; } // Update active section this.elements.sections.forEach(s => { s.classList.remove('active'); s.style.display = 'none'; }); // Additionally, make sure scheduling section is completely hidden if (section !== 'scheduling' && this.elements.schedulingSection) { this.elements.schedulingSection.style.display = 'none'; } // Update navigation this.elements.navItems.forEach(item => { item.classList.remove('active'); }); // Show selected section let newTitle = 'Home'; // Default title const sponsorsSection = document.getElementById('sponsorsSection'); // Get sponsors section element const sponsorsNav = document.getElementById('sponsorsNav'); // Get sponsors nav element if (section === 'home' && this.elements.homeSection) { this.elements.homeSection.classList.add('active'); this.elements.homeSection.style.display = 'block'; if (this.elements.homeNav) this.elements.homeNav.classList.add('active'); newTitle = 'Home'; this.currentSection = 'home'; // Show main sidebar when returning to home and clear settings sidebar preference localStorage.removeItem('huntarr-settings-sidebar'); this.showMainSidebar(); // Disconnect logs if switching away from logs this.disconnectAllEventSources(); // Check app connections when returning to home page to update status this.checkAppConnections(); // Load Swaparr status this.loadSwaparrStatus(); // Stats are already loaded, no need to reload unless data changed // this.loadMediaStats(); } else if (section === 'logs' && this.elements.logsSection) { this.elements.logsSection.classList.add('active'); this.elements.logsSection.style.display = 'block'; if (this.elements.logsNav) this.elements.logsNav.classList.add('active'); newTitle = 'Logs'; this.currentSection = 'logs'; // Show main sidebar for main sections and clear settings sidebar preference localStorage.removeItem('huntarr-settings-sidebar'); this.showMainSidebar(); // Comprehensive LogsModule debugging console.log('[huntarrUI] === LOGS SECTION DEBUG START ==='); console.log('[huntarrUI] window object keys:', Object.keys(window).filter(k => k.includes('Log'))); console.log('[huntarrUI] window.LogsModule exists:', !!window.LogsModule); console.log('[huntarrUI] window.LogsModule type:', typeof window.LogsModule); if (window.LogsModule) { console.log('[huntarrUI] LogsModule methods:', Object.keys(window.LogsModule)); console.log('[huntarrUI] LogsModule.init type:', typeof window.LogsModule.init); console.log('[huntarrUI] LogsModule.connectToLogs type:', typeof window.LogsModule.connectToLogs); try { console.log('[huntarrUI] Calling LogsModule.init()...'); window.LogsModule.init(); console.log('[huntarrUI] LogsModule.init() completed successfully'); // LogsModule will handle its own connection - don't interfere with pagination console.log('[huntarrUI] LogsModule initialized - letting it handle its own connections'); } catch (error) { console.error('[huntarrUI] Error during LogsModule calls:', error); } } else { console.error('[huntarrUI] LogsModule not found - logs functionality unavailable'); console.log('[huntarrUI] Available window properties:', Object.keys(window).slice(0, 20)); } console.log('[huntarrUI] === LOGS SECTION DEBUG END ==='); } else if (section === 'hunt-manager' && document.getElementById('huntManagerSection')) { document.getElementById('huntManagerSection').classList.add('active'); document.getElementById('huntManagerSection').style.display = 'block'; if (document.getElementById('huntManagerNav')) document.getElementById('huntManagerNav').classList.add('active'); newTitle = 'Hunt Manager'; this.currentSection = 'hunt-manager'; // Show main sidebar for main sections and clear settings sidebar preference localStorage.removeItem('huntarr-settings-sidebar'); this.showMainSidebar(); // Load hunt manager data if the module exists if (typeof huntManagerModule !== 'undefined') { huntManagerModule.refresh(); } } else if (section === 'requestarr' && document.getElementById('requestarr-section')) { document.getElementById('requestarr-section').classList.add('active'); document.getElementById('requestarr-section').style.display = 'block'; if (document.getElementById('requestarrNav')) document.getElementById('requestarrNav').classList.add('active'); newTitle = 'Requestarr'; this.currentSection = 'requestarr'; // Switch to Requestarr sidebar this.showRequestarrSidebar(); // Show home view by default this.showRequestarrView('home'); // Initialize requestarr module if it exists if (typeof window.requestarrModule !== 'undefined') { window.requestarrModule.loadInstances(); } } else if (section === 'requestarr-history' && document.getElementById('requestarr-section')) { document.getElementById('requestarr-section').classList.add('active'); document.getElementById('requestarr-section').style.display = 'block'; newTitle = 'Requestarr - History'; this.currentSection = 'requestarr-history'; // Switch to Requestarr sidebar this.showRequestarrSidebar(); // Show history view this.showRequestarrView('history'); } else if (section === 'apps') { console.log('[huntarrUI] Apps section requested - redirecting to Sonarr by default'); // Instead of showing apps dashboard, redirect to Sonarr this.switchSection('sonarr'); window.location.hash = '#sonarr'; return; } else if (section === 'sonarr' && document.getElementById('sonarrSection')) { document.getElementById('sonarrSection').classList.add('active'); document.getElementById('sonarrSection').style.display = 'block'; if (document.getElementById('appsSonarrNav')) document.getElementById('appsSonarrNav').classList.add('active'); newTitle = 'Sonarr'; this.currentSection = 'sonarr'; // Switch to Apps sidebar this.showAppsSidebar(); // Initialize app module for sonarr if (typeof appsModule !== 'undefined') { appsModule.init('sonarr'); } } else if (section === 'radarr' && document.getElementById('radarrSection')) { document.getElementById('radarrSection').classList.add('active'); document.getElementById('radarrSection').style.display = 'block'; if (document.getElementById('appsRadarrNav')) document.getElementById('appsRadarrNav').classList.add('active'); newTitle = 'Radarr'; this.currentSection = 'radarr'; // Switch to Apps sidebar this.showAppsSidebar(); // Initialize app module for radarr if (typeof appsModule !== 'undefined') { appsModule.init('radarr'); } } else if (section === 'lidarr' && document.getElementById('lidarrSection')) { document.getElementById('lidarrSection').classList.add('active'); document.getElementById('lidarrSection').style.display = 'block'; if (document.getElementById('appsLidarrNav')) document.getElementById('appsLidarrNav').classList.add('active'); newTitle = 'Lidarr'; this.currentSection = 'lidarr'; // Switch to Apps sidebar this.showAppsSidebar(); // Initialize app module for lidarr if (typeof appsModule !== 'undefined') { appsModule.init('lidarr'); } } else if (section === 'readarr' && document.getElementById('readarrSection')) { document.getElementById('readarrSection').classList.add('active'); document.getElementById('readarrSection').style.display = 'block'; if (document.getElementById('appsReadarrNav')) document.getElementById('appsReadarrNav').classList.add('active'); newTitle = 'Readarr'; this.currentSection = 'readarr'; // Switch to Apps sidebar this.showAppsSidebar(); // Initialize app module for readarr if (typeof appsModule !== 'undefined') { appsModule.init('readarr'); } } else if (section === 'whisparr' && document.getElementById('whisparrSection')) { document.getElementById('whisparrSection').classList.add('active'); document.getElementById('whisparrSection').style.display = 'block'; if (document.getElementById('appsWhisparrNav')) document.getElementById('appsWhisparrNav').classList.add('active'); newTitle = 'Whisparr V2'; this.currentSection = 'whisparr'; // Switch to Apps sidebar this.showAppsSidebar(); // Initialize app module for whisparr if (typeof appsModule !== 'undefined') { appsModule.init('whisparr'); } } else if (section === 'eros' && document.getElementById('erosSection')) { document.getElementById('erosSection').classList.add('active'); document.getElementById('erosSection').style.display = 'block'; if (document.getElementById('appsErosNav')) document.getElementById('appsErosNav').classList.add('active'); newTitle = 'Whisparr V3'; this.currentSection = 'eros'; // Switch to Apps sidebar this.showAppsSidebar(); // Initialize app module for eros if (typeof appsModule !== 'undefined') { appsModule.init('eros'); } } else if (section === 'swaparr' && document.getElementById('swaparrSection')) { document.getElementById('swaparrSection').classList.add('active'); document.getElementById('swaparrSection').style.display = 'block'; if (document.getElementById('swaparrNav')) document.getElementById('swaparrNav').classList.add('active'); newTitle = 'Swaparr'; this.currentSection = 'swaparr'; // Show main sidebar for main sections and clear settings sidebar preference localStorage.removeItem('huntarr-settings-sidebar'); this.showMainSidebar(); // Initialize Swaparr section this.initializeSwaparr(); } else if (section === 'settings' && document.getElementById('settingsSection')) { console.log('[huntarrUI] Switching to settings section'); document.getElementById('settingsSection').classList.add('active'); document.getElementById('settingsSection').style.display = 'block'; if (document.getElementById('settingsNav')) document.getElementById('settingsNav').classList.add('active'); newTitle = 'Settings'; this.currentSection = 'settings'; // Switch to Settings sidebar console.log('[huntarrUI] About to call showSettingsSidebar()'); this.showSettingsSidebar(); console.log('[huntarrUI] Called showSettingsSidebar()'); // Set localStorage to maintain Settings sidebar preference localStorage.setItem('huntarr-settings-sidebar', 'true'); // Initialize settings if not already done this.initializeSettings(); } else if (section === 'scheduling' && document.getElementById('schedulingSection')) { document.getElementById('schedulingSection').classList.add('active'); document.getElementById('schedulingSection').style.display = 'block'; if (document.getElementById('schedulingNav')) document.getElementById('schedulingNav').classList.add('active'); newTitle = 'Scheduling'; this.currentSection = 'scheduling'; // Switch to Settings sidebar for scheduling this.showSettingsSidebar(); // Set localStorage to maintain Settings sidebar preference localStorage.setItem('huntarr-settings-sidebar', 'true'); } else if (section === 'notifications' && document.getElementById('notificationsSection')) { document.getElementById('notificationsSection').classList.add('active'); document.getElementById('notificationsSection').style.display = 'block'; if (document.getElementById('settingsNotificationsNav')) document.getElementById('settingsNotificationsNav').classList.add('active'); newTitle = 'Notifications'; this.currentSection = 'notifications'; // Switch to Settings sidebar for notifications this.showSettingsSidebar(); // Set localStorage to maintain Settings sidebar preference localStorage.setItem('huntarr-settings-sidebar', 'true'); // Initialize notifications settings if not already done this.initializeNotifications(); } else if (section === 'backup-restore' && document.getElementById('backupRestoreSection')) { document.getElementById('backupRestoreSection').classList.add('active'); document.getElementById('backupRestoreSection').style.display = 'block'; if (document.getElementById('settingsBackupRestoreNav')) document.getElementById('settingsBackupRestoreNav').classList.add('active'); newTitle = 'Backup / Restore'; this.currentSection = 'backup-restore'; // Switch to Settings sidebar for backup/restore this.showSettingsSidebar(); // Set localStorage to maintain Settings sidebar preference localStorage.setItem('huntarr-settings-sidebar', 'true'); // Initialize backup/restore functionality if not already done this.initializeBackupRestore(); } else if (section === 'prowlarr' && document.getElementById('prowlarrSection')) { document.getElementById('prowlarrSection').classList.add('active'); document.getElementById('prowlarrSection').style.display = 'block'; if (document.getElementById('appsProwlarrNav')) document.getElementById('appsProwlarrNav').classList.add('active'); newTitle = 'Prowlarr'; this.currentSection = 'prowlarr'; // Switch to Apps sidebar for prowlarr this.showAppsSidebar(); // Initialize prowlarr settings if not already done this.initializeProwlarr(); } else if (section === 'user' && document.getElementById('userSection')) { document.getElementById('userSection').classList.add('active'); document.getElementById('userSection').style.display = 'block'; if (document.getElementById('userNav')) document.getElementById('userNav').classList.add('active'); newTitle = 'User'; this.currentSection = 'user'; // Switch to Settings sidebar for user this.showSettingsSidebar(); // Set localStorage to maintain Settings sidebar preference localStorage.setItem('huntarr-settings-sidebar', 'true'); // Initialize user module if not already done this.initializeUser(); } else { // Default to home if section is unknown or element missing if (this.elements.homeSection) { this.elements.homeSection.classList.add('active'); this.elements.homeSection.style.display = 'block'; } if (this.elements.homeNav) this.elements.homeNav.classList.add('active'); newTitle = 'Home'; this.currentSection = 'home'; // Show main sidebar and clear settings sidebar preference localStorage.removeItem('huntarr-settings-sidebar'); this.showMainSidebar(); } // Disconnect logs when switching away from logs section if (this.currentSection !== 'logs' && window.LogsModule) { window.LogsModule.disconnectAllEventSources(); } // Update the page title const pageTitleElement = document.getElementById('currentPageTitle'); if (pageTitleElement) { pageTitleElement.textContent = newTitle; } else { console.warn("[huntarrUI] currentPageTitle element not found during section switch."); } }, // Sidebar switching functions showMainSidebar: function() { document.getElementById('sidebar').style.display = 'flex'; document.getElementById('apps-sidebar').style.display = 'none'; document.getElementById('settings-sidebar').style.display = 'none'; document.getElementById('requestarr-sidebar').style.display = 'none'; }, showAppsSidebar: function() { document.getElementById('sidebar').style.display = 'none'; document.getElementById('apps-sidebar').style.display = 'flex'; document.getElementById('settings-sidebar').style.display = 'none'; document.getElementById('requestarr-sidebar').style.display = 'none'; }, showSettingsSidebar: function() { document.getElementById('sidebar').style.display = 'none'; document.getElementById('apps-sidebar').style.display = 'none'; document.getElementById('settings-sidebar').style.display = 'flex'; document.getElementById('requestarr-sidebar').style.display = 'none'; }, showRequestarrSidebar: function() { document.getElementById('sidebar').style.display = 'none'; document.getElementById('apps-sidebar').style.display = 'none'; document.getElementById('settings-sidebar').style.display = 'none'; document.getElementById('requestarr-sidebar').style.display = 'flex'; }, // Simple event source disconnection for compatibility disconnectAllEventSources: function() { // Delegate to LogsModule if it exists if (window.LogsModule && typeof window.LogsModule.disconnectAllEventSources === 'function') { window.LogsModule.disconnectAllEventSources(); } // Clear local references this.eventSources = {}; }, // App tab switching handleAppTabChange: function(e) { const app = e.target.getAttribute('data-app'); if (!app) return; // Update active tab this.elements.appTabs.forEach(tab => { tab.classList.remove('active'); }); e.target.classList.add('active'); // Let LogsModule handle app switching to preserve pagination this.currentApp = app; if (window.LogsModule && typeof window.LogsModule.handleAppChange === 'function') { window.LogsModule.handleAppChange(app); } }, // Log option dropdown handling - Delegated to LogsModule // (Removed to prevent conflicts with LogsModule.handleLogOptionChange) // History option dropdown handling handleHistoryOptionChange: function(app) { if (app && app.target && typeof app.target.value === 'string') { app = app.target.value; } else if (app && app.target && typeof app.target.getAttribute === 'function') { app = app.target.getAttribute('data-app'); } if (!app || app === this.currentHistoryApp) return; // Update the select value const historyAppSelect = document.getElementById('historyAppSelect'); if (historyAppSelect) historyAppSelect.value = app; // Update the current history app text with proper capitalization let displayName = app.charAt(0).toUpperCase() + app.slice(1); if (app === 'whisparr') displayName = 'Whisparr V2'; else if (app === 'eros') displayName = 'Whisparr V3'; if (this.elements.currentHistoryApp) this.elements.currentHistoryApp.textContent = displayName; // Update the placeholder text this.updateHistoryPlaceholder(app); // Switch to the selected app history this.currentHistoryApp = app; }, // Update the history placeholder text based on the selected app updateHistoryPlaceholder: function(app) { if (!this.elements.historyPlaceholderText) return; let message = ""; if (app === 'all') { message = "The History feature will be available in a future update. Stay tuned for enhancements that will allow you to view your media processing history."; } else { let displayName = this.capitalizeFirst(app); message = `The ${displayName} History feature is under development and will be available in a future update. You'll be able to track your ${displayName} media processing history here.`; } this.elements.historyPlaceholderText.textContent = message; }, // Settings option handling handleSettingsOptionChange: function(e) { e.preventDefault(); // Prevent default anchor behavior const app = e.target.getAttribute('data-app'); if (!app || app === this.currentSettingsApp) return; // Do nothing if same tab clicked // Update active option this.elements.settingsOptions.forEach(option => { option.classList.remove('active'); }); e.target.classList.add('active'); // Update the current settings app text with proper capitalization let displayName = app.charAt(0).toUpperCase() + app.slice(1); this.elements.currentSettingsApp.textContent = displayName; // Close the dropdown this.elements.settingsDropdownContent.classList.remove('show'); // Hide all settings panels this.elements.appSettingsPanels.forEach(panel => { panel.classList.remove('active'); panel.style.display = 'none'; }); // Show the selected app's settings panel const selectedPanel = document.getElementById(app + 'Settings'); if (selectedPanel) { selectedPanel.classList.add('active'); selectedPanel.style.display = 'block'; } this.currentSettingsTab = app; console.log(`[huntarrUI] Switched settings tab to: ${this.currentSettingsTab}`); // Added logging }, // Compatibility methods that delegate to LogsModule connectToLogs: function() { if (window.LogsModule && typeof window.LogsModule.connectToLogs === 'function') { window.LogsModule.connectToLogs(); } }, clearLogs: function() { if (window.LogsModule && typeof window.LogsModule.clearLogs === 'function') { window.LogsModule.clearLogs(); } }, // Insert log entry in chronological order to maintain proper reverse time sorting insertLogInChronologicalOrder: function(newLogEntry) { if (!this.elements.logsContainer || !newLogEntry) return; // Parse timestamp from the new log entry const newTimestamp = this.parseLogTimestamp(newLogEntry); // If we can't parse the timestamp, just append to the end if (!newTimestamp) { this.elements.logsContainer.appendChild(newLogEntry); return; } // Get all existing log entries const existingEntries = Array.from(this.elements.logsContainer.children); // If no existing entries, just add the new one if (existingEntries.length === 0) { this.elements.logsContainer.appendChild(newLogEntry); return; } // Find the correct position to insert (maintaining chronological order) // Since CSS will reverse the order, we want older entries first in DOM let insertPosition = null; for (let i = 0; i < existingEntries.length; i++) { const existingTimestamp = this.parseLogTimestamp(existingEntries[i]); // If we can't parse existing timestamp, skip it if (!existingTimestamp) continue; // If new log is newer than existing log, insert before it if (newTimestamp > existingTimestamp) { insertPosition = existingEntries[i]; break; } } // Insert in the correct position if (insertPosition) { this.elements.logsContainer.insertBefore(newLogEntry, insertPosition); } else { // If no position found, append to the end (oldest) this.elements.logsContainer.appendChild(newLogEntry); } }, // Parse timestamp from log entry DOM element parseLogTimestamp: function(logEntry) { if (!logEntry) return null; try { // Look for timestamp elements const dateSpan = logEntry.querySelector('.log-timestamp .date'); const timeSpan = logEntry.querySelector('.log-timestamp .time'); if (!dateSpan || !timeSpan) return null; const dateText = dateSpan.textContent.trim(); const timeText = timeSpan.textContent.trim(); // Skip invalid timestamps if (!dateText || !timeText || dateText === '--' || timeText === '--:--:--') { return null; } // Combine date and time into a proper timestamp const timestampString = `${dateText} ${timeText}`; const timestamp = new Date(timestampString); // Return timestamp if valid, null otherwise return isNaN(timestamp.getTime()) ? null : timestamp; } catch (error) { console.warn('[huntarrUI] Error parsing log timestamp:', error); return null; } }, // Search logs functionality with performance optimization searchLogs: function() { if (!this.elements.logsContainer || !this.elements.logSearchInput) return; const searchText = this.elements.logSearchInput.value.trim().toLowerCase(); // If empty search, reset everything if (!searchText) { this.clearLogSearch(); return; } // Show clear search button when searching if (this.elements.clearSearchButton) { this.elements.clearSearchButton.style.display = 'block'; } // Filter log entries based on search text - with performance optimization const logEntries = Array.from(this.elements.logsContainer.querySelectorAll('.log-entry')); let matchCount = 0; // Set a limit for highlighting to prevent browser lockup const MAX_ENTRIES_TO_PROCESS = 300; const processedLogEntries = logEntries.slice(0, MAX_ENTRIES_TO_PROCESS); const remainingCount = Math.max(0, logEntries.length - MAX_ENTRIES_TO_PROCESS); // Process in batches to prevent UI lockup processedLogEntries.forEach((entry, index) => { const entryText = entry.textContent.toLowerCase(); // Show/hide based on search match if (entryText.includes(searchText)) { entry.style.display = ''; matchCount++; // Simple highlight by replacing HTML - much more performant this.simpleHighlightMatch(entry, searchText); } else { entry.style.display = 'none'; } }); // Handle any remaining entries - only for visibility, don't highlight if (remainingCount > 0) { logEntries.slice(MAX_ENTRIES_TO_PROCESS).forEach(entry => { const entryText = entry.textContent.toLowerCase(); if (entryText.includes(searchText)) { entry.style.display = ''; matchCount++; } else { entry.style.display = 'none'; } }); } // Update search results info if (this.elements.logSearchResults) { let resultsText = `Found ${matchCount} matching log entries`; this.elements.logSearchResults.textContent = resultsText; this.elements.logSearchResults.style.display = 'block'; } // Disable auto-scroll when searching if (this.elements.autoScrollCheckbox && this.elements.autoScrollCheckbox.checked) { // Save auto-scroll state to restore later if needed this.autoScrollWasEnabled = true; this.elements.autoScrollCheckbox.checked = false; } }, // New simplified highlighting method that's much more performant simpleHighlightMatch: function(logEntry, searchText) { // Only proceed if the search text is meaningful if (searchText.length < 2) return; // Store original HTML if not already stored if (!logEntry.hasAttribute('data-original-html')) { logEntry.setAttribute('data-original-html', logEntry.innerHTML); } const html = logEntry.getAttribute('data-original-html'); const escapedSearchText = searchText.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // Escape regex special chars // Simple case-insensitive replace with highlight span (using a more efficient regex approach) const regex = new RegExp(`(${escapedSearchText})`, 'gi'); const newHtml = html.replace(regex, '$1'); logEntry.innerHTML = newHtml; }, // Clear log search and reset to default view clearLogSearch: function() { if (!this.elements.logsContainer) return; // Clear search input if (this.elements.logSearchInput) { this.elements.logSearchInput.value = ''; } // Hide clear search button if (this.elements.clearSearchButton) { this.elements.clearSearchButton.style.display = 'none'; } // Hide search results info if (this.elements.logSearchResults) { this.elements.logSearchResults.style.display = 'none'; } // Show all log entries - use a more efficient approach const allLogEntries = this.elements.logsContainer.querySelectorAll('.log-entry'); // Process in batches for better performance Array.from(allLogEntries).forEach(entry => { // Display all entries entry.style.display = ''; // Restore original HTML if it exists if (entry.hasAttribute('data-original-html')) { entry.innerHTML = entry.getAttribute('data-original-html'); } }); // Restore auto-scroll if it was enabled if (this.autoScrollWasEnabled && this.elements.autoScrollCheckbox) { this.elements.autoScrollCheckbox.checked = true; this.autoScrollWasEnabled = false; } }, // Settings handling loadAllSettings: function() { // Disable save button until changes are made this.updateSaveResetButtonState(false); this.settingsChanged = false; // Get all settings to populate forms HuntarrUtils.fetchWithTimeout('./api/settings') .then(response => response.json()) .then(data => { console.log('Loaded settings:', data); // Store original settings for comparison this.originalSettings = data; // Cache settings in localStorage for timezone access try { localStorage.setItem('huntarr-settings-cache', JSON.stringify(data)); } catch (e) { console.warn('[huntarrUI] Failed to cache settings in localStorage:', e); } // Populate each app's settings form if (data.sonarr) this.populateSettingsForm('sonarr', data.sonarr); if (data.radarr) this.populateSettingsForm('radarr', data.radarr); if (data.lidarr) this.populateSettingsForm('lidarr', data.lidarr); if (data.readarr) this.populateSettingsForm('readarr', data.readarr); if (data.whisparr) this.populateSettingsForm('whisparr', data.whisparr); if (data.eros) this.populateSettingsForm('eros', data.eros); if (data.swaparr) { // Cache Swaparr settings globally for instance visibility logic window.swaparrSettings = data.swaparr; this.populateSettingsForm('swaparr', data.swaparr); } if (data.prowlarr) this.populateSettingsForm('prowlarr', data.prowlarr); if (data.general) this.populateSettingsForm('general', data.general); // Update duration displays (like sleep durations) if (typeof SettingsForms !== 'undefined' && typeof SettingsForms.updateDurationDisplay === 'function') { SettingsForms.updateDurationDisplay(); } // Update Swaparr instance visibility based on global setting if (typeof SettingsForms !== 'undefined' && typeof SettingsForms.updateAllSwaparrInstanceVisibility === 'function') { SettingsForms.updateAllSwaparrInstanceVisibility(); } // Load stateful info immediately, don't wait for loadAllSettings to complete this.loadStatefulInfo(); }) .catch(error => { console.error('Error loading settings:', error); this.showNotification('Error loading settings. Please try again.', 'error'); }); }, populateSettingsForm: function(app, appSettings) { // Cache the form for this app const form = document.getElementById(`${app}Settings`); if (!form) return; // Check if SettingsForms is loaded to generate the form if (typeof SettingsForms !== 'undefined') { const formFunction = SettingsForms[`generate${app.charAt(0).toUpperCase()}${app.slice(1)}Form`]; if (typeof formFunction === 'function') { formFunction(form, appSettings); // This function already calls setupInstanceManagement internally // Update duration displays for this app if (typeof SettingsForms.updateDurationDisplay === 'function') { try { SettingsForms.updateDurationDisplay(); } catch (e) { console.error(`[huntarrUI] Error updating duration display:`, e); } } // Update Swaparr instance visibility based on global setting if (typeof SettingsForms.updateAllSwaparrInstanceVisibility === 'function') { try { SettingsForms.updateAllSwaparrInstanceVisibility(); } catch (e) { console.error(`[huntarrUI] Error updating Swaparr instance visibility:`, e); } } } else { console.error(`[huntarrUI] Form generator function not found for app: ${app}`); } } else { console.error('[huntarrUI] SettingsForms is not defined'); return; } }, // Called when any setting input changes in the active tab markSettingsAsChanged() { if (!this.settingsChanged) { console.log("[huntarrUI] Settings marked as changed."); this.settingsChanged = true; this.updateSaveResetButtonState(true); // Enable buttons } }, saveSettings: function() { const app = this.currentSettingsTab; console.log(`[huntarrUI] saveSettings called for app: ${app}`); // Clear the unsaved changes flag BEFORE sending the request // This prevents the "unsaved changes" dialog from appearing this.settingsChanged = false; this.updateSaveResetButtonState(false); // Use getFormSettings for all apps, as it handles different structures let settings = this.getFormSettings(app); if (!settings) { console.error(`[huntarrUI] Failed to collect settings for app: ${app}`); this.showNotification('Error collecting settings from form.', 'error'); return; } console.log(`[huntarrUI] Collected settings for ${app}:`, settings); // Check if this is general settings and if the authentication mode has changed const isAuthModeChanged = app === 'general' && this.originalSettings && this.originalSettings.general && this.originalSettings.general.auth_mode !== settings.auth_mode; // Log changes to authentication settings console.log(`[huntarrUI] Authentication mode changed: ${isAuthModeChanged}`); console.log(`[huntarrUI] Sending settings payload for ${app}:`, settings); // Use the correct endpoint based on app type const endpoint = app === 'general' ? './api/settings/general' : `./api/settings/${app}`; HuntarrUtils.fetchWithTimeout(endpoint, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }) .then(response => { if (!response.ok) { // Try to get error message from response body return response.json().then(errData => { throw new Error(errData.error || `HTTP error! status: ${response.status}`); }).catch(() => { // Fallback if response body is not JSON or empty throw new Error(`HTTP error! status: ${response.status}`); }); } return response.json(); }) .then(savedConfig => { console.log('[huntarrUI] Settings saved successfully. Full config received:', savedConfig); // Only reload the page if Authentication Mode was changed if (isAuthModeChanged) { this.showNotification('Settings saved successfully. Reloading page to apply authentication changes...', 'success'); setTimeout(() => { window.location.href = './'; // Redirect to home page after a brief delay }, 1500); return; } // Settings auto-save notification removed per user request // Update original settings state with the full config returned from backend if (typeof savedConfig === 'object' && savedConfig !== null) { this.originalSettings = JSON.parse(JSON.stringify(savedConfig)); // Cache Swaparr settings globally if they were updated if (app === 'swaparr') { // Handle both nested (savedConfig.swaparr) and direct (savedConfig) formats const swaparrData = savedConfig.swaparr || (savedConfig && !savedConfig.sonarr && !savedConfig.radarr ? savedConfig : null); if (swaparrData) { window.swaparrSettings = swaparrData; console.log('[huntarrUI] Updated Swaparr settings cache:', window.swaparrSettings); } } // Check if low usage mode setting has changed and apply it immediately if (app === 'general' && 'low_usage_mode' in settings) { this.applyLowUsageMode(settings.low_usage_mode); } } else { console.error('[huntarrUI] Invalid config received from backend after save:', savedConfig); this.loadAllSettings(); return; } // Re-populate the form with the saved data const currentAppSettings = this.originalSettings[app] || {}; // Preserve instances data if missing in the response but was in our sent data if (app === 'sonarr' && !currentAppSettings.instances && settings.instances) { currentAppSettings.instances = settings.instances; } this.populateSettingsForm(app, currentAppSettings); // Update connection status and UI this.checkAppConnection(app); this.updateHomeConnectionStatus(); // If general settings were saved, refresh the stateful info display if (app === 'general') { // Update the displayed interval hours if it's available in the settings if (settings.stateful_management_hours && document.getElementById('stateful_management_hours')) { const intervalInput = document.getElementById('stateful_management_hours'); const intervalDaysSpan = document.getElementById('stateful_management_days'); const expiresDateEl = document.getElementById('stateful_expires_date'); // Update the input value intervalInput.value = settings.stateful_management_hours; // Update the days display if (intervalDaysSpan) { const days = (settings.stateful_management_hours / 24).toFixed(1); intervalDaysSpan.textContent = `${days} days`; } // Show updating indicator if (expiresDateEl) { expiresDateEl.textContent = 'Updating...'; } // Also directly update the stateful expiration on the server and update UI this.updateStatefulExpirationOnUI(); } else { this.loadStatefulInfo(); } // Dispatch a custom event that community-resources.js can listen for window.dispatchEvent(new CustomEvent('settings-saved', { detail: { appType: app, settings: settings } })); } }) .catch(error => { console.error('Error saving settings:', error); this.showNotification(`Error saving settings: ${error.message}`, 'error'); // If there was an error, mark settings as changed again this.settingsChanged = true; this.updateSaveResetButtonState(true); }); }, // Auto-save enabled - save button removed, no state to update updateSaveResetButtonState(enable) { // No-op since save button is removed for auto-save }, // Setup auto-save for settings setupSettingsAutoSave: function() { console.log('[huntarrUI] Setting up immediate settings auto-save'); // Add event listeners to the settings container const settingsContainer = document.getElementById('settingsSection'); if (settingsContainer) { // Listen for input events (for text inputs, textareas, range sliders) settingsContainer.addEventListener('input', (event) => { if (event.target.matches('input, textarea')) { this.triggerSettingsAutoSave(); } }); // Listen for change events (for checkboxes, selects, radio buttons) settingsContainer.addEventListener('change', (event) => { if (event.target.matches('input, select, textarea')) { // Special handling for settings that can take effect immediately if (event.target.id === 'low_usage_mode') { console.log('[huntarrUI] Low Usage Mode toggled, applying immediately'); this.applyLowUsageMode(event.target.checked); } else if (event.target.id === 'timezone') { console.log('[huntarrUI] Timezone changed, applying immediately'); this.applyTimezoneChange(event.target.value); } else if (event.target.id === 'auth_mode') { console.log('[huntarrUI] Authentication mode changed, applying immediately'); this.applyAuthModeChange(event.target.value); } else if (event.target.id === 'check_for_updates') { console.log('[huntarrUI] Update checking toggled, applying immediately'); this.applyUpdateCheckingChange(event.target.checked); } this.triggerSettingsAutoSave(); } }); console.log('[huntarrUI] Settings auto-save listeners added'); } }, // Trigger immediate auto-save triggerSettingsAutoSave: function() { if (window._settingsCurrentlySaving) { console.log('[huntarrUI] Settings auto-save skipped - already saving'); return; } // Determine what type of settings we're saving const app = this.currentSettingsTab; const isGeneralSettings = this.currentSection === 'settings' && !app; if (!app && !isGeneralSettings) { console.log('[huntarrUI] No current settings tab for auto-save'); return; } if (isGeneralSettings) { console.log('[huntarrUI] Triggering immediate general settings auto-save'); this.autoSaveGeneralSettings(true).catch(error => { console.error('[huntarrUI] General settings auto-save failed:', error); }); } else { console.log(`[huntarrUI] Triggering immediate settings auto-save for: ${app}`); this.autoSaveSettings(app); } }, // Auto-save settings function autoSaveSettings: function(app) { if (window._settingsCurrentlySaving) { console.log(`[huntarrUI] Auto-save for ${app} skipped - already saving`); return; } console.log(`[huntarrUI] Auto-saving settings for: ${app}`); window._settingsCurrentlySaving = true; // Use the existing saveSettings logic but make it silent const originalShowNotification = this.showNotification; // Temporarily override showNotification to suppress success messages this.showNotification = (message, type) => { if (type === 'error') { // Only show error notifications originalShowNotification.call(this, message, type); } // Suppress success notifications for auto-save }; // Call the existing saveSettings function this.saveSettings(); // Schedule restoration of showNotification after save completes setTimeout(() => { this.showNotification = originalShowNotification; window._settingsCurrentlySaving = false; }, 1000); }, // Clean URL by removing special characters from the end cleanUrlString: function(url) { if (!url) return ""; // Trim whitespace first let cleanUrl = url.trim(); // First remove any trailing slashes cleanUrl = cleanUrl.replace(/[\/\\]+$/g, ''); // Then remove any other trailing special characters // This regex will match any special character at the end that is not alphanumeric, hyphen, period, or underscore return cleanUrl.replace(/[^a-zA-Z0-9\-\._]$/g, ''); }, // Get settings from the form, updated to handle instances consistently getFormSettings: function(app) { const settings = {}; let form = document.getElementById(`${app}Settings`); // Special handling for Swaparr since it has its own section structure if (app === 'swaparr') { form = document.getElementById('swaparrContainer') || document.querySelector('.swaparr-container') || document.querySelector('[data-app-type="swaparr"]'); } if (!form) { console.error(`[huntarrUI] Settings form for ${app} not found.`); return null; } // Special handling for Swaparr settings if (app === 'swaparr') { console.log('[huntarrUI] Processing Swaparr settings'); console.log('[huntarrUI] Form:', form); // Get all inputs and select elements in the Swaparr form const swaparrInputs = form.querySelectorAll('input, select, textarea'); swaparrInputs.forEach(input => { let key = input.id; let value; // Remove 'swaparr_' prefix to get clean key name if (key.startsWith('swaparr_')) { key = key.substring(8); // Remove 'swaparr_' prefix } if (input.type === 'checkbox') { value = input.checked; } else if (input.type === 'number') { value = input.value === '' ? null : parseInt(input.value, 10); } else { value = input.value.trim(); } console.log(`[huntarrUI] Processing Swaparr input: ${key} = ${value}`); // Handle field name mappings for settings that have different names if (key === 'malicious_detection') { key = 'malicious_file_detection'; console.log(`[huntarrUI] Mapped malicious_detection -> malicious_file_detection`); } // Only include non-tag-system fields if (key && !key.includes('_tags') && !key.includes('_input')) { // Only include non-tag-system fields // Special handling for sleep_duration - convert minutes to seconds if (key === 'sleep_duration' && input.type === 'number') { settings[key] = value * 60; // Convert minutes to seconds console.log(`[huntarrUI] Converted sleep_duration from ${value} minutes to ${settings[key]} seconds`); } else { settings[key] = value; } } }); // Handle tag containers separately const tagContainers = [ { containerId: 'swaparr_malicious_extensions_tags', settingKey: 'malicious_extensions' }, { containerId: 'swaparr_suspicious_patterns_tags', settingKey: 'suspicious_patterns' }, { containerId: 'swaparr_quality_patterns_tags', settingKey: 'blocked_quality_patterns' } ]; tagContainers.forEach(({ containerId, settingKey }) => { const container = document.getElementById(containerId); if (container) { const tags = Array.from(container.querySelectorAll('.tag-text')).map(el => el.textContent); settings[settingKey] = tags; console.log(`[huntarrUI] Collected tags for ${settingKey}:`, tags); } else { console.warn(`[huntarrUI] Tag container not found: ${containerId}`); settings[settingKey] = []; } }); console.log('[huntarrUI] Final Swaparr settings:', settings); return settings; } // Special handling for general settings if (app === 'general') { console.log('[huntarrUI] Processing general settings'); console.log('[huntarrUI] Form:', form); console.log('[huntarrUI] Form HTML (first 500 chars):', form.innerHTML.substring(0, 500)); // Debug: Check if apprise_urls exists anywhere const globalAppriseElement = document.querySelector('#apprise_urls'); console.log('[huntarrUI] Global apprise_urls element:', globalAppriseElement); // Get all inputs and select elements in the general form AND notifications container const generalInputs = form.querySelectorAll('input, select, textarea'); const notificationsContainer = document.querySelector('#notificationsContainer'); const notificationInputs = notificationsContainer ? notificationsContainer.querySelectorAll('input, select, textarea') : []; // Combine inputs from both containers const allInputs = [...generalInputs, ...notificationInputs]; allInputs.forEach(input => { let key = input.id; let value; if (input.type === 'checkbox') { value = input.checked; } else if (input.type === 'number') { value = input.value === '' ? null : parseInt(input.value, 10); } else { value = input.value.trim(); } console.log(`[huntarrUI] Processing input: ${key} = ${value}`); // Handle special cases if (key === 'apprise_urls') { console.log('[huntarrUI] Processing Apprise URLs'); console.log('[huntarrUI] Raw apprise_urls value:', input.value); // Split by newline and filter empty lines settings.apprise_urls = input.value.split('\n') .map(url => url.trim()) .filter(url => url.length > 0); console.log('[huntarrUI] Processed apprise_urls:', settings.apprise_urls); } else if (key && !key.includes('_instance_')) { // Only include non-instance fields settings[key] = value; } }); console.log('[huntarrUI] Final general settings:', settings); return settings; } // Handle apps that use instances (Sonarr, Radarr, etc.) // Get all instance items in the form const instanceItems = form.querySelectorAll('.instance-item'); settings.instances = []; // Check if multi-instance UI elements exist (like Sonarr) if (instanceItems.length > 0) { console.log(`[huntarrUI] Found ${instanceItems.length} instance items for ${app}. Processing multi-instance mode.`); // Multi-instance logic (current Sonarr logic) instanceItems.forEach((item, index) => { const instanceId = item.dataset.instanceId; // Gets the data-instance-id const nameInput = form.querySelector(`#${app}-name-${instanceId}`); const urlInput = form.querySelector(`#${app}-url-${instanceId}`); const keyInput = form.querySelector(`#${app}-key-${instanceId}`); const enabledInput = form.querySelector(`#${app}-enabled-${instanceId}`); if (urlInput && keyInput) { // Need URL and Key at least settings.instances.push({ // Use nameInput value if available, otherwise generate a default name: nameInput && nameInput.value.trim() !== '' ? nameInput.value.trim() : `Instance ${index + 1}`, api_url: this.cleanUrlString(urlInput.value), api_key: keyInput.value.trim(), // Default to true if toggle doesn't exist or is checked enabled: enabledInput ? enabledInput.checked : true }); } }); } else { console.log(`[huntarrUI] No instance items found for ${app}. Processing single-instance mode.`); // Single-instance logic (for Radarr, Lidarr, etc.) // Look for the standard IDs used in their forms const nameInput = form.querySelector(`#${app}_instance_name`); // Check for a specific name field const urlInput = form.querySelector(`#${app}_api_url`); const keyInput = form.querySelector(`#${app}_api_key`); // Assuming single instances might have an enable toggle like #app_enabled const enabledInput = form.querySelector(`#${app}_enabled`); // Only add if URL and Key have values if (urlInput && urlInput.value.trim() && keyInput && keyInput.value.trim()) { settings.instances.push({ name: nameInput && nameInput.value.trim() !== '' ? nameInput.value.trim() : `${app} Instance 1`, // Default name api_url: this.cleanUrlString(urlInput.value), api_key: keyInput.value.trim(), // Default to true if toggle doesn't exist or is checked enabled: enabledInput ? enabledInput.checked : true }); } } console.log(`[huntarrUI] Processed instances for ${app}:`, settings.instances); // Now collect any OTHER settings NOT part of the instance structure const allInputs = form.querySelectorAll('input, select'); const handledInstanceFieldIds = new Set(); // Identify IDs used in instance collection to avoid double-adding them if (instanceItems.length > 0) { // Multi-instance: Iterate items again to get IDs instanceItems.forEach((item) => { const instanceId = item.dataset.instanceId; if(instanceId) { handledInstanceFieldIds.add(`${app}-name-${instanceId}`); handledInstanceFieldIds.add(`${app}-url-${instanceId}`); handledInstanceFieldIds.add(`${app}-key-${instanceId}`); handledInstanceFieldIds.add(`${app}-enabled-${instanceId}`); } }); } else { // Single-instance: Check for standard IDs if (form.querySelector(`#${app}_instance_name`)) handledInstanceFieldIds.add(`${app}_instance_name`); if (form.querySelector(`#${app}_api_url`)) handledInstanceFieldIds.add(`${app}_api_url`); if (form.querySelector(`#${app}_api_key`)) handledInstanceFieldIds.add(`${app}_api_key`); if (form.querySelector(`#${app}_enabled`)) handledInstanceFieldIds.add(`${app}_enabled`); } allInputs.forEach(input => { // Handle special case for Whisparr version if (input.id === 'whisparr_version') { if (app === 'whisparr') { settings['whisparr_version'] = input.value.trim(); return; // Skip further processing for this field } } // Skip buttons and fields already processed as part of an instance if (input.type === 'button' || handledInstanceFieldIds.has(input.id)) { return; } // Get the field key (remove app prefix) let key = input.id; if (key.startsWith(`${app}_`)) { key = key.substring(app.length + 1); } // Skip empty keys or keys that are just numbers (unlikely but possible) if (!key || /^\d+$/.test(key)) return; // Store the value if (input.type === 'checkbox') { settings[key] = input.checked; } else if (input.type === 'number') { // Handle potential empty string for numbers, store as null or default? settings[key] = input.value === '' ? null : parseInt(input.value, 10); } else { settings[key] = input.value.trim(); } }); console.log(`[huntarrUI] Final collected settings for ${app}:`, settings); return settings; }, // Test notification functionality testNotification: function() { console.log('[huntarrUI] Testing notification...'); const statusElement = document.getElementById('testNotificationStatus'); const buttonElement = document.getElementById('testNotificationBtn'); if (!statusElement || !buttonElement) { console.error('[huntarrUI] Test notification elements not found'); return; } // Disable button and show loading buttonElement.disabled = true; buttonElement.innerHTML = ' Auto-saving...'; statusElement.innerHTML = 'Auto-saving settings before testing...'; // Auto-save general settings before testing this.autoSaveGeneralSettings() .then(() => { // Update button text to show we're now testing buttonElement.innerHTML = ' Sending...'; statusElement.innerHTML = 'Sending test notification...'; // Now test with the saved settings return HuntarrUtils.fetchWithTimeout('./api/test-notification', { method: 'POST', headers: { 'Content-Type': 'application/json' } }); }) .then(response => response.json()) .then(data => { console.log('[huntarrUI] Test notification response:', data); if (data.success) { statusElement.innerHTML = '✓ Test notification sent successfully!'; this.showNotification('Test notification sent! Check your notification service.', 'success'); } else { statusElement.innerHTML = '✗ Failed to send test notification'; this.showNotification(data.error || 'Failed to send test notification', 'error'); } }) .catch(error => { console.error('[huntarrUI] Test notification error:', error); statusElement.innerHTML = '✗ Error during auto-save or testing'; this.showNotification('Error during auto-save or testing: ' + error.message, 'error'); }) .finally(() => { // Re-enable button buttonElement.disabled = false; buttonElement.innerHTML = ' Test Notification'; // Clear status after 5 seconds setTimeout(() => { if (statusElement) { statusElement.innerHTML = ''; } }, 5000); }); }, // Auto-save general settings (used by test notification and auto-save) autoSaveGeneralSettings: function(silent = false) { console.log('[huntarrUI] Auto-saving general settings...'); return new Promise((resolve, reject) => { // Find the general settings form using the correct selectors const generalForm = document.querySelector('#generalSettings') || document.querySelector('.app-settings-panel[data-app-type="general"]') || document.querySelector('#settingsSection[data-app-type="general"]') || document.querySelector('#general'); if (!generalForm) { console.error('[huntarrUI] Could not find general settings form for auto-save'); console.log('[huntarrUI] Available forms:', document.querySelectorAll('.app-settings-panel, #settingsSection, [id*="general"], [id*="General"]')); reject(new Error('Could not find general settings form')); return; } console.log('[huntarrUI] Found general form:', generalForm); // Get settings from the form using the correct app parameter let settings = {}; try { settings = this.getFormSettings('general'); console.log('[huntarrUI] Auto-save collected settings:', settings); } catch (error) { console.error('[huntarrUI] Error collecting settings for auto-save:', error); reject(error); return; } // Save the settings HuntarrUtils.fetchWithTimeout('./api/settings/general', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }) .then(response => response.json()) .then(data => { if (data.success !== false) { // API returns all settings on success, not just success:true console.log('[huntarrUI] Auto-save successful'); resolve(); } else { console.error('[huntarrUI] Auto-save failed:', data); reject(new Error(data.error || 'Failed to auto-save settings')); } }) .catch(error => { console.error('[huntarrUI] Auto-save request failed:', error); reject(error); }); }); }, // Auto-save Swaparr settings autoSaveSwaparrSettings: function(silent = false) { console.log('[huntarrUI] Auto-saving Swaparr settings...'); return new Promise((resolve, reject) => { // Find the Swaparr settings form const swaparrForm = document.querySelector('#swaparrContainer') || document.querySelector('.swaparr-container') || document.querySelector('[data-app-type="swaparr"]'); if (!swaparrForm) { console.error('[huntarrUI] Could not find Swaparr settings form for auto-save'); reject(new Error('Could not find Swaparr settings form')); return; } console.log('[huntarrUI] Found Swaparr form:', swaparrForm); // Get settings from the form using the correct app parameter let settings = {}; try { settings = this.getFormSettings('swaparr'); console.log('[huntarrUI] Auto-save collected Swaparr settings:', settings); } catch (error) { console.error('[huntarrUI] Error collecting Swaparr settings for auto-save:', error); reject(error); return; } // Save the settings HuntarrUtils.fetchWithTimeout('./api/swaparr/settings', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(settings) }) .then(response => response.json()) .then(data => { if (data.success !== false) { // API returns all settings on success, not just success:true console.log('[huntarrUI] Swaparr auto-save successful'); // Update Swaparr field visibility in all loaded app forms if (window.SettingsForms && typeof window.SettingsForms.updateSwaparrFieldsDisabledState === 'function') { console.log('[huntarrUI] Broadcasting Swaparr state change to all app forms...'); window.SettingsForms.updateSwaparrFieldsDisabledState(); } resolve(); } else { console.error('[huntarrUI] Swaparr auto-save failed:', data); reject(new Error(data.error || 'Failed to auto-save Swaparr settings')); } }) .catch(error => { console.error('[huntarrUI] Swaparr auto-save request failed:', error); reject(error); }); }); }, // Handle instance management events setupInstanceEventHandlers: function() { console.log("DEBUG: setupInstanceEventHandlers called"); // Added logging const settingsPanels = document.querySelectorAll('.app-settings-panel'); settingsPanels.forEach(panel => { console.log(`DEBUG: Adding listeners to panel '${panel.id}'`); // Added logging panel.addEventListener('addInstance', (e) => { console.log(`DEBUG: addInstance event listener fired for panel '${panel.id}'. Event detail:`, e.detail); this.addAppInstance(e.detail.appName); }); panel.addEventListener('removeInstance', (e) => { this.removeAppInstance(e.detail.appName, e.detail.instanceId); }); panel.addEventListener('testConnection', (e) => { this.testInstanceConnection(e.detail.appName, e.detail.instanceId, e.detail.url, e.detail.apiKey); }); }); }, // Add a new instance to the app addAppInstance: function(appName) { console.log(`DEBUG: addAppInstance called for app '${appName}'`); const container = document.getElementById(`${appName}Settings`); if (!container) return; // Get current settings const currentSettings = this.getFormSettings(appName); if (!currentSettings.instances) { currentSettings.instances = []; } // Limit to 9 instances if (currentSettings.instances.length >= 9) { this.showNotification('Maximum of 9 instances allowed', 'error'); return; } // Add new instance with a default name currentSettings.instances.push({ name: `Instance ${currentSettings.instances.length + 1}`, api_url: '', api_key: '', enabled: true }); // Regenerate form with new instance SettingsForms[`generate${appName.charAt(0).toUpperCase()}${appName.slice(1)}Form`](container, currentSettings); // Update controls like duration displays SettingsForms.updateDurationDisplay(); this.showNotification('New instance added', 'success'); }, // Remove an instance removeAppInstance: function(appName, instanceId) { const container = document.getElementById(`${appName}Settings`); if (!container) return; // Get current settings const currentSettings = this.getFormSettings(appName); // Remove the instance if (currentSettings.instances && instanceId >= 0 && instanceId < currentSettings.instances.length) { // Keep at least one instance if (currentSettings.instances.length > 1) { const removedName = currentSettings.instances[instanceId].name; currentSettings.instances.splice(instanceId, 1); // Regenerate form SettingsForms[`generate${appName.charAt(0).toUpperCase()}${appName.slice(1)}Form`](container, currentSettings); // Update controls like duration displays SettingsForms.updateDurationDisplay(); this.showNotification(`Instance "${removedName}" removed`, 'info'); } else { this.showNotification('Cannot remove the last instance', 'error'); } } }, // Test connection for a specific instance testInstanceConnection: function(appName, instanceId, url, apiKey) { console.log(`Testing connection for ${appName} instance ${instanceId} with URL: ${url}`); // Make sure instanceId is treated as a number instanceId = parseInt(instanceId, 10); // Find the status span where we'll display the result const statusSpan = document.getElementById(`${appName}_instance_${instanceId}_status`); if (!statusSpan) { console.error(`Status span not found for ${appName} instance ${instanceId}`); return; } // Show testing status statusSpan.textContent = 'Testing...'; statusSpan.className = 'connection-status testing'; // Validate URL and API key if (!url || !apiKey) { statusSpan.textContent = 'Missing URL or API key'; statusSpan.className = 'connection-status error'; return; } // Check if URL is properly formatted if (!url.startsWith('http://') && !url.startsWith('https://')) { statusSpan.textContent = 'URL must start with http:// or https://'; statusSpan.className = 'connection-status error'; return; } // Clean the URL (remove special characters from the end) url = this.cleanUrlString(url); // Make the API request to test the connection HuntarrUtils.fetchWithTimeout(`./api/${appName}/test-connection`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ api_url: url, api_key: apiKey }) }) .then(response => { if (!response.ok) { return response.json().then(errorData => { throw new Error(errorData.message || this.getConnectionErrorMessage(response.status)); }).catch(() => { // Fallback if response body is not JSON or empty throw new Error(this.getConnectionErrorMessage(response.status)); }); } return response.json(); }) .then(data => { console.log(`Connection test response data for ${appName} instance ${instanceId}:`, data); if (data.success) { statusSpan.textContent = data.message || 'Connected'; statusSpan.className = 'connection-status success'; // If a version was returned, display it if (data.version) { statusSpan.textContent += ` (v${data.version})`; } } else { statusSpan.textContent = data.message || 'Failed'; statusSpan.className = 'connection-status error'; } }) .catch(error => { console.error(`Error testing connection for ${appName} instance ${instanceId}:`, error); // Extract the most relevant part of the error message let errorMessage = error.message || 'Unknown error'; if (errorMessage.includes('Name or service not known')) { errorMessage = 'Unable to resolve hostname. Check the URL.'; } else if (errorMessage.includes('Connection refused')) { errorMessage = 'Connection refused. Check that the service is running.'; } else if (errorMessage.includes('connect ETIMEDOUT') || errorMessage.includes('timeout')) { errorMessage = 'Connection timed out. Check URL and port.'; } else if (errorMessage.includes('401') || errorMessage.includes('Authentication failed')) { errorMessage = 'Invalid API key'; } else if (errorMessage.includes('404') || errorMessage.includes('not found')) { errorMessage = 'URL endpoint not found. Check the URL.'; } else if (errorMessage.startsWith('HTTP error!')) { errorMessage = 'Connection failed. Check URL and port.'; } statusSpan.textContent = errorMessage; statusSpan.className = 'connection-status error'; }); }, // Helper function to translate HTTP error codes to user-friendly messages getConnectionErrorMessage: function(status) { switch(status) { case 400: return 'Invalid request. Check URL format.'; case 401: return 'Invalid API key'; case 403: return 'Access forbidden. Check permissions.'; case 404: return 'Service not found at this URL. Check address.'; case 500: return 'Server error. Check if the service is working properly.'; case 502: return 'Bad gateway. Check network connectivity.'; case 503: return 'Service unavailable. Check if the service is running.'; case 504: return 'Gateway timeout. Check network connectivity.'; default: return `Connection error. Check URL and port.`; } }, // App connections checkAppConnections: function() { this.checkAppConnection('sonarr'); this.checkAppConnection('radarr'); this.checkAppConnection('lidarr'); this.checkAppConnection('readarr'); // Added readarr this.checkAppConnection('whisparr'); // Added whisparr this.checkAppConnection('eros'); // Enable actual Eros API check }, checkAppConnection: function(app) { HuntarrUtils.fetchWithTimeout(`./api/status/${app}`) .then(response => response.json()) .then(data => { // Pass the whole data object for all apps this.updateConnectionStatus(app, data); // Still update the configuredApps flag for potential other uses, but after updating status this.configuredApps[app] = data.configured === true; // Ensure it's a boolean }) .catch(error => { console.error(`Error checking ${app} connection:`, error); // Pass a default 'not configured' status object on error this.updateConnectionStatus(app, { configured: false, connected: false }); }); }, updateConnectionStatus: function(app, statusData) { const statusElement = this.elements[`${app}HomeStatus`]; if (!statusElement) return; let isConfigured = false; let isConnected = false; // Try to determine configured and connected status from statusData object // Default to false if properties are missing isConfigured = statusData?.configured === true; isConnected = statusData?.connected === true; // Special handling for *arr apps' multi-instance connected count let connectedCount = statusData?.connected_count ?? 0; let totalConfigured = statusData?.total_configured ?? 0; // For all *arr apps, 'isConfigured' means at least one instance is configured if (['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr'].includes(app)) { isConfigured = totalConfigured > 0; // For *arr apps, 'isConnected' means at least one instance is connected isConnected = isConfigured && connectedCount > 0; } // --- Visibility Logic --- if (isConfigured) { // Ensure the box is visible if (this.elements[`${app}HomeStatus`].closest('.app-stats-card')) { this.elements[`${app}HomeStatus`].closest('.app-stats-card').style.display = ''; } } else { // Not configured - HIDE the box if (this.elements[`${app}HomeStatus`].closest('.app-stats-card')) { this.elements[`${app}HomeStatus`].closest('.app-stats-card').style.display = 'none'; } // Update badge even if hidden (optional, but good practice) statusElement.className = 'status-badge not-configured'; statusElement.innerHTML = ' Not Configured'; return; // No need to update badge further if not configured } // --- Badge Update Logic (only runs if configured) --- if (['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr'].includes(app)) { // *Arr specific badge text (already checked isConfigured) statusElement.innerHTML = ` Connected ${connectedCount}/${totalConfigured}`; statusElement.className = 'status-badge ' + (isConnected ? 'connected' : 'error'); } else { // Standard badge update for other configured apps if (isConnected) { statusElement.className = 'status-badge connected'; statusElement.innerHTML = ' Connected'; } else { statusElement.className = 'status-badge not-connected'; statusElement.innerHTML = ' Not Connected'; } } }, // Load and update Swaparr status card loadSwaparrStatus: function() { HuntarrUtils.fetchWithTimeout('./api/swaparr/status') .then(response => response.json()) .then(data => { const swaparrCard = document.getElementById('swaparrStatusCard'); if (!swaparrCard) return; // Show/hide card based on whether Swaparr is enabled if (data.enabled && data.configured) { swaparrCard.style.display = 'block'; // Update persistent statistics with large number formatting (like other apps) const persistentStats = data.persistent_statistics || {}; document.getElementById('swaparr-processed').textContent = this.formatLargeNumber(persistentStats.processed || 0); document.getElementById('swaparr-strikes').textContent = this.formatLargeNumber(persistentStats.strikes || 0); document.getElementById('swaparr-removals').textContent = this.formatLargeNumber(persistentStats.removals || 0); document.getElementById('swaparr-ignored').textContent = this.formatLargeNumber(persistentStats.ignored || 0); // Setup button event handlers after content is loaded setTimeout(() => { this.setupSwaparrResetCycle(); }, 100); } else { swaparrCard.style.display = 'none'; } }) .catch(error => { console.error('Error loading Swaparr status:', error); const swaparrCard = document.getElementById('swaparrStatusCard'); if (swaparrCard) { swaparrCard.style.display = 'none'; } }); }, // Setup Swaparr Reset buttons setupSwaparrResetCycle: function() { // Handle header reset data button (like Live Hunts Executed) const resetDataButton = document.getElementById('reset-swaparr-data'); if (resetDataButton) { resetDataButton.addEventListener('click', () => { this.resetSwaparrData(); }); } // Note: Inline reset cycle button is now handled automatically by CycleCountdown system // via the cycle-reset-button class and data-app="swaparr" attribute }, // Reset Swaparr data function resetSwaparrData: function() { // Prevent multiple executions if (this.swaparrResetInProgress) { return; } // Show confirmation if (!confirm('Are you sure you want to reset all Swaparr data? This will clear all strike counts and removed items data.')) { return; } this.swaparrResetInProgress = true; // Immediately update the UI first to provide immediate feedback (like Live Hunts) this.updateSwaparrStatsDisplay({ processed: 0, strikes: 0, removals: 0, ignored: 0 }); // Show success notification immediately this.showNotification('Swaparr statistics reset successfully', 'success'); // Try to send the reset to the server, but don't depend on it for UI feedback try { HuntarrUtils.fetchWithTimeout('./api/swaparr/reset-stats', { method: 'POST' }) .then(response => { // Just log the response, don't rely on it for UI feedback if (!response.ok) { console.warn('Server responded with non-OK status for Swaparr stats reset'); } return response.json().catch(() => ({})); }) .then(data => { console.log('Swaparr stats reset response:', data); }) .catch(error => { console.warn('Error communicating with server for Swaparr stats reset:', error); }) .finally(() => { // Reset the flag after a delay setTimeout(() => { this.swaparrResetInProgress = false; }, 1000); }); } catch (error) { console.warn('Error in Swaparr stats reset:', error); this.swaparrResetInProgress = false; } }, // Update Swaparr stats display with animation (like Live Hunts) updateSwaparrStatsDisplay: function(stats) { const elements = { 'processed': document.getElementById('swaparr-processed'), 'strikes': document.getElementById('swaparr-strikes'), 'removals': document.getElementById('swaparr-removals'), 'ignored': document.getElementById('swaparr-ignored') }; for (const [key, element] of Object.entries(elements)) { if (element && stats.hasOwnProperty(key)) { const currentValue = this.parseFormattedNumber(element.textContent); const targetValue = stats[key]; if (currentValue !== targetValue) { // Animate the number change this.animateNumber(element, currentValue, targetValue, 500); } } } }, // Setup Swaparr status polling setupSwaparrStatusPolling: function() { // Load initial status this.loadSwaparrStatus(); // Set up polling to refresh Swaparr status every 30 seconds // Only poll when home section is active to reduce unnecessary requests setInterval(() => { if (this.currentSection === 'home') { this.loadSwaparrStatus(); } }, 30000); }, // Setup Prowlarr status polling setupProwlarrStatusPolling: function() { // Load initial status this.loadProwlarrStatus(); // Set up polling to refresh Prowlarr status every 30 seconds // Only poll when home section is active to reduce unnecessary requests setInterval(() => { if (this.currentSection === 'home') { this.loadProwlarrStatus(); } }, 30000); }, // Load and update Prowlarr status card loadProwlarrStatus: function() { const prowlarrCard = document.getElementById('prowlarrStatusCard'); if (!prowlarrCard) return; // First check if Prowlarr is configured and enabled HuntarrUtils.fetchWithTimeout('./api/prowlarr/status') .then(response => response.json()) .then(statusData => { // Only show card if Prowlarr is configured and enabled if (statusData.configured && statusData.enabled) { prowlarrCard.style.display = 'block'; // Update connection status const statusElement = document.getElementById('prowlarrConnectionStatus'); if (statusElement) { if (statusData.connected) { statusElement.textContent = '🟢 Connected'; statusElement.className = 'status-badge connected'; } else { statusElement.textContent = '🔴 Disconnected'; statusElement.className = 'status-badge error'; } } // Load data if connected if (statusData.connected) { // Load indexers quickly first this.loadProwlarrIndexers(); // Load statistics separately (cached) this.loadProwlarrStats(); // Set up periodic refresh for statistics (every 5 minutes) if (!this.prowlarrStatsInterval) { this.prowlarrStatsInterval = setInterval(() => { this.loadProwlarrStats(); }, 5 * 60 * 1000); // 5 minutes } } else { // Show disconnected state this.updateIndexersList(null, 'Prowlarr is disconnected'); this.updateProwlarrStatistics(null, 'Prowlarr is disconnected'); // Clear interval if disconnected if (this.prowlarrStatsInterval) { clearInterval(this.prowlarrStatsInterval); this.prowlarrStatsInterval = null; } } } else { // Hide card if not configured or disabled prowlarrCard.style.display = 'none'; console.log('[huntarrUI] Prowlarr card hidden - configured:', statusData.configured, 'enabled:', statusData.enabled); } }) .catch(error => { console.error('Error loading Prowlarr status:', error); // Hide card on error prowlarrCard.style.display = 'none'; }); }, // Load detailed Prowlarr statistics loadProwlarrStats: function() { HuntarrUtils.fetchWithTimeout('./api/prowlarr/stats') .then(response => response.json()) .then(data => { if (data.success) { this.updateProwlarrStatsDisplay(data.stats); } else { console.error('Failed to load Prowlarr stats:', data.error); this.updateProwlarrStatsDisplay({ active_indexers: '--', total_api_calls: '--', throttled_indexers: '--', failed_indexers: '--', health_status: 'Error loading stats' }); } }) .catch(error => { console.error('Error loading Prowlarr stats:', error); this.updateProwlarrStatsDisplay({ active_indexers: '--', total_api_calls: '--', throttled_indexers: '--', failed_indexers: '--', health_status: 'Connection error' }); }); }, // Update Prowlarr stats display updateProwlarrStatsDisplay: function(stats) { // Update stat numbers const activeElement = document.getElementById('prowlarr-active-indexers'); if (activeElement) activeElement.textContent = stats.active_indexers; const callsElement = document.getElementById('prowlarr-total-calls'); if (callsElement) callsElement.textContent = this.formatLargeNumber(stats.total_api_calls); const throttledElement = document.getElementById('prowlarr-throttled'); if (throttledElement) throttledElement.textContent = stats.throttled_indexers; const failedElement = document.getElementById('prowlarr-failed'); if (failedElement) failedElement.textContent = stats.failed_indexers; // Update health status const healthElement = document.getElementById('prowlarr-health-status'); if (healthElement) { healthElement.textContent = stats.health_status || 'Unknown'; // Add color coding based on health if (stats.health_status && stats.health_status.includes('throttled')) { healthElement.style.color = '#f59e0b'; // amber } else if (stats.health_status && (stats.health_status.includes('failed') || stats.health_status.includes('disabled'))) { healthElement.style.color = '#ef4444'; // red } else if (stats.health_status && stats.health_status.includes('healthy')) { healthElement.style.color = '#10b981'; // green } else { healthElement.style.color = '#9ca3af'; // gray } } }, // Load Prowlarr indexers quickly loadProwlarrIndexers: function() { HuntarrUtils.fetchWithTimeout('./api/prowlarr/indexers') .then(response => response.json()) .then(data => { if (data.success && data.indexer_details) { this.updateIndexersList(data.indexer_details); } else { console.error('Failed to load Prowlarr indexers:', data.error); this.updateIndexersList(null, data.error || 'Failed to load indexers'); } }) .catch(error => { console.error('Error loading Prowlarr indexers:', error); this.updateIndexersList(null, 'Connection error'); }); }, // Load Prowlarr statistics (cached) loadProwlarrStats: function() { HuntarrUtils.fetchWithTimeout('./api/prowlarr/stats') .then(response => response.json()) .then(data => { if (data.success && data.stats) { this.updateProwlarrStatistics(data.stats); } else { console.error('Failed to load Prowlarr stats:', data.error); this.updateProwlarrStatistics(null, data.error || 'Failed to load stats'); } }) .catch(error => { console.error('Error loading Prowlarr stats:', error); this.updateProwlarrStatistics(null, 'Connection error'); }); }, // Update indexers list display updateIndexersList: function(indexerDetails, errorMessage = null) { const indexersList = document.getElementById('prowlarr-indexers-list'); if (!indexersList) return; if (errorMessage) { // Show error state indexersList.innerHTML = `
Error: Settings forms not loaded
'; } }) .catch(error => { console.error('[huntarrUI] Error loading settings:', error); generalSettings.innerHTML = 'Error loading settings
'; }); }, initializeNotifications: function() { console.log('[huntarrUI] initializeNotifications called'); // Check if notifications are already initialized const notificationsContainer = document.getElementById('notificationsContainer'); if (!notificationsContainer) { console.error('[huntarrUI] notificationsContainer element not found!'); return; } console.log('[huntarrUI] notificationsContainer found:', notificationsContainer); console.log('[huntarrUI] Current container content:', notificationsContainer.innerHTML.trim()); // Check if notifications are actually initialized (ignore HTML comments) const currentContent = notificationsContainer.innerHTML.trim(); if (currentContent !== '' && !currentContent.includes('')) { console.log('[huntarrUI] Notifications already initialized, skipping'); return; // Already initialized } console.log('[huntarrUI] Loading notifications settings from API...'); // Load settings from API and generate the notifications form fetch('./api/settings') .then(response => response.json()) .then(settings => { console.log('[huntarrUI] Loaded settings for notifications:', settings); console.log('[huntarrUI] General settings:', settings.general); console.log('[huntarrUI] SettingsForms available:', typeof SettingsForms !== 'undefined'); console.log('[huntarrUI] generateNotificationsForm available:', typeof SettingsForms !== 'undefined' && SettingsForms.generateNotificationsForm); // Generate the notifications form - pass the general settings which contain notification settings if (typeof SettingsForms !== 'undefined' && SettingsForms.generateNotificationsForm) { console.log('[huntarrUI] Calling SettingsForms.generateNotificationsForm...'); SettingsForms.generateNotificationsForm(notificationsContainer, settings.general || {}); console.log('[huntarrUI] Notifications form generated successfully'); } else { console.error('[huntarrUI] SettingsForms.generateNotificationsForm not available'); notificationsContainer.innerHTML = 'Error: Notifications forms not loaded
'; } }) .catch(error => { console.error('[huntarrUI] Error loading notifications settings:', error); notificationsContainer.innerHTML = 'Error loading notifications settings
'; }); }, initializeBackupRestore: function() { console.log('[huntarrUI] initializeBackupRestore called'); // Initialize backup/restore functionality if (typeof BackupRestore !== 'undefined') { BackupRestore.initialize(); } else { console.error('[huntarrUI] BackupRestore module not loaded'); } }, initializeProwlarr: function() { console.log('[huntarrUI] initializeProwlarr called'); // Check if prowlarr is already initialized const prowlarrContainer = document.getElementById('prowlarrContainer'); if (!prowlarrContainer) { console.error('[huntarrUI] prowlarrContainer element not found!'); return; } console.log('[huntarrUI] prowlarrContainer found:', prowlarrContainer); console.log('[huntarrUI] Current container content:', prowlarrContainer.innerHTML.trim()); // Check if prowlarr is actually initialized (ignore HTML comments) const currentContent = prowlarrContainer.innerHTML.trim(); if (currentContent !== '' && !currentContent.includes('')) { console.log('[huntarrUI] Prowlarr already initialized, skipping'); return; // Already initialized } console.log('[huntarrUI] Loading prowlarr settings from API...'); // Load settings from API and generate the prowlarr form fetch('./api/settings') .then(response => response.json()) .then(settings => { console.log('[huntarrUI] Loaded settings for prowlarr:', settings); console.log('[huntarrUI] Prowlarr settings:', settings.prowlarr); console.log('[huntarrUI] SettingsForms available:', typeof SettingsForms !== 'undefined'); console.log('[huntarrUI] generateProwlarrForm available:', typeof SettingsForms !== 'undefined' && SettingsForms.generateProwlarrForm); // Generate the prowlarr form if (typeof SettingsForms !== 'undefined' && SettingsForms.generateProwlarrForm) { console.log('[huntarrUI] Calling SettingsForms.generateProwlarrForm...'); SettingsForms.generateProwlarrForm(prowlarrContainer, settings.prowlarr || {}); console.log('[huntarrUI] Prowlarr form generated successfully'); } else { console.error('[huntarrUI] SettingsForms.generateProwlarrForm not available'); prowlarrContainer.innerHTML = 'Error: Prowlarr forms not loaded
'; } }) .catch(error => { console.error('[huntarrUI] Error loading prowlarr settings:', error); prowlarrContainer.innerHTML = 'Error loading prowlarr settings
'; }); }, initializeUser: function() { console.log('[huntarrUI] initializeUser called'); // Check if UserModule is available and initialize it if (typeof UserModule !== 'undefined') { if (!window.userModule) { console.log('[huntarrUI] Creating UserModule instance...'); window.userModule = new UserModule(); console.log('[huntarrUI] UserModule initialized successfully'); } else { console.log('[huntarrUI] UserModule already exists'); } } else { console.error('[huntarrUI] UserModule not available - user.js may not be loaded'); } }, initializeSwaparr: function() { console.log('[huntarrUI] initializeSwaparr called'); // Check if Swaparr is already initialized const swaparrContainer = document.getElementById('swaparrContainer'); if (!swaparrContainer) { console.error('[huntarrUI] swaparrContainer element not found!'); return; } console.log('[huntarrUI] swaparrContainer found:', swaparrContainer); console.log('[huntarrUI] Current container content:', swaparrContainer.innerHTML.trim()); // Check if Swaparr is actually initialized (ignore HTML comments) const currentContent = swaparrContainer.innerHTML.trim(); if (currentContent !== '' && !currentContent.includes('')) { console.log('[huntarrUI] Swaparr already initialized, skipping'); return; // Already initialized } console.log('[huntarrUI] Loading Swaparr settings from API...'); // Load settings from API and generate the Swaparr form fetch('./api/swaparr/settings') .then(response => response.json()) .then(settings => { console.log('[huntarrUI] Loaded Swaparr settings:', settings); console.log('[huntarrUI] SettingsForms available:', typeof SettingsForms !== 'undefined'); console.log('[huntarrUI] generateSwaparrForm available:', typeof SettingsForms !== 'undefined' && SettingsForms.generateSwaparrForm); // Generate the Swaparr form if (typeof SettingsForms !== 'undefined' && SettingsForms.generateSwaparrForm) { console.log('[huntarrUI] Calling SettingsForms.generateSwaparrForm...'); SettingsForms.generateSwaparrForm(swaparrContainer, settings || {}); console.log('[huntarrUI] Swaparr form generated successfully'); // Load Swaparr apps table/status this.loadSwaparrApps(); } else { console.error('[huntarrUI] SettingsForms.generateSwaparrForm not available'); swaparrContainer.innerHTML = 'Error: Swaparr forms not loaded
'; } }) .catch(error => { console.error('[huntarrUI] Error loading Swaparr settings:', error); swaparrContainer.innerHTML = 'Error loading Swaparr settings
'; }); }, loadSwaparrApps: function() { console.log('[huntarrUI] loadSwaparrApps called'); // Get the Swaparr apps panel const swaparrAppsPanel = document.getElementById('swaparrApps'); if (!swaparrAppsPanel) { console.error('[huntarrUI] swaparrApps panel not found'); return; } // Check if there's a dedicated Swaparr apps module if (typeof window.swaparrModule !== 'undefined' && window.swaparrModule.loadApps) { console.log('[huntarrUI] Using dedicated Swaparr module to load apps'); window.swaparrModule.loadApps(); } else if (typeof SwaparrApps !== 'undefined') { console.log('[huntarrUI] Using SwaparrApps module to load apps'); SwaparrApps.loadApps(); } else { console.log('[huntarrUI] No dedicated Swaparr apps module found, using generic approach'); // Fall back to loading Swaparr status/info this.loadSwaparrStatus(); } }, // Setup Prowlarr status polling setupProwlarrStatusPolling: function() { console.log('[huntarrUI] Setting up Prowlarr status polling'); // Load initial status this.loadProwlarrStatus(); // Set up polling to refresh Prowlarr status every 30 seconds // Only poll when home section is active to reduce unnecessary requests this.prowlarrPollingInterval = setInterval(() => { if (this.currentSection === 'home') { this.loadProwlarrStatus(); } }, 30000); // Set up refresh button handler const refreshButton = document.getElementById('refresh-prowlarr-data'); if (refreshButton) { refreshButton.addEventListener('click', () => { console.log('[huntarrUI] Manual Prowlarr refresh triggered'); this.loadProwlarrStatus(); }); } }, }; // Note: redirectToSwaparr function removed - Swaparr now has its own dedicated section // Initialize when document is ready document.addEventListener('DOMContentLoaded', function() { huntarrUI.init(); // Initialize our enhanced UI features if (typeof StatsTooltips !== 'undefined') { StatsTooltips.init(); } if (typeof CardHoverEffects !== 'undefined') { CardHoverEffects.init(); } if (typeof CircularProgress !== 'undefined') { CircularProgress.init(); } if (typeof BackgroundPattern !== 'undefined') { BackgroundPattern.init(); } // Initialize per-instance reset button listeners if (typeof SettingsForms !== 'undefined' && typeof SettingsForms.setupInstanceResetListeners === 'function') { SettingsForms.setupInstanceResetListeners(); } // Initialize UserModule when available if (typeof UserModule !== 'undefined') { console.log('[huntarrUI] UserModule available, initializing...'); window.userModule = new UserModule(); } }); // Expose huntarrUI to the global scope for access by app modules window.huntarrUI = huntarrUI; // Expose state management timezone refresh function globally for settings forms window.refreshStateManagementTimezone = function() { if (window.huntarrUI && typeof window.huntarrUI.refreshStateManagementTimezone === 'function') { window.huntarrUI.refreshStateManagementTimezone(); } else { console.warn('[huntarrUI] refreshStateManagementTimezone function not available'); } };