document.addEventListener('DOMContentLoaded', function() { // Initialize navigation initNavigation(); // Initialize event listeners initEventListeners(); // Load initial dashboard data loadDashboardData(); // Set up dark mode based on user preference initDarkMode(); // Set up auto refresh if enabled initAutoRefresh(); // Initialize Bootstrap tooltips const tooltips = document.querySelectorAll('[data-bs-toggle="tooltip"]'); tooltips.forEach(tooltip => new bootstrap.Tooltip(tooltip)); }); function initNavigation() { const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); navLinks.forEach(link => { link.addEventListener('click', function(e) { e.preventDefault(); const page = this.getAttribute('data-page'); showPage(page); }); }); // Set active page from URL hash or default to dashboard const hash = window.location.hash.substring(1); showPage(hash || 'dashboard'); } function showPage(page) { // Hide all pages const pages = document.querySelectorAll('.page-content'); pages.forEach(p => p.classList.add('d-none')); // Remove active class from all nav links const navLinks = document.querySelectorAll('.navbar-nav .nav-link'); navLinks.forEach(link => link.classList.remove('active')); // Show selected page const selectedPage = document.getElementById(`page-${page}`); if (selectedPage) { selectedPage.classList.remove('d-none'); // Set active class on nav link const activeNav = document.querySelector(`.nav-link[data-page="${page}"]`); if (activeNav) { activeNav.classList.add('active'); } // Update URL hash window.location.hash = page; // Load page-specific data loadPageData(page); } } function loadPageData(page) { switch (page) { case 'dashboard': loadDashboardData(); break; case 'feeds': loadFeeds(); loadAllItems(); loadMatchedItems(); break; case 'torrents': loadTorrents(); break; case 'settings': loadSettings(); break; } } function initEventListeners() { // RSS Feeds page document.getElementById('btn-add-feed').addEventListener('click', showAddFeedModal); document.getElementById('btn-refresh-feeds').addEventListener('click', refreshFeeds); document.getElementById('save-feed-btn').addEventListener('click', saveFeed); // Torrents page document.getElementById('btn-add-torrent').addEventListener('click', showAddTorrentModal); document.getElementById('btn-refresh-torrents').addEventListener('click', loadTorrents); document.getElementById('save-torrent-btn').addEventListener('click', saveTorrent); // Logs page document.getElementById('btn-refresh-logs').addEventListener('click', refreshLogs); document.getElementById('btn-clear-logs').addEventListener('click', clearLogs); document.getElementById('btn-apply-log-filters').addEventListener('click', applyLogFilters); document.getElementById('btn-reset-log-filters').addEventListener('click', resetLogFilters); document.getElementById('btn-export-logs').addEventListener('click', exportLogs); // Settings page document.getElementById('settings-form').addEventListener('submit', saveSettings); document.getElementById('dark-mode-toggle').addEventListener('click', toggleDarkMode); document.getElementById('btn-reset-settings').addEventListener('click', resetSettings); // Configuration operations document.getElementById('btn-backup-config').addEventListener('click', backupConfig); document.getElementById('btn-reset-config').addEventListener('click', resetConfig); // Additional Transmission Instances document.getElementById('add-transmission-instance').addEventListener('click', addTransmissionInstance); } // Dashboard function loadDashboardData() { // Fetch dashboard statistics fetch('/api/dashboard/stats') .then(response => response.json()) .then(stats => { document.getElementById('active-downloads').textContent = stats.activeDownloads; document.getElementById('seeding-torrents').textContent = stats.seedingTorrents; document.getElementById('active-feeds').textContent = stats.activeFeeds; document.getElementById('completed-today').textContent = stats.completedToday; document.getElementById('added-today').textContent = stats.addedToday; document.getElementById('feeds-count').textContent = stats.feedsCount; document.getElementById('matched-count').textContent = stats.matchedCount; // Format download/upload speeds const downloadSpeed = formatBytes(stats.downloadSpeed) + '/s'; const uploadSpeed = formatBytes(stats.uploadSpeed) + '/s'; document.getElementById('download-speed').textContent = downloadSpeed; document.getElementById('upload-speed').textContent = uploadSpeed; document.getElementById('current-speed').textContent = `↓${downloadSpeed} ↑${uploadSpeed}`; // Set progress bars (max 100%) const maxSpeed = Math.max(stats.downloadSpeed, stats.uploadSpeed, 1); const dlPercent = Math.min(Math.round((stats.downloadSpeed / maxSpeed) * 100), 100); const ulPercent = Math.min(Math.round((stats.uploadSpeed / maxSpeed) * 100), 100); document.getElementById('download-speed-bar').style.width = `${dlPercent}%`; document.getElementById('upload-speed-bar').style.width = `${ulPercent}%`; }) .catch(error => { console.error('Error loading dashboard stats:', error); }); // Load chart data loadDownloadHistoryChart(); // Load other dashboard components loadActiveTorrents(); loadRecentMatches(); } function loadSystemStatus() { const statusElement = document.getElementById('system-status'); statusElement.innerHTML = '
'; fetch('/api/config') .then(response => response.json()) .then(config => { // Create system status HTML let html = ''; statusElement.innerHTML = html; }) .catch(error => { console.error('Error loading system status:', error); statusElement.innerHTML = '
Error loading system status
'; }); } function loadRecentMatches() { const matchesElement = document.getElementById('recent-matches'); matchesElement.innerHTML = '
'; fetch('/api/feeds/matched') .then(response => response.json()) .then(items => { // Sort by publish date descending and take the first 5 const recentItems = items.sort((a, b) => new Date(b.publishDate) - new Date(a.publishDate)).slice(0, 5); if (recentItems.length === 0) { matchesElement.innerHTML = '
No matched items yet
'; return; } let html = '
'; recentItems.forEach(item => { const date = new Date(item.publishDate); html += `
${item.title}
${formatDate(date)}
Matched rule: ${item.matchedRule} ${item.isDownloaded ? 'Downloaded' : 'Not Downloaded'}
`; }); html += '
'; matchesElement.innerHTML = html; }) .catch(error => { console.error('Error loading recent matches:', error); matchesElement.innerHTML = '
Error loading recent matches
'; }); } function loadActiveTorrents() { const torrentsElement = document.getElementById('active-torrents'); torrentsElement.innerHTML = '
'; fetch('/api/torrents') .then(response => response.json()) .then(torrents => { console.log('Dashboard torrents:', torrents); // Sort by progress ascending and filter for active torrents const activeTorrents = torrents .filter(t => t && t.status && (t.status === 'Downloading' || t.status === 'Seeding')) .sort((a, b) => (a.percentDone || 0) - (b.percentDone || 0)); if (activeTorrents.length === 0) { torrentsElement.innerHTML = '
No active torrents
'; return; } let html = '
'; activeTorrents.forEach(torrent => { // Handle potential null or undefined values if (!torrent || !torrent.name) { return; } // Safely calculate percentages and sizes with error handling let progressPercent = 0; try { progressPercent = Math.round((torrent.percentDone || 0) * 100); } catch (e) { console.warn('Error calculating progress percent:', e); } let sizeInGB = '0.00'; try { if (torrent.totalSize && torrent.totalSize > 0) { sizeInGB = (torrent.totalSize / 1073741824).toFixed(2); } } catch (e) { console.warn('Error calculating size in GB:', e); } const torrentStatus = torrent.status || 'Unknown'; const statusClass = torrentStatus.toLowerCase().replace(/\s+/g, '-'); html += `
${torrent.name}
${torrentStatus}
${progressPercent}%
Size: ${sizeInGB} GB
`; }); html += '
'; torrentsElement.innerHTML = html; }) .catch(error => { console.error('Error loading active torrents:', error); torrentsElement.innerHTML = '
Error loading active torrents
'; }); } // RSS Feeds function loadFeeds() { const feedsElement = document.getElementById('feeds-list'); feedsElement.innerHTML = '
'; fetch('/api/feeds') .then(response => response.json()) .then(feeds => { if (feeds.length === 0) { feedsElement.innerHTML = '
No feeds added yet
'; return; } let html = '
'; feeds.forEach(feed => { const lastChecked = feed.lastChecked ? new Date(feed.lastChecked) : null; html += `
${feed.name}
${lastChecked ? 'Last checked: ' + formatDate(lastChecked) : 'Never checked'}

${feed.url}

${feed.rules.length} rules
`; }); html += '
'; feedsElement.innerHTML = html; // Add event listeners document.querySelectorAll('.btn-edit-feed').forEach(btn => { btn.addEventListener('click', function() { const feedId = this.getAttribute('data-feed-id'); editFeed(feedId); }); }); document.querySelectorAll('.btn-delete-feed').forEach(btn => { btn.addEventListener('click', function() { const feedId = this.getAttribute('data-feed-id'); deleteFeed(feedId); }); }); }) .catch(error => { console.error('Error loading feeds:', error); feedsElement.innerHTML = '
Error loading feeds
'; }); } function loadAllItems() { const itemsElement = document.getElementById('all-items-list'); itemsElement.innerHTML = '
'; fetch('/api/feeds/items') .then(response => response.json()) .then(items => { if (items.length === 0) { itemsElement.innerHTML = '
No feed items yet
'; return; } let html = '
'; items.forEach(item => { const date = new Date(item.publishDate); const classes = `feed-item ${item.isMatched ? 'matched' : ''} ${item.isDownloaded ? 'downloaded' : ''}`; html += `
${formatDate(date)}
${item.isMatched ? `
Matched rule: ${item.matchedRule}
` : ''} ${item.isDownloaded ? '
Downloaded
' : ''} ${!item.isDownloaded && item.isMatched ? `
` : '' }
`; }); html += '
'; itemsElement.innerHTML = html; // Add event listeners document.querySelectorAll('.btn-download-item').forEach(btn => { btn.addEventListener('click', function() { const itemId = this.getAttribute('data-item-id'); downloadItem(itemId); }); }); }) .catch(error => { console.error('Error loading feed items:', error); itemsElement.innerHTML = '
Error loading feed items
'; }); } function loadMatchedItems() { const matchedElement = document.getElementById('matched-items-list'); matchedElement.innerHTML = '
'; fetch('/api/feeds/matched') .then(response => response.json()) .then(items => { if (items.length === 0) { matchedElement.innerHTML = '
No matched items yet
'; return; } let html = '
'; items.forEach(item => { const date = new Date(item.publishDate); const classes = `feed-item matched ${item.isDownloaded ? 'downloaded' : ''}`; html += `
${formatDate(date)}
Matched rule: ${item.matchedRule}
${item.isDownloaded ? '
Downloaded
' : ''} ${!item.isDownloaded ? `
` : '' }
`; }); html += '
'; matchedElement.innerHTML = html; // Add event listeners document.querySelectorAll('.btn-download-matched-item').forEach(btn => { btn.addEventListener('click', function() { const itemId = this.getAttribute('data-item-id'); downloadItem(itemId); }); }); }) .catch(error => { console.error('Error loading matched items:', error); matchedElement.innerHTML = '
Error loading matched items
'; }); } function showAddFeedModal() { // Clear form document.getElementById('feed-name').value = ''; document.getElementById('feed-url').value = ''; document.getElementById('feed-rules').value = ''; document.getElementById('feed-auto-download').checked = false; // Update modal title and button text document.querySelector('#add-feed-modal .modal-title').textContent = 'Add RSS Feed'; document.getElementById('save-feed-btn').textContent = 'Add Feed'; // Remove feed ID data attribute document.getElementById('save-feed-btn').removeAttribute('data-feed-id'); // Show modal const modal = new bootstrap.Modal(document.getElementById('add-feed-modal')); modal.show(); } function editFeed(feedId) { // Fetch feed data fetch(`/api/feeds`) .then(response => response.json()) .then(feeds => { const feed = feeds.find(f => f.id === feedId); if (!feed) { alert('Feed not found'); return; } // Populate form document.getElementById('feed-name').value = feed.name; document.getElementById('feed-url').value = feed.url; document.getElementById('feed-rules').value = feed.rules.join('\n'); document.getElementById('feed-auto-download').checked = feed.autoDownload; // Update modal title and button text document.querySelector('#add-feed-modal .modal-title').textContent = 'Edit RSS Feed'; document.getElementById('save-feed-btn').textContent = 'Save Changes'; // Add feed ID data attribute document.getElementById('save-feed-btn').setAttribute('data-feed-id', feedId); // Show modal const modal = new bootstrap.Modal(document.getElementById('add-feed-modal')); modal.show(); }) .catch(error => { console.error('Error fetching feed:', error); alert('Error fetching feed'); }); } function saveFeed() { const name = document.getElementById('feed-name').value.trim(); const url = document.getElementById('feed-url').value.trim(); const rulesText = document.getElementById('feed-rules').value.trim(); const autoDownload = document.getElementById('feed-auto-download').checked; if (!name || !url) { alert('Please enter a name and URL'); return; } // Parse rules (split by new line and remove empty lines) const rules = rulesText.split('\n').filter(rule => rule.trim() !== ''); const feedId = document.getElementById('save-feed-btn').getAttribute('data-feed-id'); const isEditing = !!feedId; const feedData = { name: name, url: url, rules: rules, autoDownload: autoDownload }; if (isEditing) { feedData.id = feedId; // Update existing feed fetch(`/api/feeds/${feedId}`, { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feedData) }) .then(response => { if (!response.ok) { throw new Error('Failed to update feed'); } // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('add-feed-modal')); modal.hide(); // Refresh feeds loadFeeds(); }) .catch(error => { console.error('Error updating feed:', error); alert('Error updating feed'); }); } else { // Add new feed fetch('/api/feeds', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(feedData) }) .then(response => { if (!response.ok) { throw new Error('Failed to add feed'); } // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('add-feed-modal')); modal.hide(); // Refresh feeds loadFeeds(); // Also refresh items since a new feed might have new items loadAllItems(); loadMatchedItems(); }) .catch(error => { console.error('Error adding feed:', error); alert('Error adding feed'); }); } } function deleteFeed(feedId) { if (!confirm('Are you sure you want to delete this feed?')) { return; } fetch(`/api/feeds/${feedId}`, { method: 'DELETE' }) .then(response => { if (!response.ok) { throw new Error('Failed to delete feed'); } // Refresh feeds loadFeeds(); // Also refresh items since items from this feed should be removed loadAllItems(); loadMatchedItems(); }) .catch(error => { console.error('Error deleting feed:', error); alert('Error deleting feed'); }); } function refreshFeeds() { const btn = document.getElementById('btn-refresh-feeds'); btn.disabled = true; btn.innerHTML = ' Refreshing...'; fetch('/api/feeds/refresh', { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to refresh feeds'); } // Re-enable button btn.disabled = false; btn.textContent = 'Refresh Feeds'; // Refresh feed items loadFeeds(); loadAllItems(); loadMatchedItems(); }) .catch(error => { console.error('Error refreshing feeds:', error); alert('Error refreshing feeds'); // Re-enable button btn.disabled = false; btn.textContent = 'Refresh Feeds'; }); } function downloadItem(itemId) { fetch(`/api/feeds/download/${itemId}`, { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to download item'); } // Refresh items loadAllItems(); loadMatchedItems(); // Also refresh torrents since a new torrent should be added loadTorrents(); }) .catch(error => { console.error('Error downloading item:', error); alert('Error downloading item'); }); } // Torrents function loadTorrents() { const torrentsElement = document.getElementById('torrents-list'); torrentsElement.innerHTML = '
'; fetch('/api/torrents') .then(response => response.json()) .then(torrents => { console.log('Loaded torrents:', torrents); if (torrents.length === 0) { torrentsElement.innerHTML = '
No torrents
'; return; } let html = '
'; torrents.forEach(torrent => { // Handle potential null or undefined values if (!torrent || !torrent.name) { console.warn('Invalid torrent data:', torrent); return; } // Safely calculate percentages and sizes with error handling let progressPercent = 0; try { progressPercent = Math.round((torrent.percentDone || 0) * 100); } catch (e) { console.warn('Error calculating progress percent:', e); } let sizeInGB = '0.00'; try { if (torrent.totalSize && torrent.totalSize > 0) { sizeInGB = (torrent.totalSize / 1073741824).toFixed(2); } } catch (e) { console.warn('Error calculating size in GB:', e); } const torrentStatus = torrent.status || 'Unknown'; const statusClass = torrentStatus.toLowerCase().replace(/\s+/g, '-'); html += `
${torrent.name}
${torrentStatus}
${progressPercent}%
Size: ${sizeInGB} GB Location: ${torrent.downloadDir || 'Unknown'}
${torrentStatus === 'Stopped' ? `` : `` } ${progressPercent >= 100 ? `` : '' }
`; }); html += '
'; torrentsElement.innerHTML = html; // Add event listeners document.querySelectorAll('.btn-start-torrent').forEach(btn => { btn.addEventListener('click', function() { const torrentId = parseInt(this.getAttribute('data-torrent-id')); startTorrent(torrentId); }); }); document.querySelectorAll('.btn-stop-torrent').forEach(btn => { btn.addEventListener('click', function() { const torrentId = parseInt(this.getAttribute('data-torrent-id')); stopTorrent(torrentId); }); }); document.querySelectorAll('.btn-remove-torrent').forEach(btn => { btn.addEventListener('click', function() { const torrentId = parseInt(this.getAttribute('data-torrent-id')); removeTorrent(torrentId); }); }); document.querySelectorAll('.btn-process-torrent').forEach(btn => { btn.addEventListener('click', function() { const torrentId = parseInt(this.getAttribute('data-torrent-id')); processTorrent(torrentId); }); }); }) .catch(error => { console.error('Error loading torrents:', error); torrentsElement.innerHTML = '
Error loading torrents
'; }); } function showAddTorrentModal() { // Clear form document.getElementById('torrent-url').value = ''; document.getElementById('torrent-download-dir').value = ''; // Show modal const modal = new bootstrap.Modal(document.getElementById('add-torrent-modal')); modal.show(); } function saveTorrent() { const url = document.getElementById('torrent-url').value.trim(); const downloadDir = document.getElementById('torrent-download-dir').value.trim(); if (!url) { alert('Please enter a torrent URL or magnet link'); return; } const torrentData = { url: url }; if (downloadDir) { torrentData.downloadDir = downloadDir; } fetch('/api/torrents', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(torrentData) }) .then(response => { if (!response.ok) { throw new Error('Failed to add torrent'); } // Close modal const modal = bootstrap.Modal.getInstance(document.getElementById('add-torrent-modal')); modal.hide(); // Refresh torrents loadTorrents(); }) .catch(error => { console.error('Error adding torrent:', error); alert('Error adding torrent'); }); } function startTorrent(torrentId) { fetch(`/api/torrents/${torrentId}/start`, { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to start torrent'); } // Refresh torrents loadTorrents(); }) .catch(error => { console.error('Error starting torrent:', error); alert('Error starting torrent'); }); } function stopTorrent(torrentId) { fetch(`/api/torrents/${torrentId}/stop`, { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to stop torrent'); } // Refresh torrents loadTorrents(); }) .catch(error => { console.error('Error stopping torrent:', error); alert('Error stopping torrent'); }); } function removeTorrent(torrentId) { if (!confirm('Are you sure you want to remove this torrent? The downloaded files will be kept.')) { return; } fetch(`/api/torrents/${torrentId}`, { method: 'DELETE' }) .then(response => { if (!response.ok) { throw new Error('Failed to remove torrent'); } // Refresh torrents loadTorrents(); }) .catch(error => { console.error('Error removing torrent:', error); alert('Error removing torrent'); }); } function processTorrent(torrentId) { fetch(`/api/torrents/${torrentId}/process`, { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to process torrent'); } alert('Torrent processing started'); }) .catch(error => { console.error('Error processing torrent:', error); alert('Error processing torrent'); }); } // Settings function loadSettings() { const form = document.getElementById('settings-form'); fetch('/api/config') .then(response => response.json()) .then(config => { // Transmission settings document.getElementById('transmission-host').value = config.transmission.host; document.getElementById('transmission-port').value = config.transmission.port; document.getElementById('transmission-use-https').checked = config.transmission.useHttps; document.getElementById('transmission-username').value = ''; document.getElementById('transmission-password').value = ''; // RSS settings document.getElementById('auto-download-enabled').checked = config.autoDownloadEnabled; document.getElementById('check-interval').value = config.checkIntervalMinutes; // Directory settings document.getElementById('download-directory').value = config.downloadDirectory; document.getElementById('media-library').value = config.mediaLibraryPath; // Post processing settings document.getElementById('post-processing-enabled').checked = config.postProcessing.enabled; document.getElementById('extract-archives').checked = config.postProcessing.extractArchives; document.getElementById('organize-media').checked = config.postProcessing.organizeMedia; document.getElementById('minimum-seed-ratio').value = config.postProcessing.minimumSeedRatio; document.getElementById('media-extensions').value = config.postProcessing.mediaExtensions.join(', '); }) .catch(error => { console.error('Error loading settings:', error); alert('Error loading settings'); }); } function saveSettings(e) { e.preventDefault(); // Show saving indicator const saveBtn = document.querySelector('#settings-form button[type="submit"]'); const originalBtnText = saveBtn.innerHTML; saveBtn.disabled = true; saveBtn.innerHTML = ' Saving...'; const config = { transmission: { host: document.getElementById('transmission-host').value.trim(), port: parseInt(document.getElementById('transmission-port').value), useHttps: document.getElementById('transmission-use-https').checked, username: document.getElementById('transmission-username').value.trim(), password: document.getElementById('transmission-password').value.trim() }, autoDownloadEnabled: document.getElementById('auto-download-enabled').checked, checkIntervalMinutes: parseInt(document.getElementById('check-interval').value), downloadDirectory: document.getElementById('download-directory').value.trim(), mediaLibraryPath: document.getElementById('media-library').value.trim(), postProcessing: { enabled: document.getElementById('post-processing-enabled').checked, extractArchives: document.getElementById('extract-archives').checked, organizeMedia: document.getElementById('organize-media').checked, minimumSeedRatio: parseInt(document.getElementById('minimum-seed-ratio').value), mediaExtensions: document.getElementById('media-extensions').value.split(',').map(ext => ext.trim()) }, userPreferences: { enableDarkMode: document.body.classList.contains('dark-mode'), autoRefreshUIEnabled: localStorage.getItem('autoRefresh') !== 'false', autoRefreshIntervalSeconds: parseInt(localStorage.getItem('refreshInterval')) || 30, notificationsEnabled: true, notificationEvents: ["torrent-added", "torrent-completed", "torrent-error"], defaultView: "dashboard", confirmBeforeDelete: true, maxItemsPerPage: 25, dateTimeFormat: "yyyy-MM-dd HH:mm:ss", showCompletedTorrents: true, keepHistoryDays: 30 }, enableDetailedLogging: false }; // Log what we're saving to help with debugging console.log('Saving configuration:', config); fetch('/api/config', { method: 'PUT', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(config) }) .then(response => { if (!response.ok) { return response.text().then(text => { throw new Error(`Failed to save settings: ${text}`); }); } return response.json(); }) .then(result => { console.log('Settings saved successfully:', result); // Show success message const settingsForm = document.getElementById('settings-form'); const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-success alert-dismissible fade show mt-3'; alertDiv.role = 'alert'; alertDiv.innerHTML = ` Success! Your settings have been saved. `; settingsForm.appendChild(alertDiv); // Auto dismiss after 3 seconds setTimeout(() => { const bsAlert = bootstrap.Alert.getOrCreateInstance(alertDiv); bsAlert.close(); }, 3000); // Re-enable save button saveBtn.disabled = false; saveBtn.innerHTML = originalBtnText; }) .catch(error => { console.error('Error saving settings:', error); // Show error message const settingsForm = document.getElementById('settings-form'); const alertDiv = document.createElement('div'); alertDiv.className = 'alert alert-danger alert-dismissible fade show mt-3'; alertDiv.role = 'alert'; alertDiv.innerHTML = ` Error! ${error.message} `; settingsForm.appendChild(alertDiv); // Re-enable save button saveBtn.disabled = false; saveBtn.innerHTML = originalBtnText; }); } // Helper functions function formatDate(date) { if (!date) return 'N/A'; // Format as "YYYY-MM-DD HH:MM" return `${date.getFullYear()}-${padZero(date.getMonth() + 1)}-${padZero(date.getDate())} ${padZero(date.getHours())}:${padZero(date.getMinutes())}`; } function padZero(num) { return num.toString().padStart(2, '0'); } // Dark Mode Functions function initDarkMode() { // Check local storage preference or system preference const darkModePreference = localStorage.getItem('darkMode'); if (darkModePreference === 'true' || (darkModePreference === null && window.matchMedia('(prefers-color-scheme: dark)').matches)) { enableDarkMode(); } else { disableDarkMode(); } } function toggleDarkMode() { if (document.body.classList.contains('dark-mode')) { disableDarkMode(); } else { enableDarkMode(); } } function enableDarkMode() { document.body.classList.add('dark-mode'); document.getElementById('dark-mode-toggle').innerHTML = ''; localStorage.setItem('darkMode', 'true'); // Also update user preferences if on settings page const darkModeCheckbox = document.getElementById('enable-dark-mode'); if (darkModeCheckbox) { darkModeCheckbox.checked = true; } } function disableDarkMode() { document.body.classList.remove('dark-mode'); document.getElementById('dark-mode-toggle').innerHTML = ''; localStorage.setItem('darkMode', 'false'); // Also update user preferences if on settings page const darkModeCheckbox = document.getElementById('enable-dark-mode'); if (darkModeCheckbox) { darkModeCheckbox.checked = false; } } // Auto-refresh function initAutoRefresh() { // Get auto-refresh settings from local storage or use defaults const autoRefresh = localStorage.getItem('autoRefresh') !== 'false'; const refreshInterval = parseInt(localStorage.getItem('refreshInterval')) || 30; if (autoRefresh) { startAutoRefresh(refreshInterval); } } function startAutoRefresh(intervalSeconds) { // Clear any existing interval if (window.refreshTimer) { clearInterval(window.refreshTimer); } // Set new interval window.refreshTimer = setInterval(() => { const currentPage = window.location.hash.substring(1) || 'dashboard'; loadPageData(currentPage); }, intervalSeconds * 1000); localStorage.setItem('autoRefresh', 'true'); localStorage.setItem('refreshInterval', intervalSeconds.toString()); } function stopAutoRefresh() { if (window.refreshTimer) { clearInterval(window.refreshTimer); window.refreshTimer = null; } localStorage.setItem('autoRefresh', 'false'); } // Chart functions function loadDownloadHistoryChart() { fetch('/api/dashboard/history') .then(response => response.json()) .then(history => { const ctx = document.getElementById('download-history-chart').getContext('2d'); // Extract dates and count values const labels = history.map(point => { const date = new Date(point.date); return `${date.getMonth() + 1}/${date.getDate()}`; }); const countData = history.map(point => point.count); const sizeData = history.map(point => point.totalSize / (1024 * 1024 * 1024)); // Convert to GB // Create or update chart if (window.downloadHistoryChart) { window.downloadHistoryChart.data.labels = labels; window.downloadHistoryChart.data.datasets[0].data = countData; window.downloadHistoryChart.data.datasets[1].data = sizeData; window.downloadHistoryChart.update(); } else { window.downloadHistoryChart = new Chart(ctx, { type: 'bar', data: { labels: labels, datasets: [ { label: 'Number of Downloads', data: countData, backgroundColor: 'rgba(13, 110, 253, 0.5)', borderColor: 'rgba(13, 110, 253, 1)', borderWidth: 1, yAxisID: 'y' }, { label: 'Total Size (GB)', data: sizeData, type: 'line', borderColor: 'rgba(25, 135, 84, 1)', backgroundColor: 'rgba(25, 135, 84, 0.1)', borderWidth: 2, fill: true, tension: 0.4, yAxisID: 'y1' } ] }, options: { responsive: true, maintainAspectRatio: false, plugins: { legend: { position: 'top', }, tooltip: { callbacks: { label: function(context) { let label = context.dataset.label || ''; if (label) { label += ': '; } if (context.datasetIndex === 0) { label += context.parsed.y; } else { label += context.parsed.y.toFixed(2) + ' GB'; } return label; } } } }, scales: { y: { type: 'linear', display: true, position: 'left', title: { display: true, text: 'Number of Downloads' }, beginAtZero: true }, y1: { type: 'linear', display: true, position: 'right', title: { display: true, text: 'Total Size (GB)' }, beginAtZero: true, grid: { drawOnChartArea: false } } } } }); } }) .catch(error => { console.error('Error loading download history chart:', error); }); } // Logs Management function refreshLogs() { const logFilters = getLogFilters(); loadLogs(logFilters); } function getLogFilters() { return { level: document.getElementById('log-level').value, search: document.getElementById('log-search').value, dateRange: document.getElementById('log-date-range').value, skip: 0, take: parseInt(document.getElementById('items-per-page')?.value || 25) }; } function loadLogs(filters) { const tbody = document.getElementById('logs-table-body'); tbody.innerHTML = 'Loading logs...'; // Build query string const query = new URLSearchParams(); if (filters.level && filters.level !== 'All') { query.append('Level', filters.level); } if (filters.search) { query.append('Search', filters.search); } // Handle date range const now = new Date(); let startDate = null; switch (filters.dateRange) { case 'today': startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate(), 0, 0, 0); break; case 'yesterday': startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1, 0, 0, 0); break; case 'week': startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7, 0, 0, 0); break; case 'month': startDate = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 30, 0, 0, 0); break; } if (startDate) { query.append('StartDate', startDate.toISOString()); } query.append('Skip', filters.skip.toString()); query.append('Take', filters.take.toString()); fetch(`/api/logs?${query.toString()}`) .then(response => response.json()) .then(logs => { if (logs.length === 0) { tbody.innerHTML = 'No logs found'; document.getElementById('log-count').textContent = '0 entries'; document.getElementById('logs-pagination-info').textContent = 'Showing 0 of 0 entries'; return; } let html = ''; logs.forEach(log => { const timestamp = new Date(log.timestamp); const levelClass = getLevelClass(log.level); html += ` ${formatDate(timestamp)} ${log.level} ${log.message} ${log.context || ''} `; }); tbody.innerHTML = html; document.getElementById('log-count').textContent = `${logs.length} entries`; document.getElementById('logs-pagination-info').textContent = `Showing ${logs.length} entries`; // Update pagination (simplified for now) updateLogPagination(filters, logs.length); }) .catch(error => { console.error('Error loading logs:', error); tbody.innerHTML = 'Error loading logs'; }); } function updateLogPagination(filters, count) { const pagination = document.getElementById('logs-pagination'); // Simplified pagination - just first page for now pagination.innerHTML = `
  • 1
  • `; } function getLevelClass(level) { switch (level.toLowerCase()) { case 'debug': return 'bg-secondary'; case 'information': return 'bg-info'; case 'warning': return 'bg-warning'; case 'error': return 'bg-danger'; case 'critical': return 'bg-dark'; default: return 'bg-secondary'; } } function applyLogFilters() { const filters = getLogFilters(); loadLogs(filters); } function resetLogFilters() { document.getElementById('log-level').value = 'All'; document.getElementById('log-search').value = ''; document.getElementById('log-date-range').value = 'week'; loadLogs(getLogFilters()); } function clearLogs() { if (!confirm('Are you sure you want to clear all logs? This action cannot be undone.')) { return; } fetch('/api/logs/clear', { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to clear logs'); } // Refresh logs loadLogs(getLogFilters()); }) .catch(error => { console.error('Error clearing logs:', error); alert('Error clearing logs'); }); } function exportLogs() { const filters = getLogFilters(); const query = new URLSearchParams(); if (filters.level && filters.level !== 'All') { query.append('Level', filters.level); } if (filters.search) { query.append('Search', filters.search); } // Create download link const link = document.createElement('a'); link.href = `/api/logs/export?${query.toString()}`; link.download = `transmission-rss-logs-${new Date().toISOString().slice(0, 10)}.csv`; document.body.appendChild(link); link.click(); document.body.removeChild(link); } // Helper function to format file sizes function formatBytes(bytes, decimals = 2) { if (bytes === 0) return '0 B'; const k = 1024; const dm = decimals < 0 ? 0 : decimals; const sizes = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB']; const i = Math.floor(Math.log(bytes) / Math.log(k)); return parseFloat((bytes / Math.pow(k, i)).toFixed(dm)) + ' ' + sizes[i]; } // Configuration Operations function backupConfig() { if (!confirm('This will create a backup of your configuration files. Do you want to continue?')) { return; } fetch('/api/config/backup', { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to backup configuration'); } return response.blob(); }) .then(blob => { // Create download link for the backup file const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.style.display = 'none'; a.href = url; a.download = `transmission-rss-config-backup-${new Date().toISOString().slice(0, 10)}.json`; document.body.appendChild(a); a.click(); window.URL.revokeObjectURL(url); alert('Configuration backup created successfully.'); }) .catch(error => { console.error('Error backing up configuration:', error); alert('Error creating configuration backup.'); }); } function resetConfig() { if (!confirm('WARNING: This will reset your configuration to default settings. All your feeds, rules, and user preferences will be lost. This cannot be undone. Are you absolutely sure?')) { return; } if (!confirm('FINAL WARNING: All feeds, rules, and settings will be permanently deleted. Type "RESET" to confirm.')) { return; } const confirmation = prompt('Type "RESET" to confirm configuration reset:'); if (confirmation !== 'RESET') { alert('Configuration reset cancelled.'); return; } fetch('/api/config/reset', { method: 'POST' }) .then(response => { if (!response.ok) { throw new Error('Failed to reset configuration'); } alert('Configuration has been reset to defaults. The application will now reload.'); window.location.reload(); }) .catch(error => { console.error('Error resetting configuration:', error); alert('Error resetting configuration.'); }); } // Transmission Instance Management function addTransmissionInstance() { const instancesList = document.getElementById('transmission-instances-list'); const instanceCount = document.querySelectorAll('.transmission-instance').length; const newInstanceIndex = instanceCount + 1; const instanceHtml = `
    Instance #${newInstanceIndex}
    `; // If the "no instances" message is showing, remove it if (instancesList.querySelector('.text-center.text-muted')) { instancesList.innerHTML = ''; } // Add the new instance instancesList.insertAdjacentHTML('beforeend', instanceHtml); } function removeTransmissionInstance(index) { const instance = document.getElementById(`transmission-instance-${index}`); if (instance) { instance.remove(); // If there are no instances left, show the "no instances" message const instancesList = document.getElementById('transmission-instances-list'); if (instancesList.children.length === 0) { instancesList.innerHTML = '
    No additional instances configured
    '; } } } function resetSettings() { if (!confirm('This will reset all settings to their default values. Are you sure?')) { return; } // Load default settings fetch('/api/config/defaults') .then(response => response.json()) .then(defaults => { // Apply defaults to form loadSettingsIntoForm(defaults); }) .catch(error => { console.error('Error loading default settings:', error); alert('Error loading default settings'); }); }