mirror of
https://github.com/plexguide/Huntarr-Sonarr.git
synced 2025-12-16 20:04:16 -06:00
5249 lines
234 KiB
JavaScript
5249 lines
234 KiB
JavaScript
/**
|
|
* 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, '<span class="search-highlight">$1</span>');
|
|
|
|
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 = '<i class="fas fa-spinner fa-spin"></i> Auto-saving...';
|
|
statusElement.innerHTML = '<span style="color: #fbbf24;">Auto-saving settings before testing...</span>';
|
|
|
|
// Auto-save general settings before testing
|
|
this.autoSaveGeneralSettings()
|
|
.then(() => {
|
|
// Update button text to show we're now testing
|
|
buttonElement.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Sending...';
|
|
statusElement.innerHTML = '<span style="color: #fbbf24;">Sending test notification...</span>';
|
|
|
|
// 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 = '<span style="color: #10b981;">✓ Test notification sent successfully!</span>';
|
|
this.showNotification('Test notification sent! Check your notification service.', 'success');
|
|
} else {
|
|
statusElement.innerHTML = '<span style="color: #ef4444;">✗ Failed to send test notification</span>';
|
|
this.showNotification(data.error || 'Failed to send test notification', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Test notification error:', error);
|
|
statusElement.innerHTML = '<span style="color: #ef4444;">✗ Error during auto-save or testing</span>';
|
|
this.showNotification('Error during auto-save or testing: ' + error.message, 'error');
|
|
})
|
|
.finally(() => {
|
|
// Re-enable button
|
|
buttonElement.disabled = false;
|
|
buttonElement.innerHTML = '<i class="fas fa-bell"></i> 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 = '<i class="fas fa-times-circle"></i> 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 = `<i class="fas fa-plug"></i> 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 = '<i class="fas fa-check-circle"></i> Connected';
|
|
} else {
|
|
statusElement.className = 'status-badge not-connected';
|
|
statusElement.innerHTML = '<i class="fas fa-times-circle"></i> 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 = `<div class="loading-text" style="color: #ef4444;">${errorMessage}</div>`;
|
|
return;
|
|
}
|
|
|
|
if (!indexerDetails || (!indexerDetails.active && !indexerDetails.throttled && !indexerDetails.failed)) {
|
|
// No indexers found
|
|
indexersList.innerHTML = '<div class="loading-text">No indexers configured</div>';
|
|
return;
|
|
}
|
|
|
|
// Combine all indexers and sort alphabetically
|
|
let allIndexers = [];
|
|
|
|
// Add active indexers
|
|
if (indexerDetails.active) {
|
|
allIndexers = allIndexers.concat(
|
|
indexerDetails.active.map(idx => ({ ...idx, status: 'active' }))
|
|
);
|
|
}
|
|
|
|
// Add throttled indexers
|
|
if (indexerDetails.throttled) {
|
|
allIndexers = allIndexers.concat(
|
|
indexerDetails.throttled.map(idx => ({ ...idx, status: 'throttled' }))
|
|
);
|
|
}
|
|
|
|
// Add failed indexers
|
|
if (indexerDetails.failed) {
|
|
allIndexers = allIndexers.concat(
|
|
indexerDetails.failed.map(idx => ({ ...idx, status: 'failed' }))
|
|
);
|
|
}
|
|
|
|
// Sort alphabetically by name
|
|
allIndexers.sort((a, b) => a.name.localeCompare(b.name));
|
|
|
|
if (allIndexers.length === 0) {
|
|
indexersList.innerHTML = '<div class="loading-text">No indexers found</div>';
|
|
return;
|
|
}
|
|
|
|
// Build the HTML for indexers list with hover interactions
|
|
const indexersHtml = allIndexers.map(indexer => {
|
|
const statusText = indexer.status === 'active' ? 'Active' :
|
|
indexer.status === 'throttled' ? 'Throttled' :
|
|
'Failed';
|
|
|
|
return `
|
|
<div class="indexer-item" data-indexer-name="${indexer.name}">
|
|
<span class="indexer-name hoverable">${indexer.name}</span>
|
|
<span class="indexer-status ${indexer.status}">${statusText}</span>
|
|
</div>
|
|
`;
|
|
}).join('');
|
|
|
|
indexersList.innerHTML = indexersHtml;
|
|
|
|
// Add hover event listeners to indexer names
|
|
const indexerItems = indexersList.querySelectorAll('.indexer-item');
|
|
indexerItems.forEach(item => {
|
|
const indexerName = item.dataset.indexerName;
|
|
const nameElement = item.querySelector('.indexer-name');
|
|
|
|
nameElement.addEventListener('mouseenter', () => {
|
|
this.showIndexerStats(indexerName);
|
|
nameElement.classList.add('hovered');
|
|
});
|
|
|
|
nameElement.addEventListener('mouseleave', () => {
|
|
this.showOverallStats();
|
|
nameElement.classList.remove('hovered');
|
|
});
|
|
});
|
|
},
|
|
|
|
// Update Prowlarr statistics display
|
|
updateProwlarrStatistics: function(stats, errorMessage = null) {
|
|
const statisticsContent = document.getElementById('prowlarr-statistics-content');
|
|
if (!statisticsContent) return;
|
|
|
|
if (errorMessage) {
|
|
statisticsContent.innerHTML = `<div class="loading-text" style="color: #ef4444;">${errorMessage}</div>`;
|
|
return;
|
|
}
|
|
|
|
if (!stats) {
|
|
statisticsContent.innerHTML = '<div class="loading-text">No statistics available</div>';
|
|
return;
|
|
}
|
|
|
|
// Debug: Log the stats data
|
|
console.log('Statistics data:', stats);
|
|
|
|
// Build statistics cards HTML
|
|
let statisticsCards = [];
|
|
|
|
// Search activity
|
|
if (stats.searches_today !== undefined) {
|
|
const todayClass = stats.searches_today > 0 ? 'success' : '';
|
|
statisticsCards.push(`
|
|
<div class="stat-card">
|
|
<div class="stat-label">Searches Today</div>
|
|
<div class="stat-value ${todayClass}">${stats.searches_today}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
// Success rate (always show, even if 0 or undefined)
|
|
let successRate = 0;
|
|
if (stats.recent_success_rate !== undefined && stats.recent_success_rate !== null) {
|
|
successRate = stats.recent_success_rate;
|
|
}
|
|
const successClass = successRate >= 80 ? 'success' :
|
|
successRate >= 60 ? 'warning' : 'error';
|
|
statisticsCards.push(`
|
|
<div class="stat-card">
|
|
<div class="stat-label">Success Rate</div>
|
|
<div class="stat-value ${successClass}">${successRate}%</div>
|
|
</div>
|
|
`);
|
|
console.log('Added success rate card:', successRate, successClass);
|
|
|
|
// Average response time
|
|
if (stats.avg_response_time !== undefined) {
|
|
const responseClass = stats.avg_response_time <= 1000 ? 'success' :
|
|
stats.avg_response_time <= 3000 ? 'warning' : 'error';
|
|
const responseTime = stats.avg_response_time >= 1000 ?
|
|
`${(stats.avg_response_time / 1000).toFixed(1)}s` :
|
|
`${stats.avg_response_time}ms`;
|
|
statisticsCards.push(`
|
|
<div class="stat-card">
|
|
<div class="stat-label">Avg Response</div>
|
|
<div class="stat-value ${responseClass}">${responseTime}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
// Total API calls
|
|
if (stats.total_api_calls !== undefined) {
|
|
statisticsCards.push(`
|
|
<div class="stat-card">
|
|
<div class="stat-label">Total Searches</div>
|
|
<div class="stat-value">${stats.total_api_calls.toLocaleString()}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
// Failed searches (only show if > 0)
|
|
if (stats.recent_failed_searches !== undefined && stats.recent_failed_searches > 0) {
|
|
statisticsCards.push(`
|
|
<div class="stat-card">
|
|
<div class="stat-label">Failed Today</div>
|
|
<div class="stat-value error">${stats.recent_failed_searches}</div>
|
|
</div>
|
|
`);
|
|
}
|
|
|
|
if (statisticsCards.length === 0) {
|
|
statisticsContent.innerHTML = '<div class="loading-text">No recent activity</div>';
|
|
} else {
|
|
const finalHTML = statisticsCards.join('');
|
|
console.log('Final Prowlarr statistics HTML:', finalHTML);
|
|
console.log('Number of stat cards:', statisticsCards.length);
|
|
statisticsContent.innerHTML = finalHTML;
|
|
}
|
|
},
|
|
|
|
// Show statistics for a specific indexer
|
|
showIndexerStats: function(indexerName) {
|
|
// Update the statistics header
|
|
const statisticsHeader = document.querySelector('.prowlarr-statistics-card .subcard-header h5');
|
|
if (statisticsHeader) {
|
|
statisticsHeader.innerHTML = `<i class="fas fa-chart-bar"></i> ${indexerName} Stats`;
|
|
}
|
|
|
|
// Fetch and display indexer-specific stats
|
|
HuntarrUtils.fetchWithTimeout(`./api/prowlarr/indexer-stats/${encodeURIComponent(indexerName)}`)
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success && data.stats) {
|
|
this.updateProwlarrStatistics(data.stats);
|
|
} else {
|
|
this.updateProwlarrStatistics(null, `No stats available for ${indexerName}`);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(`Error loading stats for ${indexerName}:`, error);
|
|
this.updateProwlarrStatistics(null, 'Error loading indexer stats');
|
|
});
|
|
},
|
|
|
|
// Show overall statistics (when not hovering)
|
|
showOverallStats: function() {
|
|
// Reset the statistics header
|
|
const statisticsHeader = document.querySelector('.prowlarr-statistics-card .subcard-header h5');
|
|
if (statisticsHeader) {
|
|
statisticsHeader.innerHTML = '<i class="fas fa-chart-bar"></i> Statistics';
|
|
}
|
|
|
|
// Show cached overall stats
|
|
this.loadProwlarrStats();
|
|
},
|
|
|
|
|
|
|
|
|
|
// User
|
|
loadUsername: function() {
|
|
const usernameElement = document.getElementById('username');
|
|
if (!usernameElement) return;
|
|
|
|
HuntarrUtils.fetchWithTimeout('./api/user/info')
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.username) {
|
|
usernameElement.textContent = data.username;
|
|
}
|
|
|
|
// Check if local access bypass is enabled and update UI visibility
|
|
this.checkLocalAccessBypassStatus();
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading username:', error);
|
|
|
|
// Still check local access bypass status even if username loading failed
|
|
this.checkLocalAccessBypassStatus();
|
|
});
|
|
},
|
|
|
|
// Check if local access bypass is enabled and update UI accordingly
|
|
checkLocalAccessBypassStatus: function() {
|
|
console.log("Checking local access bypass status...");
|
|
HuntarrUtils.fetchWithTimeout('./api/get_local_access_bypass_status') // Corrected URL
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
// Log error if response is not OK (e.g., 404, 500)
|
|
console.error(`Error fetching bypass status: ${response.status} ${response.statusText}`);
|
|
// Attempt to read response body for more details, if available
|
|
response.text().then(text => console.error('Response body:', text));
|
|
// Throw an error to trigger the catch block with a clearer message
|
|
throw new Error(`HTTP error ${response.status}`);
|
|
}
|
|
return response.json(); // Only parse JSON if response is OK
|
|
})
|
|
.then(data => {
|
|
if (data && typeof data.isEnabled === 'boolean') {
|
|
console.log("Local access bypass status received:", data.isEnabled);
|
|
this.updateUIForLocalAccessBypass(data.isEnabled);
|
|
} else {
|
|
// Handle cases where response is JSON but not the expected format
|
|
console.error('Invalid data format received for bypass status:', data);
|
|
this.updateUIForLocalAccessBypass(false); // Default to disabled/showing elements
|
|
}
|
|
})
|
|
.catch(error => {
|
|
// Catch network errors and the error thrown from !response.ok
|
|
console.error('Error checking local access bypass status:', error);
|
|
// Default to showing elements if we can't determine status
|
|
this.updateUIForLocalAccessBypass(false);
|
|
});
|
|
},
|
|
|
|
// Update UI elements visibility based on local access bypass status
|
|
updateUIForLocalAccessBypass: function(isEnabled) {
|
|
console.log("Updating UI for local access bypass:", isEnabled);
|
|
|
|
// Get the user info container in topbar (username and logout button)
|
|
const userInfoContainer = document.getElementById('userInfoContainer');
|
|
|
|
// Get the user nav item in sidebar
|
|
const userNav = document.getElementById('userNav');
|
|
|
|
// Set display style explicitly based on local access bypass setting
|
|
if (isEnabled === true) {
|
|
console.log("Local access bypass is ENABLED - hiding user elements");
|
|
|
|
// Hide user info in topbar
|
|
if (userInfoContainer) {
|
|
userInfoContainer.style.display = 'none';
|
|
console.log(" • Hidden userInfoContainer");
|
|
} else {
|
|
console.warn(" ⚠ userInfoContainer not found");
|
|
}
|
|
|
|
// Always show user nav in sidebar regardless of authentication mode
|
|
if (userNav) {
|
|
userNav.style.display = '';
|
|
userNav.style.removeProperty('display'); // Remove any !important styles
|
|
console.log(" • User nav always visible (regardless of auth mode)");
|
|
} else {
|
|
console.warn(" ⚠ userNav not found");
|
|
}
|
|
} else {
|
|
console.log("Local access bypass is DISABLED - showing user elements");
|
|
|
|
// Show user info in topbar
|
|
if (userInfoContainer) {
|
|
userInfoContainer.style.display = '';
|
|
console.log(" • Showing userInfoContainer");
|
|
} else {
|
|
console.warn(" ⚠ userInfoContainer not found");
|
|
}
|
|
|
|
// Show user nav in sidebar
|
|
if (userNav) {
|
|
userNav.style.display = '';
|
|
console.log(" • Showing userNav");
|
|
} else {
|
|
console.warn(" ⚠ userNav not found");
|
|
}
|
|
}
|
|
},
|
|
|
|
logout: function(e) { // Added logout function
|
|
e.preventDefault(); // Prevent default link behavior
|
|
console.log('[huntarrUI] Logging out...');
|
|
HuntarrUtils.fetchWithTimeout('./logout', { // Use the correct endpoint defined in Flask
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
}
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.log('[huntarrUI] Logout successful, redirecting to login.');
|
|
window.location.href = './login'; // Redirect to login page
|
|
} else {
|
|
console.error('[huntarrUI] Logout failed:', data.message);
|
|
this.showNotification('Logout failed. Please try again.', 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error during logout:', error);
|
|
this.showNotification('An error occurred during logout.', 'error');
|
|
});
|
|
},
|
|
|
|
// Media statistics handling
|
|
loadMediaStats: function() {
|
|
// Prevent multiple simultaneous stats loading
|
|
if (this.isLoadingStats) {
|
|
console.debug('Stats already loading, skipping duplicate request');
|
|
return;
|
|
}
|
|
|
|
this.isLoadingStats = true;
|
|
|
|
// Try to load cached stats first for immediate display
|
|
const cachedStats = localStorage.getItem('huntarr-stats-cache');
|
|
if (cachedStats) {
|
|
try {
|
|
const parsedStats = JSON.parse(cachedStats);
|
|
const cacheAge = Date.now() - (parsedStats.timestamp || 0);
|
|
// Use cache if less than 5 minutes old
|
|
if (cacheAge < 300000) {
|
|
console.log('[huntarrUI] Using cached stats for immediate display');
|
|
this.updateStatsDisplay(parsedStats.stats, true); // true = from cache
|
|
}
|
|
} catch (e) {
|
|
console.log('[huntarrUI] Failed to parse cached stats');
|
|
}
|
|
}
|
|
|
|
// Add loading class to stats container to hide raw JSON
|
|
const statsContainer = document.querySelector('.media-stats-container');
|
|
if (statsContainer) {
|
|
statsContainer.classList.add('stats-loading');
|
|
}
|
|
|
|
HuntarrUtils.fetchWithTimeout('./api/stats')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Network response was not ok');
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success && data.stats) {
|
|
// Store raw stats data globally for tooltips to access
|
|
window.mediaStats = data.stats;
|
|
|
|
// Cache the fresh stats with timestamp
|
|
localStorage.setItem('huntarr-stats-cache', JSON.stringify({
|
|
stats: data.stats,
|
|
timestamp: Date.now()
|
|
}));
|
|
|
|
// Update display
|
|
this.updateStatsDisplay(data.stats);
|
|
|
|
// Remove loading class after stats are loaded
|
|
if (statsContainer) {
|
|
statsContainer.classList.remove('stats-loading');
|
|
}
|
|
} else {
|
|
console.error('Failed to load statistics:', data.message || 'Unknown error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching statistics:', error);
|
|
// Remove loading class on error too
|
|
if (statsContainer) {
|
|
statsContainer.classList.remove('stats-loading');
|
|
}
|
|
})
|
|
.finally(() => {
|
|
// Always clear the loading flag
|
|
this.isLoadingStats = false;
|
|
});
|
|
},
|
|
|
|
updateStatsDisplay: function(stats, isFromCache = false) {
|
|
// Update each app's statistics
|
|
const apps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros', 'swaparr'];
|
|
const statTypes = ['hunted', 'upgraded'];
|
|
|
|
// More robust low usage mode detection - check multiple sources
|
|
const isLowUsageMode = this.isLowUsageModeEnabled();
|
|
|
|
|
|
console.log(`[huntarrUI] updateStatsDisplay - Low usage mode: ${isLowUsageMode}, from cache: ${isFromCache}`);
|
|
|
|
apps.forEach(app => {
|
|
if (stats[app]) {
|
|
statTypes.forEach(type => {
|
|
const element = document.getElementById(`${app}-${type}`);
|
|
if (element) {
|
|
// Get current and target values, ensuring they're valid numbers
|
|
const currentText = element.textContent || '0';
|
|
const currentValue = this.parseFormattedNumber(currentText);
|
|
const targetValue = Math.max(0, parseInt(stats[app][type]) || 0); // Ensure non-negative
|
|
|
|
// If low usage mode is enabled or loading from cache, skip animations and set values directly
|
|
if (isLowUsageMode || isFromCache) {
|
|
element.textContent = this.formatLargeNumber(targetValue);
|
|
} else {
|
|
// Only animate if values are different and both are valid
|
|
if (currentValue !== targetValue && !isNaN(currentValue) && !isNaN(targetValue)) {
|
|
// Cancel any existing animation for this element
|
|
if (element.animationFrame) {
|
|
cancelAnimationFrame(element.animationFrame);
|
|
}
|
|
|
|
// Animate the number change
|
|
this.animateNumber(element, currentValue, targetValue);
|
|
} else if (isNaN(currentValue) || currentValue < 0) {
|
|
// If current value is invalid, set directly without animation
|
|
element.textContent = this.formatLargeNumber(targetValue);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
// Helper function to parse formatted numbers back to integers
|
|
parseFormattedNumber: function(formattedStr) {
|
|
if (!formattedStr || typeof formattedStr !== 'string') return 0;
|
|
|
|
// Remove any formatting (K, M, commas, etc.)
|
|
const cleanStr = formattedStr.replace(/[^\d.-]/g, '');
|
|
const parsed = parseInt(cleanStr);
|
|
|
|
// Handle K and M suffixes
|
|
if (formattedStr.includes('K')) {
|
|
return Math.floor(parsed * 1000);
|
|
} else if (formattedStr.includes('M')) {
|
|
return Math.floor(parsed * 1000000);
|
|
}
|
|
|
|
return isNaN(parsed) ? 0 : Math.max(0, parsed);
|
|
},
|
|
|
|
animateNumber: function(element, start, end) {
|
|
// Ensure start and end are valid numbers
|
|
start = Math.max(0, parseInt(start) || 0);
|
|
end = Math.max(0, parseInt(end) || 0);
|
|
|
|
// If start equals end, just set the value
|
|
if (start === end) {
|
|
element.textContent = this.formatLargeNumber(end);
|
|
return;
|
|
}
|
|
|
|
const duration = 600; // Animation duration in milliseconds - reduced for faster loading feel
|
|
const startTime = performance.now();
|
|
|
|
const updateNumber = (currentTime) => {
|
|
const elapsedTime = currentTime - startTime;
|
|
const progress = Math.min(elapsedTime / duration, 1);
|
|
|
|
// Easing function for smooth animation
|
|
const easeOutQuad = progress * (2 - progress);
|
|
|
|
const currentValue = Math.max(0, Math.floor(start + (end - start) * easeOutQuad));
|
|
|
|
// Format number for display
|
|
element.textContent = this.formatLargeNumber(currentValue);
|
|
|
|
if (progress < 1) {
|
|
// Store the animation frame ID to allow cancellation
|
|
element.animationFrame = requestAnimationFrame(updateNumber);
|
|
} else {
|
|
// Ensure we end with the exact formatted target number
|
|
element.textContent = this.formatLargeNumber(end);
|
|
// Clear the animation frame reference
|
|
element.animationFrame = null;
|
|
}
|
|
};
|
|
|
|
// Store the animation frame ID to allow cancellation
|
|
element.animationFrame = requestAnimationFrame(updateNumber);
|
|
},
|
|
|
|
// Format large numbers with appropriate suffixes (K, M, B, T)
|
|
formatLargeNumber: function(num) {
|
|
if (num < 1000) {
|
|
// 0-999: Display as is
|
|
return num.toString();
|
|
} else if (num < 10000) {
|
|
// 1,000-9,999: Display with single decimal and K (e.g., 5.2K)
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
} else if (num < 100000) {
|
|
// 10,000-99,999: Display with single decimal and K (e.g., 75.4K)
|
|
return (num / 1000).toFixed(1) + 'K';
|
|
} else if (num < 1000000) {
|
|
// 100,000-999,999: Display with K (no decimal) (e.g., 982K)
|
|
return Math.floor(num / 1000) + 'K';
|
|
} else if (num < 10000000) {
|
|
// 1,000,000-9,999,999: Display with single decimal and M (e.g., 9.7M)
|
|
return (num / 1000000).toFixed(1) + 'M';
|
|
} else if (num < 100000000) {
|
|
// 10,000,000-99,999,999: Display with single decimal and M (e.g., 99.7M)
|
|
return (num / 1000000).toFixed(1) + 'M';
|
|
} else if (num < 1000000000) {
|
|
// 100,000,000-999,999,999: Display with M (no decimal)
|
|
return Math.floor(num / 1000000) + 'M';
|
|
} else if (num < 1000000000000) {
|
|
// 1B - 999B: Display with single decimal and B
|
|
return (num / 1000000000).toFixed(1) + 'B';
|
|
} else {
|
|
// 1T+: Display with T
|
|
return (num / 1000000000000).toFixed(1) + 'T';
|
|
}
|
|
},
|
|
|
|
resetMediaStats: function(appType = null) {
|
|
// Directly update the UI first to provide immediate feedback
|
|
const stats = {
|
|
'sonarr': {'hunted': 0, 'upgraded': 0},
|
|
'radarr': {'hunted': 0, 'upgraded': 0},
|
|
'lidarr': {'hunted': 0, 'upgraded': 0},
|
|
'readarr': {'hunted': 0, 'upgraded': 0},
|
|
'whisparr': {'hunted': 0, 'upgraded': 0},
|
|
'eros': {'hunted': 0, 'upgraded': 0}
|
|
};
|
|
|
|
// Immediately update UI before even showing the confirmation
|
|
if (appType) {
|
|
// Only reset the specific app's stats
|
|
this.updateStatsDisplay({
|
|
[appType]: stats[appType]
|
|
});
|
|
} else {
|
|
// Reset all stats
|
|
this.updateStatsDisplay(stats);
|
|
}
|
|
|
|
// Show a success notification
|
|
this.showNotification('Statistics reset successfully', 'success');
|
|
|
|
// Try to send the reset to the server, but don't depend on it
|
|
try {
|
|
const requestBody = appType ? { app_type: appType } : {};
|
|
|
|
HuntarrUtils.fetchWithTimeout('./api/stats/reset_public', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(requestBody)
|
|
})
|
|
.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 stats reset');
|
|
}
|
|
return response.json().catch(() => ({}));
|
|
})
|
|
.then(data => {
|
|
console.log('Stats reset response:', data);
|
|
})
|
|
.catch(error => {
|
|
console.warn('Error communicating with server for stats reset:', error);
|
|
});
|
|
} catch (error) {
|
|
console.warn('Error in stats reset:', error);
|
|
}
|
|
},
|
|
|
|
// Utility functions
|
|
showNotification: function(message, type) {
|
|
// Create a notification element
|
|
const notification = document.createElement('div');
|
|
notification.className = `notification ${type}`;
|
|
notification.textContent = message;
|
|
|
|
// Add to the document
|
|
document.body.appendChild(notification);
|
|
|
|
// Ensure any existing notification is removed first to prevent stacking
|
|
const existingNotifications = document.querySelectorAll('.notification');
|
|
existingNotifications.forEach(n => {
|
|
if (n !== notification) {
|
|
n.classList.remove('show');
|
|
setTimeout(() => n.remove(), 300);
|
|
}
|
|
});
|
|
|
|
// Fade in
|
|
setTimeout(() => {
|
|
notification.classList.add('show');
|
|
}, 10);
|
|
|
|
// Remove after a delay
|
|
setTimeout(() => {
|
|
notification.classList.remove('show');
|
|
setTimeout(() => {
|
|
notification.remove();
|
|
}, 300);
|
|
}, 3000);
|
|
},
|
|
|
|
capitalizeFirst: function(string) {
|
|
return string.charAt(0).toUpperCase() + string.slice(1);
|
|
},
|
|
|
|
// Load current version from version.txt
|
|
loadCurrentVersion: function() {
|
|
HuntarrUtils.fetchWithTimeout('./version.txt')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error('Failed to load version.txt');
|
|
}
|
|
return response.text();
|
|
})
|
|
.then(version => {
|
|
const versionElement = document.getElementById('version-value');
|
|
if (versionElement) {
|
|
versionElement.textContent = version.trim();
|
|
versionElement.style.display = 'inline'; // Show the element
|
|
}
|
|
|
|
// Store in localStorage for topbar access
|
|
try {
|
|
const versionInfo = localStorage.getItem('huntarr-version-info') || '{}';
|
|
const parsedInfo = JSON.parse(versionInfo);
|
|
parsedInfo.currentVersion = version.trim();
|
|
localStorage.setItem('huntarr-version-info', JSON.stringify(parsedInfo));
|
|
} catch (e) {
|
|
console.error('Error saving current version to localStorage:', e);
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading current version:', error);
|
|
const versionElement = document.getElementById('version-value');
|
|
if (versionElement) {
|
|
versionElement.textContent = 'Error';
|
|
versionElement.style.display = 'inline'; // Show the element even on error
|
|
}
|
|
});
|
|
},
|
|
|
|
// Load latest version from GitHub releases
|
|
loadLatestVersion: function() {
|
|
HuntarrUtils.fetchWithTimeout('https://api.github.com/repos/plexguide/Huntarr.io/releases/latest')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
// Handle rate limiting or other errors
|
|
if (response.status === 403) {
|
|
console.warn('GitHub API rate limit likely exceeded.');
|
|
throw new Error('Rate limited');
|
|
}
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const latestVersionElement = document.getElementById('latest-version-value');
|
|
if (latestVersionElement && data && data.tag_name) {
|
|
// Remove potential 'v' prefix for consistency if needed, or keep it
|
|
latestVersionElement.textContent = data.tag_name;
|
|
latestVersionElement.style.display = 'inline'; // Show the element
|
|
|
|
// Store in localStorage for topbar access
|
|
try {
|
|
const versionInfo = localStorage.getItem('huntarr-version-info') || '{}';
|
|
const parsedInfo = JSON.parse(versionInfo);
|
|
parsedInfo.latestVersion = data.tag_name;
|
|
localStorage.setItem('huntarr-version-info', JSON.stringify(parsedInfo));
|
|
} catch (e) {
|
|
console.error('Error saving latest version to localStorage:', e);
|
|
}
|
|
} else if (latestVersionElement) {
|
|
latestVersionElement.textContent = 'N/A';
|
|
latestVersionElement.style.display = 'inline'; // Show the element
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading latest version from GitHub:', error);
|
|
const latestVersionElement = document.getElementById('latest-version-value');
|
|
if (latestVersionElement) {
|
|
latestVersionElement.textContent = error.message === 'Rate limited' ? 'Rate Limited' : 'Error';
|
|
latestVersionElement.style.display = 'inline'; // Show the element even on error
|
|
}
|
|
});
|
|
},
|
|
|
|
// Load latest beta version from GitHub tags
|
|
loadBetaVersion: function() {
|
|
HuntarrUtils.fetchWithTimeout('https://api.github.com/repos/plexguide/Huntarr.io/tags?per_page=100')
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
// Handle rate limiting or other errors
|
|
if (response.status === 403) {
|
|
console.warn('GitHub API rate limit likely exceeded.');
|
|
throw new Error('Rate limited');
|
|
}
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
const betaVersionElement = document.getElementById('beta-version-value');
|
|
|
|
if (betaVersionElement && data && Array.isArray(data) && data.length > 0) {
|
|
// Find the first tag that starts with B (case insensitive)
|
|
const betaTag = data.find(tag => tag.name.toUpperCase().startsWith('B'));
|
|
|
|
if (betaTag) {
|
|
betaVersionElement.textContent = betaTag.name;
|
|
// Store in localStorage for future reference
|
|
try {
|
|
const versionInfo = localStorage.getItem('huntarr-version-info') || '{}';
|
|
const parsedInfo = JSON.parse(versionInfo);
|
|
parsedInfo.betaVersion = betaTag.name;
|
|
localStorage.setItem('huntarr-version-info', JSON.stringify(parsedInfo));
|
|
} catch (e) {
|
|
console.error('Error saving beta version to localStorage:', e);
|
|
}
|
|
} else {
|
|
betaVersionElement.textContent = 'None';
|
|
}
|
|
} else if (betaVersionElement) {
|
|
betaVersionElement.textContent = 'N/A';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error loading beta version from GitHub:', error);
|
|
const betaVersionElement = document.getElementById('beta-version-value');
|
|
if (betaVersionElement) {
|
|
betaVersionElement.textContent = error.message === 'Rate limited' ? 'Rate Limited' : 'Error';
|
|
}
|
|
});
|
|
},
|
|
|
|
// Load GitHub star count
|
|
loadGitHubStarCount: function() {
|
|
const starsElement = document.getElementById('github-stars-value');
|
|
if (!starsElement) return;
|
|
|
|
// First, try to load from cache immediately for fast display
|
|
const cachedData = localStorage.getItem('huntarr-github-stars');
|
|
if (cachedData) {
|
|
try {
|
|
const parsed = JSON.parse(cachedData);
|
|
if (parsed.stars !== undefined) {
|
|
starsElement.textContent = parsed.stars.toLocaleString();
|
|
// If cache is recent (less than 1 hour), skip API call
|
|
const cacheAge = Date.now() - (parsed.timestamp || 0);
|
|
if (cacheAge < 3600000) { // 1 hour = 3600000ms
|
|
return;
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('Invalid cached star data, will fetch fresh');
|
|
localStorage.removeItem('huntarr-github-stars');
|
|
}
|
|
}
|
|
|
|
starsElement.textContent = 'Loading...';
|
|
|
|
// GitHub API endpoint for repository information
|
|
const apiUrl = 'https://api.github.com/repos/plexguide/huntarr';
|
|
|
|
HuntarrUtils.fetchWithTimeout(apiUrl)
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`GitHub API error: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data && data.stargazers_count !== undefined) {
|
|
// Format the number with commas for thousands
|
|
const formattedStars = data.stargazers_count.toLocaleString();
|
|
starsElement.textContent = formattedStars;
|
|
|
|
// Store in localStorage to avoid excessive API requests
|
|
const cacheData = {
|
|
stars: data.stargazers_count,
|
|
timestamp: Date.now()
|
|
};
|
|
localStorage.setItem('huntarr-github-stars', JSON.stringify(cacheData));
|
|
} else {
|
|
throw new Error('Star count not found in response');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error fetching GitHub stars:', error);
|
|
|
|
// Try to load from cache if we have it
|
|
const cachedData = localStorage.getItem('huntarr-github-stars');
|
|
if (cachedData) {
|
|
try {
|
|
const parsed = JSON.parse(cachedData);
|
|
if (parsed.stars !== undefined) {
|
|
starsElement.textContent = parsed.stars.toLocaleString();
|
|
} else {
|
|
starsElement.textContent = 'N/A';
|
|
}
|
|
} catch (e) {
|
|
console.error('Failed to parse cached star data:', e);
|
|
starsElement.textContent = 'N/A';
|
|
localStorage.removeItem('huntarr-github-stars'); // Clear bad cache
|
|
}
|
|
} else {
|
|
starsElement.textContent = 'N/A';
|
|
}
|
|
});
|
|
},
|
|
|
|
// Update home connection status
|
|
updateHomeConnectionStatus: function() {
|
|
console.log('[huntarrUI] Updating home connection statuses...');
|
|
// This function should ideally call checkAppConnection for all relevant apps
|
|
// or use the stored configuredApps status if checkAppConnection updates it.
|
|
this.checkAppConnections(); // Re-check all connections after a save might be simplest
|
|
},
|
|
|
|
// Load stateful management info
|
|
loadStatefulInfo: function(attempts = 0, skipCache = false) {
|
|
const initialStateEl = document.getElementById('stateful_initial_state');
|
|
const expiresDateEl = document.getElementById('stateful_expires_date');
|
|
const intervalInput = document.getElementById('stateful_management_hours');
|
|
const intervalDaysSpan = document.getElementById('stateful_management_days');
|
|
|
|
// Max retry attempts - increased for better reliability
|
|
const maxAttempts = 5;
|
|
|
|
console.log(`[StatefulInfo] Loading stateful info (attempt ${attempts + 1}, skipCache: ${skipCache})`);
|
|
|
|
// Update UI to show loading state instead of N/A on first attempt
|
|
if (attempts === 0) {
|
|
if (initialStateEl && initialStateEl.textContent !== 'Loading...') initialStateEl.textContent = 'Loading...';
|
|
if (expiresDateEl && expiresDateEl.textContent !== 'Updating...') expiresDateEl.textContent = 'Loading...';
|
|
}
|
|
|
|
// First check if we have cached data in localStorage that we can use immediately
|
|
const cachedStatefulData = localStorage.getItem('huntarr-stateful-data');
|
|
if (!skipCache && cachedStatefulData && attempts === 0) {
|
|
try {
|
|
const parsedData = JSON.parse(cachedStatefulData);
|
|
const cacheAge = Date.now() - parsedData.timestamp;
|
|
|
|
// Use cache if it's less than 5 minutes old while waiting for fresh data
|
|
if (cacheAge < 300000) {
|
|
console.log('[StatefulInfo] Using cached data while fetching fresh data');
|
|
|
|
// Display cached data
|
|
if (initialStateEl && parsedData.created_at_ts) {
|
|
const createdDate = new Date(parsedData.created_at_ts * 1000);
|
|
initialStateEl.textContent = this.formatDateNicely(createdDate);
|
|
}
|
|
|
|
if (expiresDateEl && parsedData.expires_at_ts) {
|
|
const expiresDate = new Date(parsedData.expires_at_ts * 1000);
|
|
expiresDateEl.textContent = this.formatDateNicely(expiresDate);
|
|
}
|
|
|
|
// Update interval input and days display
|
|
if (intervalInput && parsedData.interval_hours) {
|
|
intervalInput.value = parsedData.interval_hours;
|
|
if (intervalDaysSpan) {
|
|
const days = (parsedData.interval_hours / 24).toFixed(1);
|
|
intervalDaysSpan.textContent = `${days} days`;
|
|
}
|
|
}
|
|
}
|
|
} catch (e) {
|
|
console.warn('[StatefulInfo] Error parsing cached data:', e);
|
|
}
|
|
}
|
|
|
|
// Always fetch fresh data from the server
|
|
HuntarrUtils.fetchWithTimeout('./api/stateful/info', {
|
|
cache: 'no-cache',
|
|
headers: {
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache',
|
|
'Expires': '0'
|
|
}
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
// Cache the response with a timestamp for future use
|
|
localStorage.setItem('huntarr-stateful-data', JSON.stringify({
|
|
...data,
|
|
timestamp: Date.now()
|
|
}));
|
|
|
|
// Handle initial state date
|
|
if (initialStateEl) {
|
|
if (data.created_at_ts) {
|
|
const createdDate = new Date(data.created_at_ts * 1000);
|
|
initialStateEl.textContent = this.formatDateNicely(createdDate);
|
|
} else {
|
|
initialStateEl.textContent = 'Not yet created';
|
|
|
|
// If this is the first state load attempt and no timestamp exists,
|
|
// it might be because the state file hasn't been created yet
|
|
if (attempts < maxAttempts) {
|
|
console.log(`[StatefulInfo] No initial state timestamp, will retry (${attempts + 1}/${maxAttempts})`);
|
|
setTimeout(() => {
|
|
this.loadStatefulInfo(attempts + 1);
|
|
}, 500); // Longer delay for better chance of success
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Handle expiration date
|
|
if (expiresDateEl) {
|
|
if (data.expires_at_ts) {
|
|
const expiresDate = new Date(data.expires_at_ts * 1000);
|
|
expiresDateEl.textContent = this.formatDateNicely(expiresDate);
|
|
} else {
|
|
expiresDateEl.textContent = 'Not set';
|
|
}
|
|
}
|
|
|
|
// Update interval input and days display
|
|
if (intervalInput && data.interval_hours) {
|
|
intervalInput.value = data.interval_hours;
|
|
if (intervalDaysSpan) {
|
|
const days = (data.interval_hours / 24).toFixed(1);
|
|
intervalDaysSpan.textContent = `${days} days`;
|
|
}
|
|
}
|
|
|
|
// Hide error notification if it was visible
|
|
const notification = document.getElementById('stateful-notification');
|
|
if (notification) {
|
|
notification.style.display = 'none';
|
|
}
|
|
|
|
// Store the data for future reference
|
|
this._cachedStatefulData = data;
|
|
|
|
console.log('[StatefulInfo] Successfully loaded and displayed stateful data');
|
|
} else {
|
|
throw new Error(data.message || 'Failed to load stateful info');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(`Error loading stateful info (attempt ${attempts + 1}/${maxAttempts + 1}):`, error);
|
|
|
|
// Retry if we haven't reached max attempts with exponential backoff
|
|
if (attempts < maxAttempts) {
|
|
const delay = Math.min(2000, 300 * Math.pow(2, attempts)); // Exponential backoff with max 2000ms
|
|
console.log(`[StatefulInfo] Retrying in ${delay}ms (attempt ${attempts + 1}/${maxAttempts})`);
|
|
setTimeout(() => {
|
|
// Double-check if still on the same page before retrying
|
|
if (document.getElementById('stateful_management_hours')) {
|
|
this.loadStatefulInfo(attempts + 1);
|
|
} else {
|
|
console.log(`[StatefulInfo] Stateful info retry cancelled; user navigated away.`);
|
|
}
|
|
}, delay);
|
|
return;
|
|
}
|
|
|
|
// Use cached data as fallback if available
|
|
const cachedStatefulData = localStorage.getItem('huntarr-stateful-data');
|
|
if (cachedStatefulData) {
|
|
try {
|
|
console.log('[StatefulInfo] Using cached data as fallback after failed fetch');
|
|
const parsedData = JSON.parse(cachedStatefulData);
|
|
|
|
if (initialStateEl && parsedData.created_at_ts) {
|
|
const createdDate = new Date(parsedData.created_at_ts * 1000);
|
|
initialStateEl.textContent = this.formatDateNicely(createdDate) + ' (cached)';
|
|
} else if (initialStateEl) {
|
|
initialStateEl.textContent = 'Not available';
|
|
}
|
|
|
|
if (expiresDateEl && parsedData.expires_at_ts) {
|
|
const expiresDate = new Date(parsedData.expires_at_ts * 1000);
|
|
expiresDateEl.textContent = this.formatDateNicely(expiresDate) + ' (cached)';
|
|
} else if (expiresDateEl) {
|
|
expiresDateEl.textContent = 'Not available';
|
|
}
|
|
|
|
// Update interval input and days display from cache
|
|
if (intervalInput && parsedData.interval_hours) {
|
|
intervalInput.value = parsedData.interval_hours;
|
|
if (intervalDaysSpan) {
|
|
const days = (parsedData.interval_hours / 24).toFixed(1);
|
|
intervalDaysSpan.textContent = `${days} days`;
|
|
}
|
|
}
|
|
|
|
return;
|
|
} catch (e) {
|
|
console.warn('[StatefulInfo] Error parsing cached data as fallback:', e);
|
|
}
|
|
}
|
|
|
|
// Final fallback if no cached data
|
|
if (initialStateEl) initialStateEl.textContent = 'Not available';
|
|
if (expiresDateEl) expiresDateEl.textContent = 'Not available';
|
|
|
|
// Show error notification
|
|
const notification = document.getElementById('stateful-notification');
|
|
if (notification) {
|
|
notification.style.display = 'block';
|
|
notification.textContent = 'Could not load stateful management info. This may affect media tracking.';
|
|
}
|
|
});
|
|
},
|
|
|
|
// Format date nicely with time, day, and relative time indication
|
|
formatDateNicely: function(date) {
|
|
if (!(date instanceof Date) || isNaN(date)) {
|
|
console.warn('[formatDateNicely] Invalid date provided:', date);
|
|
return 'Invalid date';
|
|
}
|
|
|
|
// Get the user's configured timezone from settings or default to UTC
|
|
const userTimezone = this.getUserTimezone();
|
|
|
|
console.log(`[formatDateNicely] Formatting date ${date.toISOString()} for timezone: ${userTimezone}`);
|
|
|
|
const options = {
|
|
weekday: 'short',
|
|
year: 'numeric',
|
|
month: 'short',
|
|
day: 'numeric',
|
|
hour: '2-digit',
|
|
minute: '2-digit',
|
|
hour12: false, // Use 24-hour format (global world time)
|
|
timeZone: userTimezone
|
|
};
|
|
|
|
let formattedDate;
|
|
try {
|
|
formattedDate = date.toLocaleDateString(undefined, options);
|
|
console.log(`[formatDateNicely] Formatted result: ${formattedDate}`);
|
|
} catch (error) {
|
|
console.error(`[formatDateNicely] Error formatting date with timezone ${userTimezone}:`, error);
|
|
// Fallback to UTC if timezone is invalid
|
|
const fallbackOptions = { ...options, timeZone: 'UTC' };
|
|
formattedDate = date.toLocaleDateString(undefined, fallbackOptions) + ' (UTC fallback)';
|
|
}
|
|
|
|
// Add relative time indicator (e.g., "in 6 days" or "7 days ago")
|
|
const now = new Date();
|
|
const diffTime = date.getTime() - now.getTime();
|
|
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24));
|
|
|
|
let relativeTime = '';
|
|
if (diffDays > 0) {
|
|
relativeTime = ` (in ${diffDays} day${diffDays !== 1 ? 's' : ''})`;
|
|
} else if (diffDays < 0) {
|
|
relativeTime = ` (${Math.abs(diffDays)} day${Math.abs(diffDays) !== 1 ? 's' : ''} ago)`;
|
|
} else {
|
|
relativeTime = ' (today)';
|
|
}
|
|
|
|
return `${formattedDate}${relativeTime}`;
|
|
},
|
|
|
|
// Helper function to get the user's configured timezone from settings
|
|
getUserTimezone: function() {
|
|
// Assume UTC as default if no timezone is set
|
|
const defaultTimezone = 'UTC';
|
|
|
|
// Try multiple sources for the timezone setting
|
|
let timezone = null;
|
|
|
|
// 1. Try to get from originalSettings.general
|
|
if (this.originalSettings && this.originalSettings.general && this.originalSettings.general.timezone) {
|
|
timezone = this.originalSettings.general.timezone;
|
|
}
|
|
|
|
// 2. Try to get from the timezone dropdown if it exists (for immediate updates)
|
|
if (!timezone) {
|
|
const timezoneSelect = document.getElementById('timezone');
|
|
if (timezoneSelect && timezoneSelect.value) {
|
|
timezone = timezoneSelect.value;
|
|
}
|
|
}
|
|
|
|
// 3. Try to get from localStorage cache
|
|
if (!timezone) {
|
|
const cachedSettings = localStorage.getItem('huntarr-settings-cache');
|
|
if (cachedSettings) {
|
|
try {
|
|
const parsed = JSON.parse(cachedSettings);
|
|
if (parsed.general && parsed.general.timezone) {
|
|
timezone = parsed.general.timezone;
|
|
}
|
|
} catch (e) {
|
|
console.warn('[getUserTimezone] Error parsing cached settings:', e);
|
|
}
|
|
}
|
|
}
|
|
|
|
// 4. Fallback to default
|
|
if (!timezone) {
|
|
timezone = defaultTimezone;
|
|
}
|
|
|
|
console.log(`[getUserTimezone] Using timezone: ${timezone}`);
|
|
return timezone;
|
|
},
|
|
|
|
// Reset stateful management - clear all processed IDs
|
|
resetStatefulManagement: function() {
|
|
console.log("Reset stateful management function called");
|
|
|
|
// Show a loading indicator or disable the button
|
|
const resetBtn = document.getElementById('reset_stateful_btn');
|
|
if (resetBtn) {
|
|
resetBtn.disabled = true;
|
|
const originalText = resetBtn.innerHTML;
|
|
resetBtn.innerHTML = '<i class="fas fa-spinner fa-spin"></i> Resetting...';
|
|
console.log("Reset button found and disabled:", resetBtn);
|
|
} else {
|
|
console.error("Reset button not found in the DOM!");
|
|
}
|
|
|
|
// Add debug logging
|
|
console.log("Sending reset request to /api/stateful/reset");
|
|
|
|
HuntarrUtils.fetchWithTimeout('./api/stateful/reset', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json',
|
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
'Pragma': 'no-cache'
|
|
},
|
|
cache: 'no-cache' // Add cache control to prevent caching
|
|
})
|
|
.then(response => {
|
|
console.log("Reset response received:", response.status, response.statusText);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log("Reset response data:", data);
|
|
|
|
if (data.success) {
|
|
this.showNotification('Stateful management reset successfully', 'success');
|
|
// Wait a moment before reloading the info to ensure it's refreshed
|
|
setTimeout(() => {
|
|
this.loadStatefulInfo(0); // Reload stateful info with fresh attempt
|
|
|
|
// Re-enable the button
|
|
if (resetBtn) {
|
|
resetBtn.disabled = false;
|
|
resetBtn.innerHTML = '<i class="fas fa-trash"></i> Reset';
|
|
}
|
|
}, 1000);
|
|
} else {
|
|
throw new Error(data.message || 'Unknown error resetting stateful management');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error("Error resetting stateful management:", error);
|
|
this.showNotification(`Error resetting stateful management: ${error.message}`, 'error');
|
|
|
|
// Re-enable the button
|
|
if (resetBtn) {
|
|
resetBtn.disabled = false;
|
|
resetBtn.innerHTML = '<i class="fas fa-trash"></i> Reset';
|
|
}
|
|
});
|
|
},
|
|
|
|
// Update stateful management expiration based on hours input
|
|
updateStatefulExpirationOnUI: function() {
|
|
const hoursInput = document.getElementById('stateful_management_hours');
|
|
if (!hoursInput) return;
|
|
|
|
const hours = parseInt(hoursInput.value) || 168;
|
|
|
|
// Show updating indicator
|
|
const expiresDateEl = document.getElementById('stateful_expires_date');
|
|
const initialStateEl = document.getElementById('stateful_initial_state');
|
|
|
|
if (expiresDateEl) {
|
|
expiresDateEl.textContent = 'Updating...';
|
|
}
|
|
|
|
const url = './api/stateful/update-expiration';
|
|
const cleanedUrl = this.cleanUrlString(url);
|
|
|
|
HuntarrUtils.fetchWithTimeout(cleanedUrl, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Accept': 'application/json',
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ hours: hours }),
|
|
cache: 'no-cache'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status} ${response.statusText}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.log('[huntarrUI] Stateful expiration updated successfully:', data);
|
|
|
|
// Get updated info to show proper dates
|
|
this.loadStatefulInfo();
|
|
|
|
// Show a notification
|
|
this.showNotification(`Updated expiration to ${hours} hours (${(hours/24).toFixed(1)} days)`, 'success');
|
|
} else {
|
|
throw new Error(data.message || 'Unknown error updating expiration');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('Error updating stateful expiration:', error);
|
|
this.showNotification(`Failed to update expiration: ${error.message}`, 'error');
|
|
// Reset the UI
|
|
if (expiresDateEl) {
|
|
expiresDateEl.textContent = 'Error updating';
|
|
}
|
|
|
|
// Try to reload original data
|
|
setTimeout(() => this.loadStatefulInfo(), 1000);
|
|
});
|
|
},
|
|
|
|
// Add the updateStatefulExpiration method
|
|
updateStatefulExpiration: function(hours) {
|
|
if (!hours || typeof hours !== 'number' || hours <= 0) {
|
|
console.error('[huntarrUI] Invalid hours value for updateStatefulExpiration:', hours);
|
|
return;
|
|
}
|
|
|
|
console.log(`[huntarrUI] Directly updating stateful expiration to ${hours} hours`);
|
|
|
|
// Make a direct API call to update the stateful expiration
|
|
HuntarrUtils.fetchWithTimeout('./api/stateful/update-expiration', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ hours: hours })
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! Status: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
console.log('[huntarrUI] Stateful expiration updated successfully:', data);
|
|
// Update the expiration date display
|
|
const expiresDateEl = document.getElementById('stateful_expires_date');
|
|
if (expiresDateEl && data.expires_date) {
|
|
expiresDateEl.textContent = data.expires_date;
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error updating stateful expiration:', error);
|
|
});
|
|
},
|
|
|
|
// Add global event handler and method to track saved settings across all apps
|
|
// Auto-save enabled - unsaved changes handlers removed
|
|
|
|
// Add a proper hasFormChanges function to compare form values with original values
|
|
hasFormChanges: function(app) {
|
|
// If we don't have original settings or current app settings, we can't compare
|
|
if (!this.originalSettings || !this.originalSettings[app]) {
|
|
return false;
|
|
}
|
|
|
|
// Get current settings from the form
|
|
const currentSettings = this.getFormSettings(app);
|
|
|
|
// For complex objects like instances, we need to stringify them for comparison
|
|
const originalJSON = JSON.stringify(this.originalSettings[app]);
|
|
const currentJSON = JSON.stringify(currentSettings);
|
|
|
|
return originalJSON !== currentJSON;
|
|
},
|
|
|
|
// Check if Low Usage Mode is enabled in settings and apply it
|
|
checkLowUsageMode: function() {
|
|
return HuntarrUtils.fetchWithTimeout('./api/settings/general', {
|
|
method: 'GET'
|
|
})
|
|
.then(response => response.json())
|
|
.then(config => {
|
|
if (config && config.low_usage_mode === true) {
|
|
this.applyLowUsageMode(true);
|
|
} else {
|
|
this.applyLowUsageMode(false);
|
|
}
|
|
return config;
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error checking Low Usage Mode:', error);
|
|
// Default to disabled on error
|
|
this.applyLowUsageMode(false);
|
|
throw error;
|
|
});
|
|
},
|
|
|
|
// Apply Low Usage Mode effects based on setting
|
|
applyLowUsageMode: function(enabled) {
|
|
console.log(`[huntarrUI] Setting Low Usage Mode: ${enabled ? 'Enabled' : 'Disabled'}`);
|
|
|
|
// Store the previous state to detect changes
|
|
const wasEnabled = document.body.classList.contains('low-usage-mode');
|
|
|
|
if (enabled) {
|
|
// Add CSS class to body to disable animations
|
|
document.body.classList.add('low-usage-mode');
|
|
|
|
// Low Usage Mode now runs without any visual indicator for a cleaner interface
|
|
} else {
|
|
// Remove CSS class from body to enable animations
|
|
document.body.classList.remove('low-usage-mode');
|
|
}
|
|
|
|
// If low usage mode state changed and we have stats data, update the display
|
|
if (wasEnabled !== enabled && window.mediaStats) {
|
|
console.log(`[huntarrUI] Low usage mode changed from ${wasEnabled} to ${enabled}, updating stats display`);
|
|
this.updateStatsDisplay(window.mediaStats);
|
|
}
|
|
},
|
|
|
|
// Apply timezone change immediately
|
|
applyTimezoneChange: function(timezone) {
|
|
console.log(`[huntarrUI] Applying timezone change to: ${timezone}`);
|
|
|
|
// Call the backend to apply timezone immediately
|
|
fetch('./api/settings/apply-timezone', {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify({ timezone: timezone })
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.log('[huntarrUI] Timezone applied successfully');
|
|
// Settings auto-save notification removed per user request
|
|
|
|
// Refresh any time displays that might be affected
|
|
this.refreshTimeDisplays();
|
|
} else {
|
|
console.error('[huntarrUI] Failed to apply timezone:', data.error);
|
|
this.showNotification(`Failed to apply timezone: ${data.error}`, 'error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error applying timezone:', error);
|
|
this.showNotification(`Error applying timezone: ${error.message}`, 'error');
|
|
});
|
|
},
|
|
|
|
// Apply authentication mode change immediately
|
|
applyAuthModeChange: function(authMode) {
|
|
console.log(`[huntarrUI] Authentication mode changed to: ${authMode}`);
|
|
|
|
// Show notification about the change
|
|
const modeNames = {
|
|
'login': 'Login Mode',
|
|
'local_bypass': 'Local Bypass Mode',
|
|
'no_login': 'No Login Mode'
|
|
};
|
|
|
|
const modeName = modeNames[authMode] || authMode;
|
|
// Settings auto-save notification removed per user request
|
|
|
|
// Settings auto-save warning notification removed per user request
|
|
},
|
|
|
|
// Apply update checking change immediately
|
|
applyUpdateCheckingChange: function(enabled) {
|
|
console.log(`[huntarrUI] Update checking ${enabled ? 'enabled' : 'disabled'}`);
|
|
// Settings auto-save notification removed per user request
|
|
},
|
|
|
|
// Refresh time displays after timezone change
|
|
refreshTimeDisplays: function() {
|
|
console.log('[huntarrUI] Refreshing all time displays for timezone change');
|
|
|
|
// 1. Refresh logs module timezone cache and reload logs
|
|
if (window.LogsModule) {
|
|
console.log('[huntarrUI] Refreshing LogsModule timezone');
|
|
window.LogsModule.userTimezone = null; // Clear cache
|
|
window.LogsModule.loadUserTimezone(); // Reload timezone
|
|
|
|
// Force reload current logs to show new timezone
|
|
if (window.LogsModule.currentLogApp) {
|
|
console.log('[huntarrUI] Reloading logs with new timezone');
|
|
window.LogsModule.loadLogsFromAPI(window.LogsModule.currentLogApp);
|
|
}
|
|
}
|
|
|
|
// 2. Refresh cycle countdown timers
|
|
if (window.CycleCountdown) {
|
|
console.log('[huntarrUI] Refreshing CycleCountdown timers');
|
|
// Force refresh cycle data to get updated timestamps
|
|
window.CycleCountdown.refreshAllData();
|
|
}
|
|
|
|
// 3. Refresh scheduler timezone display if on scheduling page
|
|
if (this.currentSection === 'scheduling' || this.currentSection === 'schedules') {
|
|
console.log('[huntarrUI] Refreshing scheduling timezone display');
|
|
if (typeof loadServerTimezone === 'function') {
|
|
loadServerTimezone(); // Reload server timezone display
|
|
}
|
|
}
|
|
|
|
// 4. Refresh hunt manager if it's currently visible (timestamps in hunt entries)
|
|
if (this.currentSection === 'hunt-manager' && window.huntManagerModule) {
|
|
console.log('[huntarrUI] Refreshing hunt manager data');
|
|
if (typeof window.huntManagerModule.refresh === 'function') {
|
|
window.huntManagerModule.refresh();
|
|
}
|
|
}
|
|
|
|
// 5. Update any cached timezone settings in huntarrUI
|
|
if (this.originalSettings && this.originalSettings.general) {
|
|
// Update cached timezone to match the new setting
|
|
const timezoneSelect = document.getElementById('timezone');
|
|
if (timezoneSelect && timezoneSelect.value) {
|
|
this.originalSettings.general.timezone = timezoneSelect.value;
|
|
console.log('[huntarrUI] Updated cached timezone setting to:', timezoneSelect.value);
|
|
}
|
|
}
|
|
|
|
// 6. Refresh any time elements with custom refresh methods
|
|
const timeElements = document.querySelectorAll('[data-time], .time-display, .timestamp');
|
|
timeElements.forEach(element => {
|
|
if (element.dataset.refreshTime) {
|
|
console.log('[huntarrUI] Refreshing custom time display element');
|
|
// Custom refresh logic could go here
|
|
}
|
|
});
|
|
|
|
console.log('[huntarrUI] Time display refresh completed');
|
|
},
|
|
|
|
// Reset the app cycle for a specific app
|
|
resetAppCycle: function(app, button) {
|
|
// Make sure we have the app and button elements
|
|
if (!app || !button) {
|
|
console.error('[huntarrUI] Missing app or button for resetAppCycle');
|
|
return;
|
|
}
|
|
|
|
// First, disable the button to prevent multiple clicks
|
|
button.disabled = true;
|
|
button.innerHTML = '<i class="fas fa-circle-notch fa-spin"></i> Resetting...';
|
|
|
|
// API endpoint
|
|
const endpoint = `./api/cycle/reset/${app}`;
|
|
|
|
HuntarrUtils.fetchWithTimeout(endpoint, {
|
|
method: 'POST'
|
|
})
|
|
.then(response => {
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to reset ${app} cycle`);
|
|
}
|
|
return response.json();
|
|
})
|
|
.then(data => {
|
|
this.showNotification(`Successfully reset ${this.capitalizeFirst(app)} cycle`, 'success');
|
|
console.log(`[huntarrUI] Reset ${app} cycle response:`, data);
|
|
|
|
// Re-enable the button with original text
|
|
button.disabled = false;
|
|
button.innerHTML = `<i class="fas fa-sync-alt"></i> Reset`;
|
|
})
|
|
.catch(error => {
|
|
console.error(`[huntarrUI] Error resetting ${app} cycle:`, error);
|
|
this.showNotification(`Error resetting ${this.capitalizeFirst(app)} cycle: ${error.message}`, 'error');
|
|
|
|
// Re-enable the button with original text
|
|
button.disabled = false;
|
|
button.innerHTML = `<i class="fas fa-sync-alt"></i> Reset`;
|
|
});
|
|
},
|
|
|
|
// More robust low usage mode detection
|
|
isLowUsageModeEnabled: function() {
|
|
// Check multiple sources to determine if low usage mode is enabled
|
|
|
|
// 1. Check CSS class on body (primary method)
|
|
const hasLowUsageClass = document.body.classList.contains('low-usage-mode');
|
|
|
|
// 2. Check if the standalone low-usage-mode.js module is enabled
|
|
const standaloneModuleEnabled = window.LowUsageMode && window.LowUsageMode.isEnabled && window.LowUsageMode.isEnabled();
|
|
|
|
// 3. Final determination based on reliable sources (no indicator checking needed)
|
|
const isEnabled = hasLowUsageClass || standaloneModuleEnabled;
|
|
|
|
console.log(`[huntarrUI] Low usage mode detection - CSS class: ${hasLowUsageClass}, Module: ${standaloneModuleEnabled}, Final: ${isEnabled}`);
|
|
|
|
return isEnabled;
|
|
},
|
|
|
|
showDashboard: function() {
|
|
// Make the dashboard grid visible after initialization to prevent FOUC
|
|
const dashboardGrid = document.querySelector('.dashboard-grid');
|
|
if (dashboardGrid) {
|
|
dashboardGrid.style.opacity = '1';
|
|
console.log('[huntarrUI] Dashboard made visible after initialization');
|
|
} else {
|
|
console.warn('[huntarrUI] Dashboard grid not found');
|
|
}
|
|
},
|
|
|
|
applyFilterToSingleEntry: function(logEntry, selectedLevel) {
|
|
// Apply the same filtering logic used in filterLogsByLevel to a single entry
|
|
const levelBadge = logEntry.querySelector('.log-level-badge, .log-level, .log-level-error, .log-level-warning, .log-level-info, .log-level-debug');
|
|
|
|
// Clear any existing filter attribute first
|
|
logEntry.removeAttribute('data-hidden-by-filter');
|
|
|
|
if (levelBadge) {
|
|
// Get the level from the badge text
|
|
let entryLevel = '';
|
|
|
|
// Get badge text and normalize it
|
|
const badgeText = levelBadge.textContent.toLowerCase().trim();
|
|
|
|
// Fixed mapping - match the actual badge text created in log entries
|
|
switch(badgeText) {
|
|
case 'information':
|
|
case 'info':
|
|
entryLevel = 'info';
|
|
break;
|
|
case 'warning':
|
|
case 'warn':
|
|
entryLevel = 'warning';
|
|
break;
|
|
case 'error':
|
|
entryLevel = 'error';
|
|
break;
|
|
case 'debug':
|
|
entryLevel = 'debug';
|
|
break;
|
|
case 'fatal':
|
|
case 'critical':
|
|
entryLevel = 'error'; // Map fatal/critical to error for filtering
|
|
break;
|
|
default:
|
|
// Try class-based detection as secondary method
|
|
if (levelBadge.classList.contains('log-level-error')) {
|
|
entryLevel = 'error';
|
|
} else if (levelBadge.classList.contains('log-level-warning')) {
|
|
entryLevel = 'warning';
|
|
} else if (levelBadge.classList.contains('log-level-info')) {
|
|
entryLevel = 'info';
|
|
} else if (levelBadge.classList.contains('log-level-debug')) {
|
|
entryLevel = 'debug';
|
|
} else {
|
|
// NO FALLBACK - if we can't determine the level, hide it
|
|
entryLevel = null;
|
|
}
|
|
}
|
|
|
|
// Show or hide based on filter match, using data attributes for pagination cooperation
|
|
if (entryLevel && entryLevel === selectedLevel) {
|
|
logEntry.style.display = '';
|
|
} else {
|
|
logEntry.style.display = 'none';
|
|
logEntry.setAttribute('data-hidden-by-filter', 'true');
|
|
}
|
|
} else {
|
|
// If no level badge found, hide the entry when filtering
|
|
logEntry.style.display = 'none';
|
|
logEntry.setAttribute('data-hidden-by-filter', 'true');
|
|
}
|
|
},
|
|
|
|
filterLogsByLevel: function(selectedLevel) {
|
|
if (!this.elements.logsContainer) return;
|
|
|
|
const allLogEntries = this.elements.logsContainer.querySelectorAll('.log-entry');
|
|
let visibleCount = 0;
|
|
let totalCount = allLogEntries.length;
|
|
|
|
console.log(`[huntarrUI] Filtering logs by level: ${selectedLevel}, total entries: ${totalCount}`);
|
|
|
|
// Debug: Log first few badge texts to see what we're working with
|
|
allLogEntries.forEach((entry, index) => {
|
|
if (index < 5) { // Log first 5 entries for debugging
|
|
const levelBadge = entry.querySelector('.log-level-badge, .log-level, .log-level-error, .log-level-warning, .log-level-info, .log-level-debug');
|
|
if (levelBadge) {
|
|
console.log(`[huntarrUI] Entry ${index}: Badge text = "${levelBadge.textContent.trim()}", Classes = ${levelBadge.className}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Clear any existing filter attributes first
|
|
allLogEntries.forEach(entry => {
|
|
entry.removeAttribute('data-hidden-by-filter');
|
|
});
|
|
|
|
allLogEntries.forEach(entry => {
|
|
if (selectedLevel === 'all') {
|
|
// Show all entries - remove any filter hiding
|
|
entry.style.display = '';
|
|
visibleCount++;
|
|
} else {
|
|
// Check if this entry matches the selected level
|
|
const levelBadge = entry.querySelector('.log-level-badge, .log-level, .log-level-error, .log-level-warning, .log-level-info, .log-level-debug');
|
|
|
|
if (levelBadge) {
|
|
// Get the level from the badge text
|
|
let entryLevel = '';
|
|
|
|
// Get badge text and normalize it
|
|
const badgeText = levelBadge.textContent.toLowerCase().trim();
|
|
|
|
// Fixed mapping - match the actual badge text created in log entries
|
|
switch(badgeText) {
|
|
case 'information':
|
|
case 'info':
|
|
entryLevel = 'info';
|
|
break;
|
|
case 'warning':
|
|
case 'warn':
|
|
entryLevel = 'warning';
|
|
break;
|
|
case 'error':
|
|
entryLevel = 'error';
|
|
break;
|
|
case 'debug':
|
|
entryLevel = 'debug';
|
|
break;
|
|
case 'fatal':
|
|
case 'critical':
|
|
entryLevel = 'error'; // Map fatal/critical to error for filtering
|
|
break;
|
|
default:
|
|
// Try class-based detection as secondary method
|
|
if (levelBadge.classList.contains('log-level-error')) {
|
|
entryLevel = 'error';
|
|
} else if (levelBadge.classList.contains('log-level-warning')) {
|
|
entryLevel = 'warning';
|
|
} else if (levelBadge.classList.contains('log-level-info')) {
|
|
entryLevel = 'info';
|
|
} else if (levelBadge.classList.contains('log-level-debug')) {
|
|
entryLevel = 'debug';
|
|
} else {
|
|
// Log unmapped badge text for debugging
|
|
console.log(`[huntarrUI] Unmapped badge text: "${badgeText}" - hiding entry`);
|
|
entryLevel = null; // Set to null to indicate unmapped
|
|
}
|
|
}
|
|
|
|
// Show or hide based on filter match, using data attributes for pagination cooperation
|
|
if (entryLevel && entryLevel === selectedLevel) {
|
|
entry.style.display = '';
|
|
visibleCount++;
|
|
} else {
|
|
entry.style.display = 'none';
|
|
entry.setAttribute('data-hidden-by-filter', 'true');
|
|
}
|
|
} else {
|
|
// If no level badge found, hide the entry when filtering
|
|
entry.style.display = 'none';
|
|
entry.setAttribute('data-hidden-by-filter', 'true');
|
|
}
|
|
}
|
|
});
|
|
|
|
// Pagination controls remain visible at all times - removed hiding logic
|
|
|
|
// Auto-scroll to top to show newest entries (logs are in reverse order)
|
|
if (this.autoScroll && this.elements.autoScrollCheckbox && this.elements.autoScrollCheckbox.checked && visibleCount > 0) {
|
|
setTimeout(() => {
|
|
window.scrollTo({
|
|
top: 0,
|
|
behavior: 'smooth'
|
|
});
|
|
}, 100);
|
|
}
|
|
|
|
console.log(`[huntarrUI] Filtered logs by level '${selectedLevel}': showing ${visibleCount}/${totalCount} entries`);
|
|
},
|
|
|
|
// Helper method to detect JSON fragments that shouldn't be displayed as log entries
|
|
isJsonFragment: function(logString) {
|
|
if (!logString || typeof logString !== 'string') return false;
|
|
|
|
const trimmed = logString.trim();
|
|
|
|
// Check for common JSON fragment patterns
|
|
const jsonPatterns = [
|
|
/^"[^"]*":\s*"[^"]*",?$/, // "key": "value",
|
|
/^"[^"]*":\s*\d+,?$/, // "key": 123,
|
|
/^"[^"]*":\s*true|false,?$/, // "key": true,
|
|
/^"[^"]*":\s*null,?$/, // "key": null,
|
|
/^"[^"]*":\s*\[[^\]]*\],?$/, // "key": [...],
|
|
/^"[^"]*":\s*\{[^}]*\},?$/, // "key": {...},
|
|
/^\s*\{?\s*$/, // Just opening brace or whitespace
|
|
/^\s*\}?,?\s*$/, // Just closing brace
|
|
/^\s*\[?\s*$/, // Just opening bracket
|
|
/^\s*\]?,?\s*$/, // Just closing bracket
|
|
/^,?\s*$/, // Just comma or whitespace
|
|
/^[^"]*':\s*[^,]*,.*':/, // Mid-object fragments like "g_items': 1, 'hunt_upgrade_items': 0"
|
|
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*\d+,/, // Property names starting without quotes
|
|
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*True|False,/, // Boolean properties without opening quotes
|
|
/^[a-zA-Z_][a-zA-Z0-9_]*':\s*'[^']*',/, // String properties without opening quotes
|
|
/.*':\s*\d+,.*':\s*\d+,/, // Multiple numeric properties in sequence
|
|
/.*':\s*True,.*':\s*False,/, // Multiple boolean properties in sequence
|
|
/.*':\s*'[^']*',.*':\s*'[^']*',/, // Multiple string properties in sequence
|
|
/^"[^"]*":\s*\[$/, // JSON key with opening bracket: "global": [
|
|
/^[a-zA-Z_][a-zA-Z0-9_\s]*:\s*\[$/, // Property key with opening bracket: global: [
|
|
/^[a-zA-Z_][a-zA-Z0-9_\s]*:\s*\{$/, // Property key with opening brace: config: {
|
|
/^[a-zA-Z_]+\s+(Mode|Setting|Config|Option):\s*(True|False|\d+)$/i, // Config fragments: "ug Mode: False"
|
|
/^[a-zA-Z_]+\s*Mode:\s*(True|False)$/i, // Mode fragments: "Debug Mode: False"
|
|
/^[a-zA-Z_]+\s*Setting:\s*.*$/i, // Setting fragments
|
|
/^[a-zA-Z_]+\s*Config:\s*.*$/i // Config fragments
|
|
];
|
|
|
|
return jsonPatterns.some(pattern => pattern.test(trimmed));
|
|
},
|
|
|
|
// Helper method to detect other invalid log lines
|
|
isInvalidLogLine: function(logString) {
|
|
if (!logString || typeof logString !== 'string') return true;
|
|
|
|
const trimmed = logString.trim();
|
|
|
|
// Skip empty lines or lines with only whitespace
|
|
if (trimmed.length === 0) return true;
|
|
|
|
// Skip lines that are clearly not log entries
|
|
if (trimmed.length < 10) return true; // Too short to be a meaningful log
|
|
|
|
// Skip lines that look like HTTP headers or other metadata
|
|
if (/^(HTTP\/|Content-|Connection:|Host:|User-Agent:)/i.test(trimmed)) return true;
|
|
|
|
// Skip partial words or fragments that don't form complete sentences
|
|
if (/^[a-zA-Z]{1,5}\s+(Mode|Setting|Config|Debug|Info|Error|Warning):/i.test(trimmed)) return true;
|
|
|
|
// Skip single words that are clearly fragments
|
|
if (/^[a-zA-Z]{1,8}$/i.test(trimmed)) return true;
|
|
|
|
// Skip lines that start with partial words and contain colons (config fragments)
|
|
if (/^[a-z]{1,8}\s*[A-Z]/i.test(trimmed) && trimmed.includes(':')) return true;
|
|
|
|
return false;
|
|
},
|
|
|
|
// Load instance-specific state management information
|
|
loadInstanceStateInfo: function(appType, instanceIndex) {
|
|
const supportedApps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros'];
|
|
if (!supportedApps.includes(appType)) return;
|
|
|
|
// Try multiple methods to get the correct instance name
|
|
let instanceName = null;
|
|
|
|
// Method 1: Try the name input field
|
|
const instanceNameElement = document.getElementById(`${appType}-name-${instanceIndex}`);
|
|
if (instanceNameElement && instanceNameElement.value && instanceNameElement.value.trim()) {
|
|
instanceName = instanceNameElement.value.trim();
|
|
}
|
|
|
|
// Method 2: Try to get from the instance header/title
|
|
if (!instanceName) {
|
|
const instanceHeader = document.querySelector(`#${appType}-instance-${instanceIndex} h3, #${appType}-instance-${instanceIndex} .instance-title`);
|
|
if (instanceHeader && instanceHeader.textContent) {
|
|
// Extract instance name from header text like "Instance 1: Default" or "Instance 2: EP Mode"
|
|
const headerText = instanceHeader.textContent.trim();
|
|
const match = headerText.match(/Instance \d+:\s*(.+)$/);
|
|
if (match && match[1]) {
|
|
instanceName = match[1].trim();
|
|
}
|
|
}
|
|
}
|
|
|
|
// Method 3: Fallback to Default for first instance, descriptive name for others
|
|
if (!instanceName) {
|
|
instanceName = instanceIndex === 0 ? 'Default' : `Instance ${instanceIndex + 1}`;
|
|
}
|
|
|
|
const hoursInput = document.getElementById(`${appType}-state-management-hours-${instanceIndex}`);
|
|
const customHours = parseInt(hoursInput?.value) || 168;
|
|
|
|
console.log(`[huntarrUI] Loading state info for ${appType}/${instanceName} (index ${instanceIndex})`);
|
|
|
|
// Load state information for this specific instance using per-instance API
|
|
HuntarrUtils.fetchWithTimeout(`./api/stateful/summary?app_type=${appType}&instance_name=${encodeURIComponent(instanceName)}`, {
|
|
method: 'GET'
|
|
})
|
|
.then(response => response.json())
|
|
.then(summaryData => {
|
|
console.log(`[huntarrUI] State data for ${appType}/${instanceName}:`, summaryData);
|
|
this.updateInstanceStateDisplay(appType, instanceIndex, summaryData, instanceName, customHours);
|
|
})
|
|
.catch(error => {
|
|
console.error(`[huntarrUI] Error loading state info for ${appType}/${instanceName} (index ${instanceIndex}):`, error);
|
|
// Fallback to default display
|
|
this.updateInstanceStateDisplay(appType, instanceIndex, null, instanceName, customHours);
|
|
});
|
|
},
|
|
|
|
// Update the instance state management display
|
|
updateInstanceStateDisplay: function(appType, instanceIndex, summaryData, instanceName, customHours) {
|
|
const resetTimeElement = document.getElementById(`${appType}-state-reset-time-${instanceIndex}`);
|
|
const itemsCountElement = document.getElementById(`${appType}-state-items-count-${instanceIndex}`);
|
|
|
|
// Update reset time from server data ONLY - no fallback calculations
|
|
if (resetTimeElement) {
|
|
if (summaryData && summaryData.next_reset_time) {
|
|
resetTimeElement.textContent = summaryData.next_reset_time;
|
|
} else {
|
|
// Show error state - server should always provide this
|
|
console.error(`[huntarrUI] No next_reset_time provided by server for ${appType}/${instanceName}`);
|
|
resetTimeElement.textContent = 'Error loading time';
|
|
}
|
|
}
|
|
|
|
// Update processed items count
|
|
if (itemsCountElement) {
|
|
const count = summaryData ? (summaryData.processed_count || 0) : 0;
|
|
itemsCountElement.textContent = count.toString();
|
|
}
|
|
},
|
|
|
|
// Refresh state management timezone displays when timezone changes
|
|
refreshStateManagementTimezone: function() {
|
|
console.log('[huntarrUI] Refreshing state management timezone displays due to settings change');
|
|
|
|
try {
|
|
// Simply reload the displays - the backend will use the new timezone automatically
|
|
this.reloadStateManagementDisplays();
|
|
|
|
} catch (error) {
|
|
console.error('[huntarrUI] Error refreshing state management timezone:', error);
|
|
}
|
|
},
|
|
|
|
// Reload state management displays after timezone change
|
|
reloadStateManagementDisplays: function() {
|
|
console.log('[huntarrUI] Reloading state management displays after timezone change');
|
|
|
|
// Refresh all visible state management displays
|
|
const supportedApps = ['sonarr', 'radarr', 'lidarr', 'readarr', 'whisparr', 'eros'];
|
|
|
|
supportedApps.forEach(appType => {
|
|
// Find all instance containers for this app
|
|
const appPanel = document.getElementById(`${appType}-panel`);
|
|
if (appPanel && appPanel.style.display !== 'none') {
|
|
// Look for state reset time elements
|
|
const stateElements = appPanel.querySelectorAll(`[id*="${appType}-state-reset-time-"]`);
|
|
|
|
stateElements.forEach(element => {
|
|
// Extract instance index from element ID
|
|
const match = element.id.match(/(\w+)-state-reset-time-(\d+)/);
|
|
if (match) {
|
|
const instanceIndex = parseInt(match[2]);
|
|
|
|
// Get instance name from the form
|
|
const instanceNameElement = document.querySelector(`#${appType}-instance-name-${instanceIndex}`);
|
|
if (instanceNameElement) {
|
|
const instanceName = instanceNameElement.value || 'Default';
|
|
|
|
console.log(`[huntarrUI] Reloading state management for ${appType} instance ${instanceIndex} (${instanceName})`);
|
|
|
|
// Fetch fresh state management data
|
|
this.loadStateManagementForInstance(appType, instanceIndex, instanceName);
|
|
}
|
|
}
|
|
});
|
|
}
|
|
});
|
|
|
|
console.log('[huntarrUI] State management timezone refresh completed');
|
|
},
|
|
|
|
// Load state management data for a specific instance
|
|
loadStateManagementForInstance: function(appType, instanceIndex, instanceName) {
|
|
const url = `./api/stateful/summary?app_type=${encodeURIComponent(appType)}&instance_name=${encodeURIComponent(instanceName)}`;
|
|
|
|
HuntarrUtils.fetchWithTimeout(url, {
|
|
method: 'GET'
|
|
})
|
|
.then(response => response.json())
|
|
.then(data => {
|
|
if (data.success) {
|
|
console.log(`[huntarrUI] Received updated state management data for ${appType}/${instanceName}:`, data);
|
|
|
|
// Update the display with the new timezone-converted data
|
|
this.updateInstanceStateDisplay(appType, instanceIndex, data, instanceName, data.expiration_hours);
|
|
} else {
|
|
console.warn(`[huntarrUI] Failed to get state management data for ${appType}/${instanceName}:`, data.message || 'Unknown error');
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error(`[huntarrUI] Error loading state management data for ${appType}/${instanceName}:`, error);
|
|
});
|
|
},
|
|
|
|
showRequestarrSidebar: function() {
|
|
// Hide main sidebar and settings sidebar
|
|
const mainSidebar = document.getElementById('sidebar');
|
|
const requestarrSidebar = document.getElementById('requestarr-sidebar');
|
|
const settingsSidebar = document.getElementById('settings-sidebar');
|
|
|
|
if (mainSidebar) mainSidebar.style.display = 'none';
|
|
if (settingsSidebar) settingsSidebar.style.display = 'none';
|
|
if (requestarrSidebar) requestarrSidebar.style.display = 'block';
|
|
|
|
// Update active states in Requestarr sidebar
|
|
this.updateRequestarrSidebarActive();
|
|
},
|
|
|
|
showRequestarrView: function(view) {
|
|
// Hide all Requestarr views
|
|
const homeView = document.getElementById('requestarr-home-view');
|
|
const historyView = document.getElementById('requestarr-history-view');
|
|
|
|
if (homeView) homeView.style.display = 'none';
|
|
if (historyView) historyView.style.display = 'none';
|
|
|
|
// Show selected view
|
|
if (view === 'home' && homeView) {
|
|
homeView.style.display = 'block';
|
|
} else if (view === 'history' && historyView) {
|
|
historyView.style.display = 'block';
|
|
}
|
|
|
|
// Update navigation states
|
|
this.updateRequestarrNavigation(view);
|
|
},
|
|
|
|
showMainSidebar: function() {
|
|
console.log('[huntarrUI] showMainSidebar called');
|
|
|
|
// Show main sidebar
|
|
const mainSidebar = document.getElementById('sidebar');
|
|
const requestarrSidebar = document.getElementById('requestarr-sidebar');
|
|
const settingsSidebar = document.getElementById('settings-sidebar');
|
|
|
|
console.log('[huntarrUI] Sidebar elements found:', {
|
|
main: !!mainSidebar,
|
|
requestarr: !!requestarrSidebar,
|
|
settings: !!settingsSidebar
|
|
});
|
|
|
|
if (mainSidebar) {
|
|
mainSidebar.style.display = 'block';
|
|
mainSidebar.style.setProperty('display', 'block', 'important');
|
|
}
|
|
if (requestarrSidebar) {
|
|
requestarrSidebar.style.display = 'none';
|
|
requestarrSidebar.style.setProperty('display', 'none', 'important');
|
|
}
|
|
if (settingsSidebar) {
|
|
settingsSidebar.style.display = 'none';
|
|
settingsSidebar.style.setProperty('display', 'none', 'important');
|
|
}
|
|
|
|
console.log('[huntarrUI] Sidebar styles applied');
|
|
|
|
// Clear Settings sidebar preference when showing main sidebar
|
|
localStorage.removeItem('huntarr-settings-sidebar');
|
|
},
|
|
|
|
showMainSidebar: function() {
|
|
// Remove flash prevention style if it exists
|
|
const flashPreventionStyle = document.getElementById('sidebar-flash-prevention');
|
|
if (flashPreventionStyle) {
|
|
flashPreventionStyle.remove();
|
|
}
|
|
|
|
// Show main sidebar and hide others
|
|
const mainSidebar = document.getElementById('sidebar');
|
|
const requestarrSidebar = document.getElementById('requestarr-sidebar');
|
|
const settingsSidebar = document.getElementById('settings-sidebar');
|
|
const appsSidebar = document.getElementById('apps-sidebar');
|
|
|
|
if (mainSidebar) mainSidebar.style.display = 'block';
|
|
if (requestarrSidebar) requestarrSidebar.style.display = 'none';
|
|
if (settingsSidebar) settingsSidebar.style.display = 'none';
|
|
if (appsSidebar) appsSidebar.style.display = 'none';
|
|
|
|
console.log('[huntarrUI] Main sidebar shown');
|
|
|
|
// Clear Settings sidebar preference when showing main sidebar
|
|
localStorage.removeItem('huntarr-settings-sidebar');
|
|
},
|
|
|
|
showRequestarrSidebar: function() {
|
|
// Remove flash prevention style if it exists
|
|
const flashPreventionStyle = document.getElementById('sidebar-flash-prevention');
|
|
if (flashPreventionStyle) {
|
|
flashPreventionStyle.remove();
|
|
}
|
|
|
|
// Hide main sidebar and show requestarr sidebar
|
|
const mainSidebar = document.getElementById('sidebar');
|
|
const requestarrSidebar = document.getElementById('requestarr-sidebar');
|
|
const settingsSidebar = document.getElementById('settings-sidebar');
|
|
const appsSidebar = document.getElementById('apps-sidebar');
|
|
|
|
if (mainSidebar) mainSidebar.style.display = 'none';
|
|
if (requestarrSidebar) requestarrSidebar.style.display = 'block';
|
|
if (settingsSidebar) settingsSidebar.style.display = 'none';
|
|
if (appsSidebar) appsSidebar.style.display = 'none';
|
|
|
|
// Update active states in Requestarr sidebar
|
|
this.updateRequestarrSidebarActive();
|
|
},
|
|
|
|
showAppsSidebar: function() {
|
|
// Remove flash prevention style if it exists
|
|
const flashPreventionStyle = document.getElementById('sidebar-flash-prevention');
|
|
if (flashPreventionStyle) {
|
|
flashPreventionStyle.remove();
|
|
}
|
|
|
|
// Hide main sidebar and show apps sidebar
|
|
const mainSidebar = document.getElementById('sidebar');
|
|
const requestarrSidebar = document.getElementById('requestarr-sidebar');
|
|
const settingsSidebar = document.getElementById('settings-sidebar');
|
|
const appsSidebar = document.getElementById('apps-sidebar');
|
|
|
|
if (mainSidebar) mainSidebar.style.display = 'none';
|
|
if (requestarrSidebar) requestarrSidebar.style.display = 'none';
|
|
if (settingsSidebar) settingsSidebar.style.display = 'none';
|
|
if (appsSidebar) appsSidebar.style.display = 'block';
|
|
|
|
// Update active states in Apps sidebar
|
|
this.updateAppsSidebarActive();
|
|
},
|
|
|
|
showSettingsSidebar: function() {
|
|
// Remove flash prevention style if it exists
|
|
const flashPreventionStyle = document.getElementById('sidebar-flash-prevention');
|
|
if (flashPreventionStyle) {
|
|
flashPreventionStyle.remove();
|
|
}
|
|
|
|
// Hide main sidebar and show settings sidebar
|
|
const mainSidebar = document.getElementById('sidebar');
|
|
const requestarrSidebar = document.getElementById('requestarr-sidebar');
|
|
const settingsSidebar = document.getElementById('settings-sidebar');
|
|
const appsSidebar = document.getElementById('apps-sidebar');
|
|
|
|
if (mainSidebar) mainSidebar.style.display = 'none';
|
|
if (requestarrSidebar) requestarrSidebar.style.display = 'none';
|
|
if (appsSidebar) appsSidebar.style.display = 'none';
|
|
if (settingsSidebar) settingsSidebar.style.display = 'block';
|
|
|
|
// Update active states in Settings sidebar
|
|
this.updateSettingsSidebarActive();
|
|
},
|
|
|
|
updateRequestarrSidebarActive: function() {
|
|
// Remove active from all Requestarr nav items
|
|
const requestarrNavItems = document.querySelectorAll('#requestarr-sidebar .nav-item');
|
|
requestarrNavItems.forEach(item => item.classList.remove('active'));
|
|
|
|
// Set appropriate active state based on current section
|
|
if (this.currentSection === 'requestarr') {
|
|
const homeNav = document.getElementById('requestarrHomeNav');
|
|
if (homeNav) homeNav.classList.add('active');
|
|
} else if (this.currentSection === 'requestarr-history') {
|
|
const historyNav = document.getElementById('requestarrHistoryNav');
|
|
if (historyNav) historyNav.classList.add('active');
|
|
}
|
|
},
|
|
|
|
updateRequestarrNavigation: function(view) {
|
|
// Remove active from all Requestarr nav items
|
|
const requestarrNavItems = document.querySelectorAll('#requestarr-sidebar .nav-item');
|
|
requestarrNavItems.forEach(item => item.classList.remove('active'));
|
|
|
|
// Set active state based on view
|
|
if (view === 'home') {
|
|
const homeNav = document.getElementById('requestarrHomeNav');
|
|
if (homeNav) homeNav.classList.add('active');
|
|
} else if (view === 'history') {
|
|
const historyNav = document.getElementById('requestarrHistoryNav');
|
|
if (historyNav) historyNav.classList.add('active');
|
|
}
|
|
},
|
|
|
|
setupRequestarrNavigation: function() {
|
|
// Return button - goes back to main Huntarr
|
|
const returnNav = document.getElementById('requestarrReturnNav');
|
|
if (returnNav) {
|
|
returnNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#home';
|
|
});
|
|
}
|
|
|
|
// Home button - shows Requestarr home
|
|
const homeNav = document.getElementById('requestarrHomeNav');
|
|
if (homeNav) {
|
|
homeNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#requestarr';
|
|
});
|
|
}
|
|
|
|
// History button - shows Requestarr history
|
|
const historyNav = document.getElementById('requestarrHistoryNav');
|
|
if (historyNav) {
|
|
historyNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#requestarr-history';
|
|
});
|
|
}
|
|
},
|
|
|
|
updateAppsSidebarActive: function() {
|
|
// Remove active from all Apps nav items
|
|
const appsNavItems = document.querySelectorAll('#apps-sidebar .nav-item');
|
|
appsNavItems.forEach(item => item.classList.remove('active'));
|
|
|
|
// Set appropriate active state based on current section
|
|
if (this.currentSection === 'sonarr') {
|
|
const sonarrNav = document.getElementById('appsSonarrNav');
|
|
if (sonarrNav) sonarrNav.classList.add('active');
|
|
} else if (this.currentSection === 'radarr') {
|
|
const radarrNav = document.getElementById('appsRadarrNav');
|
|
if (radarrNav) radarrNav.classList.add('active');
|
|
} else if (this.currentSection === 'lidarr') {
|
|
const lidarrNav = document.getElementById('appsLidarrNav');
|
|
if (lidarrNav) lidarrNav.classList.add('active');
|
|
} else if (this.currentSection === 'readarr') {
|
|
const readarrNav = document.getElementById('appsReadarrNav');
|
|
if (readarrNav) readarrNav.classList.add('active');
|
|
} else if (this.currentSection === 'whisparr') {
|
|
const whisparrNav = document.getElementById('appsWhisparrNav');
|
|
if (whisparrNav) whisparrNav.classList.add('active');
|
|
} else if (this.currentSection === 'eros') {
|
|
const erosNav = document.getElementById('appsErosNav');
|
|
if (erosNav) erosNav.classList.add('active');
|
|
}
|
|
},
|
|
|
|
updateSettingsSidebarActive: function() {
|
|
// Remove active from all Settings nav items
|
|
const settingsNavItems = document.querySelectorAll('#settings-sidebar .nav-item');
|
|
settingsNavItems.forEach(item => item.classList.remove('active'));
|
|
|
|
// Set appropriate active state based on current section
|
|
if (this.currentSection === 'settings') {
|
|
const mainNav = document.getElementById('settingsMainNav');
|
|
if (mainNav) mainNav.classList.add('active');
|
|
} else if (this.currentSection === 'scheduling') {
|
|
const schedulingNav = document.getElementById('settingsSchedulingNav');
|
|
if (schedulingNav) schedulingNav.classList.add('active');
|
|
} else if (this.currentSection === 'notifications') {
|
|
const notificationsNav = document.getElementById('settingsNotificationsNav');
|
|
if (notificationsNav) notificationsNav.classList.add('active');
|
|
} else if (this.currentSection === 'user') {
|
|
const userNav = document.getElementById('settingsUserNav');
|
|
if (userNav) userNav.classList.add('active');
|
|
}
|
|
},
|
|
|
|
setupAppsNavigation: function() {
|
|
// Return button - goes back to main Huntarr
|
|
const returnNav = document.getElementById('appsReturnNav');
|
|
if (returnNav) {
|
|
returnNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#home';
|
|
});
|
|
}
|
|
|
|
// Sonarr button
|
|
const sonarrNav = document.getElementById('appsSonarrNav');
|
|
if (sonarrNav) {
|
|
sonarrNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#sonarr';
|
|
});
|
|
}
|
|
|
|
// Radarr button
|
|
const radarrNav = document.getElementById('appsRadarrNav');
|
|
if (radarrNav) {
|
|
radarrNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#radarr';
|
|
});
|
|
}
|
|
|
|
// Lidarr button
|
|
const lidarrNav = document.getElementById('appsLidarrNav');
|
|
if (lidarrNav) {
|
|
lidarrNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#lidarr';
|
|
});
|
|
}
|
|
|
|
// Readarr button
|
|
const readarrNav = document.getElementById('appsReadarrNav');
|
|
if (readarrNav) {
|
|
readarrNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#readarr';
|
|
});
|
|
}
|
|
|
|
// Whisparr button
|
|
const whisparrNav = document.getElementById('appsWhisparrNav');
|
|
if (whisparrNav) {
|
|
whisparrNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#whisparr';
|
|
});
|
|
}
|
|
|
|
// Eros button
|
|
const erosNav = document.getElementById('appsErosNav');
|
|
if (erosNav) {
|
|
erosNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#eros';
|
|
});
|
|
}
|
|
},
|
|
|
|
setupSettingsNavigation: function() {
|
|
// Return button - goes back to main Huntarr
|
|
const returnNav = document.getElementById('settingsReturnNav');
|
|
if (returnNav) {
|
|
returnNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
// Clear Settings sidebar preference when returning to main
|
|
localStorage.removeItem('huntarr-settings-sidebar');
|
|
window.location.hash = '#home';
|
|
});
|
|
}
|
|
|
|
// Main button - shows Settings main page
|
|
const mainNav = document.getElementById('settingsMainNav');
|
|
if (mainNav) {
|
|
mainNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#settings';
|
|
});
|
|
}
|
|
|
|
// Scheduling button - shows Scheduling page
|
|
const schedulingNav = document.getElementById('settingsSchedulingNav');
|
|
if (schedulingNav) {
|
|
schedulingNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#scheduling';
|
|
});
|
|
}
|
|
|
|
// Notifications button - shows Notifications page
|
|
const notificationsNav = document.getElementById('settingsNotificationsNav');
|
|
if (notificationsNav) {
|
|
notificationsNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#notifications';
|
|
});
|
|
}
|
|
|
|
// User button - shows User page
|
|
const userNav = document.getElementById('settingsUserNav');
|
|
if (userNav) {
|
|
userNav.addEventListener('click', (e) => {
|
|
e.preventDefault();
|
|
window.location.hash = '#user';
|
|
});
|
|
}
|
|
},
|
|
|
|
initializeSettings: function() {
|
|
// Check if settings are already initialized
|
|
const generalSettings = document.getElementById('generalSettings');
|
|
if (!generalSettings || generalSettings.innerHTML.trim() !== '') {
|
|
return; // Already initialized
|
|
}
|
|
|
|
// Load settings from API and generate the form
|
|
fetch('./api/settings')
|
|
.then(response => response.json())
|
|
.then(settings => {
|
|
console.log('[huntarrUI] Loaded settings:', settings);
|
|
console.log('[huntarrUI] General settings:', settings.general);
|
|
|
|
// Generate the general settings form - pass only the general settings
|
|
if (typeof SettingsForms !== 'undefined' && SettingsForms.generateGeneralForm) {
|
|
SettingsForms.generateGeneralForm(generalSettings, settings.general || {});
|
|
} else {
|
|
console.error('[huntarrUI] SettingsForms not available');
|
|
generalSettings.innerHTML = '<p>Error: Settings forms not loaded</p>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error loading settings:', error);
|
|
generalSettings.innerHTML = '<p>Error loading settings</p>';
|
|
});
|
|
},
|
|
|
|
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('<!-- Notifications content will be loaded here -->')) {
|
|
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 = '<p>Error: Notifications forms not loaded</p>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error loading notifications settings:', error);
|
|
notificationsContainer.innerHTML = '<p>Error loading notifications settings</p>';
|
|
});
|
|
},
|
|
|
|
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('<!-- Prowlarr content will be loaded here -->')) {
|
|
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 = '<p>Error: Prowlarr forms not loaded</p>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error loading prowlarr settings:', error);
|
|
prowlarrContainer.innerHTML = '<p>Error loading prowlarr settings</p>';
|
|
});
|
|
},
|
|
|
|
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('<!-- Swaparr settings content will be shown here -->')) {
|
|
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 = '<p>Error: Swaparr forms not loaded</p>';
|
|
}
|
|
})
|
|
.catch(error => {
|
|
console.error('[huntarrUI] Error loading Swaparr settings:', error);
|
|
swaparrContainer.innerHTML = '<p>Error loading Swaparr settings</p>';
|
|
});
|
|
},
|
|
|
|
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');
|
|
}
|
|
};
|