+
+
+
+ 0 / ${totalDisplay} directories
+
+
+
+ 0
+ Scanned
+
+
+ 0
+ Series Found
+
+
+
+ Current:
+ ${this.escapeHtml(data?.directory || 'Initializing...')}
+
+
+
+ 0.0s
+
+
+ `;
+
+ document.body.appendChild(overlay);
+
+ // Add click-outside-to-close handler
+ overlay.addEventListener('click', (e) => {
+ // Only close if clicking the overlay background, not the container
+ if (e.target === overlay) {
+ this.removeScanProgressOverlay();
+ }
+ });
+
+ // Trigger animation by adding visible class after a brief delay
+ requestAnimationFrame(() => {
+ overlay.classList.add('visible');
+ });
+ }
+
+ /**
+ * Update the scan progress overlay with current progress
+ * @param {Object} data - Scan progress event data
+ */
+ updateScanProgressOverlay(data) {
+ const overlay = document.getElementById('scan-progress-overlay');
+ if (!overlay) return;
+
+ // Update total items if provided (in case it wasn't available at start)
+ if (data.total_items && data.total_items > 0) {
+ this.scanTotalItems = data.total_items;
+ const totalCount = document.getElementById('scan-total-count');
+ if (totalCount) {
+ totalCount.textContent = this.scanTotalItems;
+ }
+ }
+
+ // Update progress bar
+ const progressBar = document.getElementById('scan-progress-bar');
+ if (progressBar && this.scanTotalItems > 0 && data.directories_scanned !== undefined) {
+ const percentage = Math.min(100, (data.directories_scanned / this.scanTotalItems) * 100);
+ progressBar.style.width = `${percentage}%`;
+ }
+
+ // Update current/total count display
+ const currentCount = document.getElementById('scan-current-count');
+ if (currentCount && data.directories_scanned !== undefined) {
+ currentCount.textContent = data.directories_scanned;
+ }
+
+ // Update directories count
+ const dirCount = document.getElementById('scan-directories-count');
+ if (dirCount && data.directories_scanned !== undefined) {
+ dirCount.textContent = data.directories_scanned;
+ }
+
+ // Update files/series count
+ const filesCount = document.getElementById('scan-files-count');
+ if (filesCount && data.files_found !== undefined) {
+ filesCount.textContent = data.files_found;
+ }
+
+ // Update current directory (truncate if too long)
+ const currentPath = document.getElementById('scan-current-path');
+ if (currentPath && data.current_directory) {
+ const maxLength = 50;
+ let displayPath = data.current_directory;
+ if (displayPath.length > maxLength) {
+ displayPath = '...' + displayPath.slice(-maxLength + 3);
+ }
+ currentPath.textContent = displayPath;
+ currentPath.title = data.current_directory; // Full path on hover
+ }
+ }
+
+ /**
+ * Hide the scan progress overlay with completion summary
+ * @param {Object} data - Scan completed event data
+ */
+ hideScanProgressOverlay(data) {
+ const overlay = document.getElementById('scan-progress-overlay');
+ if (!overlay) return;
+
+ const container = overlay.querySelector('.scan-progress-container');
+ if (container) {
+ container.classList.add('completed');
+ }
+
+ // Update title
+ const titleText = overlay.querySelector('.scan-title-text');
+ if (titleText) {
+ titleText.textContent = 'Scan Complete';
+ }
+
+ // Complete the progress bar
+ const progressBar = document.getElementById('scan-progress-bar');
+ if (progressBar) {
+ progressBar.style.width = '100%';
+ }
+
+ // Update final stats
+ if (data) {
+ const dirCount = document.getElementById('scan-directories-count');
+ if (dirCount && data.total_directories !== undefined) {
+ dirCount.textContent = data.total_directories;
+ }
+
+ const filesCount = document.getElementById('scan-files-count');
+ if (filesCount && data.total_files !== undefined) {
+ filesCount.textContent = data.total_files;
+ }
+
+ // Update progress text to show final count
+ const currentCount = document.getElementById('scan-current-count');
+ const totalCount = document.getElementById('scan-total-count');
+ if (currentCount && data.total_directories !== undefined) {
+ currentCount.textContent = data.total_directories;
+ }
+ if (totalCount && data.total_directories !== undefined) {
+ totalCount.textContent = data.total_directories;
+ }
+
+ // Show elapsed time
+ const elapsedTimeEl = document.getElementById('scan-elapsed-time');
+ const elapsedValueEl = document.getElementById('scan-elapsed-value');
+ if (elapsedTimeEl && elapsedValueEl && data.elapsed_seconds !== undefined) {
+ elapsedValueEl.textContent = `${data.elapsed_seconds.toFixed(1)}s`;
+ elapsedTimeEl.classList.remove('hidden');
+ }
+
+ // Update current directory to show completion message
+ const currentPath = document.getElementById('scan-current-path');
+ if (currentPath) {
+ currentPath.textContent = 'Scan finished successfully';
+ }
+ }
+
+ // Auto-dismiss after 3 seconds
+ setTimeout(() => {
+ this.removeScanProgressOverlay();
+ }, 3000);
+ }
+
+ /**
+ * Remove the scan progress overlay from the DOM
+ */
+ removeScanProgressOverlay() {
+ const overlay = document.getElementById('scan-progress-overlay');
+ if (overlay) {
+ overlay.classList.remove('visible');
+ // Wait for fade out animation before removing
+ setTimeout(() => {
+ if (overlay.parentElement) {
+ overlay.remove();
+ }
+ }, 300);
+ }
+ }
+
+ /**
+ * Reopen the scan progress overlay if a scan is in progress
+ * Called when user clicks on the rescan status indicator
+ */
+ async reopenScanOverlay() {
+ // Check if overlay already exists
+ const existingOverlay = document.getElementById('scan-progress-overlay');
+ if (existingOverlay) {
+ // Overlay is already open, do nothing
+ return;
+ }
+
+ // Check if scan is running via API
+ try {
+ const response = await this.makeAuthenticatedRequest('/api/anime/scan/status');
+ if (!response || !response.ok) {
+ console.log('Could not fetch scan status');
+ return;
+ }
+
+ const data = await response.json();
+ console.log('Scan status for reopen:', data);
+
+ if (data.is_scanning) {
+ // A scan is in progress, show the overlay
+ this.showScanProgressOverlay({
+ directory: data.directory,
+ total_items: data.total_items
+ });
+
+ // Update with current progress
+ this.updateScanProgressOverlay({
+ directories_scanned: data.directories_scanned,
+ files_found: data.directories_scanned,
+ current_directory: data.current_directory,
+ total_items: data.total_items
+ });
+ }
+ } catch (error) {
+ console.error('Error checking scan status for reopen:', error);
+ }
+ }
+
+ /**
+ * Check if a scan is currently in progress (useful after page reload)
+ * and show the progress overlay if so
+ */
+ async checkActiveScanStatus() {
+ try {
+ const response = await this.makeAuthenticatedRequest('/api/anime/scan/status');
+ if (!response || !response.ok) {
+ console.log('Could not fetch scan status, response:', response?.status);
+ return;
+ }
+
+ const data = await response.json();
+ console.log('Scan status check result:', data);
+
+ if (data.is_scanning) {
+ console.log('Scan is active, updating UI indicators');
+
+ // Update the process status indicator FIRST before showing overlay
+ // This ensures the header icon shows the running state immediately
+ this.updateProcessStatus('rescan', true);
+
+ // A scan is in progress, show the overlay
+ this.showScanProgressOverlay({
+ directory: data.directory,
+ total_items: data.total_items
+ });
+
+ // Update with current progress
+ this.updateScanProgressOverlay({
+ directories_scanned: data.directories_scanned,
+ files_found: data.directories_scanned,
+ current_directory: data.current_directory,
+ total_items: data.total_items
+ });
+
+ // Double-check the status indicator was updated
+ const statusElement = document.getElementById('rescan-status');
+ if (statusElement) {
+ console.log('Rescan status element classes:', statusElement.className);
+ } else {
+ console.warn('Rescan status element not found in DOM');
+ }
+ } else {
+ console.log('No active scan detected');
+ // Ensure indicator shows idle state
+ this.updateProcessStatus('rescan', false);
+ }
+ } catch (error) {
+ console.error('Error checking scan status:', error);
+ }
+ }
+
showLoading() {
document.getElementById('loading-overlay').classList.remove('hidden');
}
@@ -1146,10 +1480,16 @@ class AniWorldApp {
updateProcessStatus(processName, isRunning, hasError = false) {
const statusElement = document.getElementById(`${processName}-status`);
- if (!statusElement) return;
+ if (!statusElement) {
+ console.warn(`Process status element not found: ${processName}-status`);
+ return;
+ }
const statusDot = statusElement.querySelector('.status-dot');
- if (!statusDot) return;
+ if (!statusDot) {
+ console.warn(`Status dot not found in ${processName}-status element`);
+ return;
+ }
// Remove all status classes from both dot and element
statusDot.classList.remove('idle', 'running', 'error');
@@ -1171,6 +1511,8 @@ class AniWorldApp {
statusElement.classList.add('idle');
statusElement.title = `${displayName} is idle`;
}
+
+ console.log(`Process status updated: ${processName} = ${isRunning ? 'running' : (hasError ? 'error' : 'idle')}`);
}
async showConfigModal() {
diff --git a/src/server/web/static/js/index/advanced-config.js b/src/server/web/static/js/index/advanced-config.js
new file mode 100644
index 0000000..56d6264
--- /dev/null
+++ b/src/server/web/static/js/index/advanced-config.js
@@ -0,0 +1,74 @@
+/**
+ * AniWorld - Advanced Config Module
+ *
+ * Handles advanced configuration settings like concurrent downloads,
+ * timeouts, and debug mode.
+ *
+ * Dependencies: constants.js, api-client.js, ui-utils.js
+ */
+
+var AniWorld = window.AniWorld || {};
+
+AniWorld.AdvancedConfig = (function() {
+ 'use strict';
+
+ const API = AniWorld.Constants.API;
+
+ /**
+ * Load advanced configuration
+ */
+ async function load() {
+ try {
+ const response = await AniWorld.ApiClient.get(API.CONFIG_SECTION + '/advanced');
+ if (!response) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ const config = data.config;
+ document.getElementById('max-concurrent-downloads').value = config.max_concurrent_downloads || 3;
+ document.getElementById('provider-timeout').value = config.provider_timeout || 30;
+ document.getElementById('enable-debug-mode').checked = config.enable_debug_mode === true;
+ }
+ } catch (error) {
+ console.error('Error loading advanced config:', error);
+ }
+ }
+
+ /**
+ * Save advanced configuration
+ */
+ async function save() {
+ try {
+ const config = {
+ max_concurrent_downloads: parseInt(document.getElementById('max-concurrent-downloads').value),
+ provider_timeout: parseInt(document.getElementById('provider-timeout').value),
+ enable_debug_mode: document.getElementById('enable-debug-mode').checked
+ };
+
+ const response = await AniWorld.ApiClient.request(API.CONFIG_SECTION + '/advanced', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify(config)
+ });
+
+ if (!response) return;
+ const data = await response.json();
+
+ if (data.success) {
+ AniWorld.UI.showToast('Advanced configuration saved successfully', 'success');
+ } else {
+ AniWorld.UI.showToast('Failed to save config: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error saving advanced config:', error);
+ AniWorld.UI.showToast('Failed to save advanced configuration', 'error');
+ }
+ }
+
+ // Public API
+ return {
+ load: load,
+ save: save
+ };
+})();
diff --git a/src/server/web/static/js/index/app-init.js b/src/server/web/static/js/index/app-init.js
new file mode 100644
index 0000000..6a979fa
--- /dev/null
+++ b/src/server/web/static/js/index/app-init.js
@@ -0,0 +1,103 @@
+/**
+ * AniWorld - Index Page Application Initializer
+ *
+ * Main entry point for the index page. Initializes all modules.
+ *
+ * Dependencies: All shared and index modules
+ */
+
+var AniWorld = window.AniWorld || {};
+
+AniWorld.IndexApp = (function() {
+ 'use strict';
+
+ let localization = null;
+
+ /**
+ * Initialize the index page application
+ */
+ async function init() {
+ console.log('AniWorld Index App initializing...');
+
+ // Initialize localization if available
+ if (typeof Localization !== 'undefined') {
+ localization = new Localization();
+ }
+
+ // Check authentication first
+ const isAuthenticated = await AniWorld.Auth.checkAuth();
+ if (!isAuthenticated) {
+ return; // Auth module handles redirect
+ }
+
+ // Initialize theme
+ AniWorld.Theme.init();
+
+ // Initialize WebSocket connection
+ AniWorld.WebSocketClient.init();
+
+ // Initialize socket event handlers for this page
+ AniWorld.IndexSocketHandler.init(localization);
+
+ // Initialize page modules
+ AniWorld.SeriesManager.init();
+ AniWorld.SelectionManager.init();
+ AniWorld.Search.init();
+ AniWorld.ScanManager.init();
+ AniWorld.ConfigManager.init();
+
+ // Bind global events
+ bindGlobalEvents();
+
+ // Load initial data
+ await AniWorld.SeriesManager.loadSeries();
+
+ console.log('AniWorld Index App initialized successfully');
+ }
+
+ /**
+ * Bind global event handlers
+ */
+ function bindGlobalEvents() {
+ // Theme toggle
+ const themeToggle = document.getElementById('theme-toggle');
+ if (themeToggle) {
+ themeToggle.addEventListener('click', function() {
+ AniWorld.Theme.toggle();
+ });
+ }
+
+ // Logout button
+ const logoutBtn = document.getElementById('logout-btn');
+ if (logoutBtn) {
+ logoutBtn.addEventListener('click', function() {
+ AniWorld.Auth.logout(AniWorld.UI.showToast);
+ });
+ }
+ }
+
+ /**
+ * Get localization instance
+ */
+ function getLocalization() {
+ return localization;
+ }
+
+ // Public API
+ return {
+ init: init,
+ getLocalization: getLocalization
+ };
+})();
+
+// Initialize the application when DOM is loaded
+document.addEventListener('DOMContentLoaded', function() {
+ AniWorld.IndexApp.init();
+});
+
+// Expose app globally for inline event handlers (backwards compatibility)
+window.app = {
+ addSeries: function(link, name) {
+ return AniWorld.Search.addSeries(link, name);
+ }
+};
diff --git a/src/server/web/static/js/index/config-manager.js b/src/server/web/static/js/index/config-manager.js
new file mode 100644
index 0000000..d7b89c1
--- /dev/null
+++ b/src/server/web/static/js/index/config-manager.js
@@ -0,0 +1,229 @@
+/**
+ * AniWorld - Config Manager Module
+ *
+ * Orchestrates configuration modal and delegates to specialized config modules.
+ *
+ * Dependencies: constants.js, api-client.js, ui-utils.js,
+ * scheduler-config.js, logging-config.js, advanced-config.js, main-config.js
+ */
+
+var AniWorld = window.AniWorld || {};
+
+AniWorld.ConfigManager = (function() {
+ 'use strict';
+
+ const API = AniWorld.Constants.API;
+
+ /**
+ * Initialize the config manager
+ */
+ function init() {
+ bindEvents();
+ }
+
+ /**
+ * Bind UI events
+ */
+ function bindEvents() {
+ // Config modal
+ const configBtn = document.getElementById('config-btn');
+ if (configBtn) {
+ configBtn.addEventListener('click', showConfigModal);
+ }
+
+ const closeConfig = document.getElementById('close-config');
+ if (closeConfig) {
+ closeConfig.addEventListener('click', hideConfigModal);
+ }
+
+ const configModal = document.querySelector('#config-modal .modal-overlay');
+ if (configModal) {
+ configModal.addEventListener('click', hideConfigModal);
+ }
+
+ // Scheduler configuration
+ bindSchedulerEvents();
+
+ // Logging configuration
+ bindLoggingEvents();
+
+ // Advanced configuration
+ bindAdvancedEvents();
+
+ // Main configuration
+ bindMainEvents();
+
+ // Status panel
+ const closeStatus = document.getElementById('close-status');
+ if (closeStatus) {
+ closeStatus.addEventListener('click', hideStatus);
+ }
+ }
+
+ /**
+ * Bind scheduler-related events
+ */
+ function bindSchedulerEvents() {
+ const schedulerEnabled = document.getElementById('scheduled-rescan-enabled');
+ if (schedulerEnabled) {
+ schedulerEnabled.addEventListener('change', AniWorld.SchedulerConfig.toggleTimeInput);
+ }
+
+ const saveScheduler = document.getElementById('save-scheduler-config');
+ if (saveScheduler) {
+ saveScheduler.addEventListener('click', AniWorld.SchedulerConfig.save);
+ }
+
+ const testScheduler = document.getElementById('test-scheduled-rescan');
+ if (testScheduler) {
+ testScheduler.addEventListener('click', AniWorld.SchedulerConfig.testRescan);
+ }
+ }
+
+ /**
+ * Bind logging-related events
+ */
+ function bindLoggingEvents() {
+ const saveLogging = document.getElementById('save-logging-config');
+ if (saveLogging) {
+ saveLogging.addEventListener('click', AniWorld.LoggingConfig.save);
+ }
+
+ const testLogging = document.getElementById('test-logging');
+ if (testLogging) {
+ testLogging.addEventListener('click', AniWorld.LoggingConfig.testLogging);
+ }
+
+ const refreshLogs = document.getElementById('refresh-log-files');
+ if (refreshLogs) {
+ refreshLogs.addEventListener('click', AniWorld.LoggingConfig.loadLogFiles);
+ }
+
+ const cleanupLogs = document.getElementById('cleanup-logs');
+ if (cleanupLogs) {
+ cleanupLogs.addEventListener('click', AniWorld.LoggingConfig.cleanupLogs);
+ }
+ }
+
+ /**
+ * Bind advanced config events
+ */
+ function bindAdvancedEvents() {
+ const saveAdvanced = document.getElementById('save-advanced-config');
+ if (saveAdvanced) {
+ saveAdvanced.addEventListener('click', AniWorld.AdvancedConfig.save);
+ }
+ }
+
+ /**
+ * Bind main configuration events
+ */
+ function bindMainEvents() {
+ const createBackup = document.getElementById('create-config-backup');
+ if (createBackup) {
+ createBackup.addEventListener('click', AniWorld.MainConfig.createBackup);
+ }
+
+ const viewBackups = document.getElementById('view-config-backups');
+ if (viewBackups) {
+ viewBackups.addEventListener('click', AniWorld.MainConfig.viewBackups);
+ }
+
+ const exportConfig = document.getElementById('export-config');
+ if (exportConfig) {
+ exportConfig.addEventListener('click', AniWorld.MainConfig.exportConfig);
+ }
+
+ const validateConfig = document.getElementById('validate-config');
+ if (validateConfig) {
+ validateConfig.addEventListener('click', AniWorld.MainConfig.validateConfig);
+ }
+
+ const resetConfig = document.getElementById('reset-config');
+ if (resetConfig) {
+ resetConfig.addEventListener('click', handleResetConfig);
+ }
+
+ const saveMain = document.getElementById('save-main-config');
+ if (saveMain) {
+ saveMain.addEventListener('click', AniWorld.MainConfig.save);
+ }
+
+ const resetMain = document.getElementById('reset-main-config');
+ if (resetMain) {
+ resetMain.addEventListener('click', AniWorld.MainConfig.reset);
+ }
+
+ const testConnection = document.getElementById('test-connection');
+ if (testConnection) {
+ testConnection.addEventListener('click', AniWorld.MainConfig.testConnection);
+ }
+
+ const browseDirectory = document.getElementById('browse-directory');
+ if (browseDirectory) {
+ browseDirectory.addEventListener('click', AniWorld.MainConfig.browseDirectory);
+ }
+ }
+
+ /**
+ * Handle reset config with modal refresh
+ */
+ async function handleResetConfig() {
+ const success = await AniWorld.MainConfig.resetAllConfig();
+ if (success) {
+ setTimeout(function() {
+ hideConfigModal();
+ showConfigModal();
+ }, 1000);
+ }
+ }
+
+ /**
+ * Show the configuration modal
+ */
+ async function showConfigModal() {
+ const modal = document.getElementById('config-modal');
+
+ try {
+ // Load current status
+ const response = await AniWorld.ApiClient.get(API.ANIME_STATUS);
+ if (!response) return;
+ const data = await response.json();
+
+ document.getElementById('anime-directory-input').value = data.directory || '';
+ document.getElementById('series-count-input').value = data.series_count || '0';
+
+ // Load all configuration sections
+ await AniWorld.SchedulerConfig.load();
+ await AniWorld.LoggingConfig.load();
+ await AniWorld.AdvancedConfig.load();
+
+ modal.classList.remove('hidden');
+ } catch (error) {
+ console.error('Error loading configuration:', error);
+ AniWorld.UI.showToast('Failed to load configuration', 'error');
+ }
+ }
+
+ /**
+ * Hide the configuration modal
+ */
+ function hideConfigModal() {
+ document.getElementById('config-modal').classList.add('hidden');
+ }
+
+ /**
+ * Hide status panel
+ */
+ function hideStatus() {
+ document.getElementById('status-panel').classList.add('hidden');
+ }
+
+ // Public API
+ return {
+ init: init,
+ showConfigModal: showConfigModal,
+ hideConfigModal: hideConfigModal,
+ hideStatus: hideStatus
+ };
+})();
diff --git a/src/server/web/static/js/index/logging-config.js b/src/server/web/static/js/index/logging-config.js
new file mode 100644
index 0000000..085d1f0
--- /dev/null
+++ b/src/server/web/static/js/index/logging-config.js
@@ -0,0 +1,278 @@
+/**
+ * AniWorld - Logging Config Module
+ *
+ * Handles logging configuration, log file management, and log viewing.
+ *
+ * Dependencies: constants.js, api-client.js, ui-utils.js
+ */
+
+var AniWorld = window.AniWorld || {};
+
+AniWorld.LoggingConfig = (function() {
+ 'use strict';
+
+ const API = AniWorld.Constants.API;
+
+ /**
+ * Load logging configuration
+ */
+ async function load() {
+ try {
+ const response = await AniWorld.ApiClient.get(API.LOGGING_CONFIG);
+ if (!response) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ const config = data.config;
+
+ // Set form values
+ document.getElementById('log-level').value = config.log_level || 'INFO';
+ document.getElementById('enable-console-logging').checked = config.enable_console_logging !== false;
+ document.getElementById('enable-console-progress').checked = config.enable_console_progress === true;
+ document.getElementById('enable-fail2ban-logging').checked = config.enable_fail2ban_logging !== false;
+
+ // Load log files
+ await loadLogFiles();
+ }
+ } catch (error) {
+ console.error('Error loading logging config:', error);
+ AniWorld.UI.showToast('Failed to load logging configuration', 'error');
+ }
+ }
+
+ /**
+ * Load log files list
+ */
+ async function loadLogFiles() {
+ try {
+ const response = await AniWorld.ApiClient.get(API.LOGGING_FILES);
+ if (!response) return;
+
+ const data = await response.json();
+
+ if (data.success) {
+ const container = document.getElementById('log-files-list');
+ container.innerHTML = '';
+
+ if (data.files.length === 0) {
+ container.innerHTML = '' +
+ '' +
+ '
' +
+ '
' +
+ '0 / ' + totalDisplay + ' directories' +
+ '
' +
+ '
' +
+ '
' +
+ '0' +
+ 'Scanned' +
+ '
' +
+ '
' +
+ '0' +
+ 'Series Found' +
+ '
' +
+ '
' +
+ '
' +
+ 'Current:' +
+ '' + AniWorld.UI.escapeHtml(data?.directory || 'Initializing...') + '' +
+ '
' +
+ '
' +
+ '' +
+ '0.0s' +
+ '
' +
+ '
';
+
+ document.body.appendChild(overlay);
+
+ // Add click-outside-to-close handler
+ overlay.addEventListener('click', function(e) {
+ // Only close if clicking the overlay background, not the container
+ if (e.target === overlay) {
+ removeScanProgressOverlay();
+ }
+ });
+
+ // Trigger animation by adding visible class after a brief delay
+ requestAnimationFrame(function() {
+ overlay.classList.add('visible');
+ });
+ }
+
+ /**
+ * Update the scan progress overlay
+ * @param {Object} data - Scan progress event data
+ */
+ function updateScanProgressOverlay(data) {
+ const overlay = document.getElementById('scan-progress-overlay');
+ if (!overlay) return;
+
+ // Update total items if provided
+ if (data.total_items && data.total_items > 0) {
+ scanTotalItems = data.total_items;
+ const totalCount = document.getElementById('scan-total-count');
+ if (totalCount) {
+ totalCount.textContent = scanTotalItems;
+ }
+ }
+
+ // Update progress bar
+ const progressBar = document.getElementById('scan-progress-bar');
+ if (progressBar && scanTotalItems > 0 && data.directories_scanned !== undefined) {
+ const percentage = Math.min(100, (data.directories_scanned / scanTotalItems) * 100);
+ progressBar.style.width = percentage + '%';
+ }
+
+ // Update current/total count display
+ const currentCount = document.getElementById('scan-current-count');
+ if (currentCount && data.directories_scanned !== undefined) {
+ currentCount.textContent = data.directories_scanned;
+ }
+
+ // Update directories count
+ const dirCount = document.getElementById('scan-directories-count');
+ if (dirCount && data.directories_scanned !== undefined) {
+ dirCount.textContent = data.directories_scanned;
+ }
+
+ // Update files/series count
+ const filesCount = document.getElementById('scan-files-count');
+ if (filesCount && data.files_found !== undefined) {
+ filesCount.textContent = data.files_found;
+ }
+
+ // Update current directory (truncate if too long)
+ const currentPath = document.getElementById('scan-current-path');
+ if (currentPath && data.current_directory) {
+ const maxLength = 50;
+ let displayPath = data.current_directory;
+ if (displayPath.length > maxLength) {
+ displayPath = '...' + displayPath.slice(-maxLength + 3);
+ }
+ currentPath.textContent = displayPath;
+ currentPath.title = data.current_directory;
+ }
+ }
+
+ /**
+ * Hide the scan progress overlay with completion summary
+ * @param {Object} data - Scan completed event data
+ */
+ function hideScanProgressOverlay(data) {
+ const overlay = document.getElementById('scan-progress-overlay');
+ if (!overlay) return;
+
+ const container = overlay.querySelector('.scan-progress-container');
+ if (container) {
+ container.classList.add('completed');
+ }
+
+ // Update title
+ const titleText = overlay.querySelector('.scan-title-text');
+ if (titleText) {
+ titleText.textContent = 'Scan Complete';
+ }
+
+ // Complete the progress bar
+ const progressBar = document.getElementById('scan-progress-bar');
+ if (progressBar) {
+ progressBar.style.width = '100%';
+ }
+
+ // Update final stats
+ if (data) {
+ const dirCount = document.getElementById('scan-directories-count');
+ if (dirCount && data.total_directories !== undefined) {
+ dirCount.textContent = data.total_directories;
+ }
+
+ const filesCount = document.getElementById('scan-files-count');
+ if (filesCount && data.total_files !== undefined) {
+ filesCount.textContent = data.total_files;
+ }
+
+ // Update progress text to show final count
+ const currentCount = document.getElementById('scan-current-count');
+ const totalCount = document.getElementById('scan-total-count');
+ if (currentCount && data.total_directories !== undefined) {
+ currentCount.textContent = data.total_directories;
+ }
+ if (totalCount && data.total_directories !== undefined) {
+ totalCount.textContent = data.total_directories;
+ }
+
+ // Show elapsed time
+ const elapsedTimeEl = document.getElementById('scan-elapsed-time');
+ const elapsedValueEl = document.getElementById('scan-elapsed-value');
+ if (elapsedTimeEl && elapsedValueEl && data.elapsed_seconds !== undefined) {
+ elapsedValueEl.textContent = data.elapsed_seconds.toFixed(1) + 's';
+ elapsedTimeEl.classList.remove('hidden');
+ }
+
+ // Update current directory to show completion message
+ const currentPath = document.getElementById('scan-current-path');
+ if (currentPath) {
+ currentPath.textContent = 'Scan finished successfully';
+ }
+ }
+
+ // Auto-dismiss after 3 seconds
+ setTimeout(function() {
+ removeScanProgressOverlay();
+ }, DEFAULTS.SCAN_AUTO_DISMISS);
+ }
+
+ /**
+ * Remove the scan progress overlay from the DOM
+ */
+ function removeScanProgressOverlay() {
+ const overlay = document.getElementById('scan-progress-overlay');
+ if (overlay) {
+ overlay.classList.remove('visible');
+ // Wait for fade out animation before removing
+ setTimeout(function() {
+ if (overlay.parentElement) {
+ overlay.remove();
+ }
+ }, 300);
+ }
+ }
+
+ /**
+ * Reopen the scan progress overlay if a scan is in progress
+ */
+ async function reopenScanOverlay() {
+ // Check if overlay already exists
+ const existingOverlay = document.getElementById('scan-progress-overlay');
+ if (existingOverlay) {
+ return;
+ }
+
+ // Check if scan is running via API
+ try {
+ const response = await AniWorld.ApiClient.get(API.ANIME_SCAN_STATUS);
+ if (!response || !response.ok) {
+ console.log('Could not fetch scan status');
+ return;
+ }
+
+ const data = await response.json();
+ console.log('Scan status for reopen:', data);
+
+ if (data.is_scanning) {
+ // A scan is in progress, show the overlay
+ showScanProgressOverlay({
+ directory: data.directory,
+ total_items: data.total_items
+ });
+
+ // Update with current progress
+ updateScanProgressOverlay({
+ directories_scanned: data.directories_scanned,
+ files_found: data.directories_scanned,
+ current_directory: data.current_directory,
+ total_items: data.total_items
+ });
+ }
+ } catch (error) {
+ console.error('Error checking scan status for reopen:', error);
+ }
+ }
+
+ /**
+ * Check if a scan is currently in progress
+ */
+ async function checkActiveScanStatus() {
+ try {
+ const response = await AniWorld.ApiClient.get(API.ANIME_SCAN_STATUS);
+ if (!response || !response.ok) {
+ console.log('Could not fetch scan status, response:', response?.status);
+ return;
+ }
+
+ const data = await response.json();
+ console.log('Scan status check result:', data);
+
+ if (data.is_scanning) {
+ console.log('Scan is active, updating UI indicators');
+
+ // Update the process status indicator
+ updateProcessStatus('rescan', true);
+
+ // Show the overlay
+ showScanProgressOverlay({
+ directory: data.directory,
+ total_items: data.total_items
+ });
+
+ // Update with current progress
+ updateScanProgressOverlay({
+ directories_scanned: data.directories_scanned,
+ files_found: data.directories_scanned,
+ current_directory: data.current_directory,
+ total_items: data.total_items
+ });
+ } else {
+ console.log('No active scan detected');
+ updateProcessStatus('rescan', false);
+ }
+ } catch (error) {
+ console.error('Error checking scan status:', error);
+ }
+ }
+
+ /**
+ * Update process status indicator
+ * @param {string} processName - Process name (e.g., 'rescan', 'download')
+ * @param {boolean} isRunning - Whether the process is running
+ * @param {boolean} hasError - Whether there's an error
+ */
+ function updateProcessStatus(processName, isRunning, hasError) {
+ hasError = hasError || false;
+ const statusElement = document.getElementById(processName + '-status');
+ if (!statusElement) {
+ console.warn('Process status element not found: ' + processName + '-status');
+ return;
+ }
+
+ const statusDot = statusElement.querySelector('.status-dot');
+ if (!statusDot) {
+ console.warn('Status dot not found in ' + processName + '-status element');
+ return;
+ }
+
+ // Remove all status classes
+ statusDot.classList.remove('idle', 'running', 'error');
+ statusElement.classList.remove('running', 'error', 'idle');
+
+ // Capitalize process name for display
+ const displayName = processName.charAt(0).toUpperCase() + processName.slice(1);
+
+ if (hasError) {
+ statusDot.classList.add('error');
+ statusElement.classList.add('error');
+ statusElement.title = displayName + ' error - click for details';
+ } else if (isRunning) {
+ statusDot.classList.add('running');
+ statusElement.classList.add('running');
+ statusElement.title = displayName + ' is running...';
+ } else {
+ statusDot.classList.add('idle');
+ statusElement.classList.add('idle');
+ statusElement.title = displayName + ' is idle';
+ }
+
+ console.log('Process status updated: ' + processName + ' = ' + (isRunning ? 'running' : (hasError ? 'error' : 'idle')));
+ }
+
+ // Public API
+ return {
+ init: init,
+ rescanSeries: rescanSeries,
+ showScanProgressOverlay: showScanProgressOverlay,
+ updateScanProgressOverlay: updateScanProgressOverlay,
+ hideScanProgressOverlay: hideScanProgressOverlay,
+ removeScanProgressOverlay: removeScanProgressOverlay,
+ reopenScanOverlay: reopenScanOverlay,
+ checkActiveScanStatus: checkActiveScanStatus,
+ updateProcessStatus: updateProcessStatus
+ };
+})();
diff --git a/src/server/web/static/js/index/scheduler-config.js b/src/server/web/static/js/index/scheduler-config.js
new file mode 100644
index 0000000..baf4da0
--- /dev/null
+++ b/src/server/web/static/js/index/scheduler-config.js
@@ -0,0 +1,124 @@
+/**
+ * AniWorld - Scheduler Config Module
+ *
+ * Handles scheduler configuration and scheduled rescan settings.
+ *
+ * Dependencies: constants.js, api-client.js, ui-utils.js
+ */
+
+var AniWorld = window.AniWorld || {};
+
+AniWorld.SchedulerConfig = (function() {
+ 'use strict';
+
+ const API = AniWorld.Constants.API;
+
+ /**
+ * Load scheduler configuration
+ */
+ async function load() {
+ try {
+ const response = await AniWorld.ApiClient.get(API.SCHEDULER_CONFIG);
+ if (!response) return;
+ const data = await response.json();
+
+ if (data.success) {
+ const config = data.config;
+
+ // Update UI elements
+ document.getElementById('scheduled-rescan-enabled').checked = config.enabled;
+ document.getElementById('scheduled-rescan-time').value = config.time || '03:00';
+ document.getElementById('auto-download-after-rescan').checked = config.auto_download_after_rescan;
+
+ // Update status display
+ document.getElementById('next-rescan-time').textContent =
+ config.next_run ? new Date(config.next_run).toLocaleString() : 'Not scheduled';
+ document.getElementById('last-rescan-time').textContent =
+ config.last_run ? new Date(config.last_run).toLocaleString() : 'Never';
+
+ const statusBadge = document.getElementById('scheduler-running-status');
+ statusBadge.textContent = config.is_running ? 'Running' : 'Stopped';
+ statusBadge.className = 'info-value status-badge ' + (config.is_running ? 'running' : 'stopped');
+
+ // Enable/disable time input based on checkbox
+ toggleTimeInput();
+ }
+ } catch (error) {
+ console.error('Error loading scheduler config:', error);
+ AniWorld.UI.showToast('Failed to load scheduler configuration', 'error');
+ }
+ }
+
+ /**
+ * Save scheduler configuration
+ */
+ async function save() {
+ try {
+ const enabled = document.getElementById('scheduled-rescan-enabled').checked;
+ const time = document.getElementById('scheduled-rescan-time').value;
+ const autoDownload = document.getElementById('auto-download-after-rescan').checked;
+
+ const response = await AniWorld.ApiClient.post(API.SCHEDULER_CONFIG, {
+ enabled: enabled,
+ time: time,
+ auto_download_after_rescan: autoDownload
+ });
+
+ if (!response) return;
+ const data = await response.json();
+
+ if (data.success) {
+ AniWorld.UI.showToast('Scheduler configuration saved successfully', 'success');
+ await load();
+ } else {
+ AniWorld.UI.showToast('Failed to save config: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error saving scheduler config:', error);
+ AniWorld.UI.showToast('Failed to save scheduler configuration', 'error');
+ }
+ }
+
+ /**
+ * Test scheduled rescan
+ */
+ async function testRescan() {
+ try {
+ const response = await AniWorld.ApiClient.post(API.SCHEDULER_TRIGGER, {});
+
+ if (!response) return;
+ const data = await response.json();
+
+ if (data.success) {
+ AniWorld.UI.showToast('Test rescan triggered successfully', 'success');
+ } else {
+ AniWorld.UI.showToast('Failed to trigger test rescan: ' + data.error, 'error');
+ }
+ } catch (error) {
+ console.error('Error triggering test rescan:', error);
+ AniWorld.UI.showToast('Failed to trigger test rescan', 'error');
+ }
+ }
+
+ /**
+ * Toggle scheduler time input visibility
+ */
+ function toggleTimeInput() {
+ const enabled = document.getElementById('scheduled-rescan-enabled').checked;
+ const timeConfig = document.getElementById('rescan-time-config');
+
+ if (enabled) {
+ timeConfig.classList.add('enabled');
+ } else {
+ timeConfig.classList.remove('enabled');
+ }
+ }
+
+ // Public API
+ return {
+ load: load,
+ save: save,
+ testRescan: testRescan,
+ toggleTimeInput: toggleTimeInput
+ };
+})();
diff --git a/src/server/web/static/js/index/search.js b/src/server/web/static/js/index/search.js
new file mode 100644
index 0000000..ce316da
--- /dev/null
+++ b/src/server/web/static/js/index/search.js
@@ -0,0 +1,156 @@
+/**
+ * AniWorld - Search Module
+ *
+ * Handles anime search functionality and result display.
+ *
+ * Dependencies: constants.js, api-client.js, ui-utils.js, series-manager.js
+ */
+
+var AniWorld = window.AniWorld || {};
+
+AniWorld.Search = (function() {
+ 'use strict';
+
+ const API = AniWorld.Constants.API;
+
+ /**
+ * Initialize the search module
+ */
+ function init() {
+ bindEvents();
+ }
+
+ /**
+ * Bind UI events
+ */
+ function bindEvents() {
+ const searchInput = document.getElementById('search-input');
+ const searchBtn = document.getElementById('search-btn');
+ const clearSearch = document.getElementById('clear-search');
+
+ if (searchBtn) {
+ searchBtn.addEventListener('click', performSearch);
+ }
+
+ if (searchInput) {
+ searchInput.addEventListener('keypress', function(e) {
+ if (e.key === 'Enter') {
+ performSearch();
+ }
+ });
+ }
+
+ if (clearSearch) {
+ clearSearch.addEventListener('click', function() {
+ if (searchInput) searchInput.value = '';
+ hideSearchResults();
+ });
+ }
+ }
+
+ /**
+ * Perform anime search
+ */
+ async function performSearch() {
+ const searchInput = document.getElementById('search-input');
+ const query = searchInput ? searchInput.value.trim() : '';
+
+ if (!query) {
+ AniWorld.UI.showToast('Please enter a search term', 'warning');
+ return;
+ }
+
+ try {
+ AniWorld.UI.showLoading();
+
+ const response = await AniWorld.ApiClient.post(API.ANIME_SEARCH, { query: query });
+
+ if (!response) return;
+ const data = await response.json();
+
+ // Check if response is a direct array (new format) or wrapped object (legacy)
+ if (Array.isArray(data)) {
+ displaySearchResults(data);
+ } else if (data.status === 'success') {
+ displaySearchResults(data.results);
+ } else {
+ AniWorld.UI.showToast('Search error: ' + (data.message || 'Unknown error'), 'error');
+ }
+ } catch (error) {
+ console.error('Search error:', error);
+ AniWorld.UI.showToast('Search failed', 'error');
+ } finally {
+ AniWorld.UI.hideLoading();
+ }
+ }
+
+ /**
+ * Display search results
+ * @param {Array} results - Search results array
+ */
+ function displaySearchResults(results) {
+ const resultsContainer = document.getElementById('search-results');
+ const resultsList = document.getElementById('search-results-list');
+
+ if (results.length === 0) {
+ resultsContainer.classList.add('hidden');
+ AniWorld.UI.showToast('No search results found', 'warning');
+ return;
+ }
+
+ resultsList.innerHTML = results.map(function(result) {
+ const displayName = AniWorld.UI.getDisplayName(result);
+ return '