/** * Transmission RSS Manager - System Status Module * @description Functionality for system status display and updates */ // System status and updates functionality function initSystemStatus() { // Elements const versionElement = document.getElementById('system-version'); const uptimeElement = document.getElementById('uptime'); const transmissionStatusElement = document.getElementById('transmission-status'); const updateStatusElement = document.getElementById('update-status'); const updateAvailableDiv = document.getElementById('update-available'); const updateButton = document.getElementById('btn-update-now'); const refreshButton = document.getElementById('btn-refresh-status'); // Load system status function loadSystemStatus() { // Add cache-busting parameter const cacheBuster = `?_=${new Date().getTime()}`; fetch('/api/system/status' + cacheBuster, { headers: authHeaders() }) .then(handleResponse) .then(data => { if (data.status === 'success') { // Update version display versionElement.textContent = data.data.version; uptimeElement.textContent = data.data.uptime; // Also update footer version const footerVersion = document.getElementById('footer-version'); if (footerVersion) { footerVersion.textContent = 'v' + data.data.version; } // Update version in about modal too if it exists const aboutVersionElement = document.getElementById('about-version'); if (aboutVersionElement) { aboutVersionElement.textContent = 'Transmission RSS Manager v' + data.data.version; } // Update transmission status with icon if (data.data.transmissionStatus === 'Connected') { transmissionStatusElement.innerHTML = ' Connected'; } else { transmissionStatusElement.innerHTML = ' Disconnected'; } } else { showNotification('Failed to load system status', 'danger'); } }) .catch(error => { console.error('Error fetching system status:', error); showNotification('Failed to connect to server', 'danger'); }); } // More robust update check status tracking const UPDATE_KEY = 'trm_update_available'; const CURRENT_VERSION_KEY = 'trm_current_version'; const REMOTE_VERSION_KEY = 'trm_remote_version'; // Force clear any existing update notification state localStorage.removeItem(UPDATE_KEY); localStorage.removeItem(CURRENT_VERSION_KEY); localStorage.removeItem(REMOTE_VERSION_KEY); let updateCheckInProgress = false; // Function to show update alert function showUpdateAlert(currentVersion, remoteVersion) { // Set status text in the system status panel updateStatusElement.innerHTML = ' Update available'; // Show only the original alert box in the dashboard try { const alertBox = updateAvailableDiv.querySelector('.alert'); if (alertBox) { alertBox.style.display = 'block'; const spanElement = alertBox.querySelector('span'); if (spanElement) { spanElement.textContent = `A new version is available: ${currentVersion} → ${remoteVersion}`; } } } catch (e) { console.error('Error showing original alert box:', e); } // We've removed the floating notification entirely, so this part is skipped console.log('Update alert shown in dashboard:', currentVersion, '->', remoteVersion); // Store in localStorage localStorage.setItem(UPDATE_KEY, 'true'); localStorage.setItem(CURRENT_VERSION_KEY, currentVersion); localStorage.setItem(REMOTE_VERSION_KEY, remoteVersion); } // Function to hide update alert function hideUpdateAlert() { // Hide original alert try { const alertBox = updateAvailableDiv.querySelector('.alert'); if (alertBox) { alertBox.style.display = 'none'; } } catch (e) { console.error('Error hiding original alert:', e); } // Clear localStorage localStorage.removeItem(UPDATE_KEY); localStorage.removeItem(CURRENT_VERSION_KEY); localStorage.removeItem(REMOTE_VERSION_KEY); console.log('Update alert hidden'); } // Check localStorage on init and set up MutationObserver to prevent hiding (function checkStoredUpdateStatus() { const isUpdateAvailable = localStorage.getItem(UPDATE_KEY) === 'true'; if (isUpdateAvailable) { const currentVersion = localStorage.getItem(CURRENT_VERSION_KEY); const remoteVersion = localStorage.getItem(REMOTE_VERSION_KEY); if (currentVersion && remoteVersion) { showUpdateAlert(currentVersion, remoteVersion); // Set up mutation observer to detect and revert any attempts to hide the update alert const alertBox = updateAvailableDiv.querySelector('.alert'); if (alertBox) { const observer = new MutationObserver(function(mutations) { mutations.forEach(function(mutation) { if (mutation.type === 'attributes' && (mutation.attributeName === 'style' || mutation.attributeName === 'class')) { // If display is being changed to hide the element, force it back to visible if (alertBox.style.display !== 'block' || alertBox.classList.contains('d-none') || alertBox.style.visibility === 'hidden' || alertBox.style.opacity === '0') { console.log('Detected attempt to hide update button, forcing display'); showUpdateAlert(currentVersion, remoteVersion); } } }); }); // Observe style and class attribute changes observer.observe(alertBox, { attributes: true, attributeFilter: ['style', 'class'] }); // Store observer in window object to prevent garbage collection window._updateButtonObserver = observer; } } } })(); // Check for updates function checkForUpdates() { updateStatusElement.innerHTML = ' Checking...'; updateCheckInProgress = true; // Add test=true parameter to force update availability for testing const testMode = localStorage.getItem('showUpdateButton') === 'true'; const cacheBuster = `_=${new Date().getTime()}`; const url = testMode ? `/api/system/check-updates?test=true&${cacheBuster}` : `/api/system/check-updates?${cacheBuster}`; // Set a timeout to detect network issues const timeoutId = setTimeout(() => { updateStatusElement.innerHTML = ' Check timed out'; updateCheckInProgress = false; showNotification('Update check timed out. Please try again later.', 'warning'); }, 10000); // 10 second timeout // Create a timeout controller const controller = new AbortController(); const timeoutId2 = setTimeout(() => controller.abort(), 15000); fetch(url, { headers: authHeaders(), // Add a fetch timeout using abort controller signal: controller.signal // 15 second timeout }) .then(response => { clearTimeout(timeoutId2); clearTimeout(timeoutId); return response; }) .catch(error => { clearTimeout(timeoutId2); clearTimeout(timeoutId); throw error; }) .then(response => { // Better error checking if (!response.ok) { return response.json().then(data => { throw new Error(data.message || `Server error: ${response.status}`); }).catch(e => { if (e instanceof SyntaxError) { throw new Error(`Server error: ${response.status}`); } throw e; }); } return response.json(); }) .then(data => { updateCheckInProgress = false; if (data.status === 'success') { if (data.data && data.data.updateAvailable) { // Show update alert with version info showUpdateAlert(data.data.currentVersion, data.data.remoteVersion); // Log to console for debugging console.log('Update available detected:', data.data.currentVersion, '->', data.data.remoteVersion); } else { // No update available updateStatusElement.innerHTML = ' Up to date'; hideUpdateAlert(); // Force reload system status to ensure version is current setTimeout(() => loadSystemStatus(), 1000); } } else { // Error status but with a response updateStatusElement.innerHTML = ' Check failed'; showNotification(data.message || 'Failed to check for updates', 'danger'); // Don't clear update status on error - keep any previous update notification } }) .catch(error => { updateCheckInProgress = false; clearTimeout(timeoutId); console.error('Error checking for updates:', error); updateStatusElement.innerHTML = ' Check failed'; // More specific error message based on the error type if (error.name === 'AbortError') { showNotification('Update check timed out. Please try again later.', 'warning'); } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { showNotification('Network error. Please check your connection and try again.', 'danger'); } else { showNotification(error.message || 'Failed to connect to server', 'danger'); } // Don't clear update status on error - keep any previous update notification }); } // Apply update function applyUpdate() { // Show confirmation dialog if (!confirm('Are you sure you want to update the application? The service will restart.')) { return; } // Disable test mode whenever we try to apply an update localStorage.setItem('showUpdateButton', 'false'); // Update toggle button text if it exists const testToggle = document.getElementById('toggle-test-update-button'); if (testToggle) { testToggle.innerText = 'Enable Test Update'; } // Show loading state on both update buttons // Original button if (updateButton) { updateButton.disabled = true; updateButton.innerHTML = ' Updating...'; } // Floating notification button const floatingButton = document.getElementById('floating-update-button'); if (floatingButton) { floatingButton.disabled = true; floatingButton.textContent = 'Updating...'; } showNotification('Applying update. Please wait...', 'info'); // Set a timeout for the update process const updateTimeoutId = setTimeout(() => { // Re-enable original button if (updateButton) { updateButton.disabled = false; updateButton.innerHTML = ' Update Now'; } // Re-enable floating button if (floatingButton) { floatingButton.disabled = false; floatingButton.textContent = 'Update Now'; } showNotification('Update process timed out. Please try again or check server logs.', 'warning'); }, 60000); // 60 second timeout for the entire update process // Create a timeout controller const updateController = new AbortController(); const updateTimeoutId2 = setTimeout(() => updateController.abort(), 45000); fetch('/api/system/update', { method: 'POST', headers: { 'Content-Type': 'application/json', ...authHeaders() }, signal: updateController.signal // 45 second timeout }) .then(response => { clearTimeout(updateTimeoutId2); return response; }) .catch(error => { clearTimeout(updateTimeoutId2); throw error; }) .then(response => { // Better error checking if (!response.ok) { return response.json().then(data => { throw new Error(data.message || `Server error: ${response.status}`); }).catch(e => { if (e instanceof SyntaxError) { throw new Error(`Server error: ${response.status}`); } throw e; }); } return response.json(); }) .then(data => { clearTimeout(updateTimeoutId); if (data.status === 'success') { // Check if there's an update message to determine if an update was actually applied const updateApplied = data.message && data.message.includes('Update applied successfully'); const noNewUpdate = data.data && data.data.output && data.data.output.includes('already have the latest version'); // Hide update notification hideUpdateAlert(); if (noNewUpdate) { // If no update was needed, show a different message showNotification('You already have the latest version. No update was needed.', 'info'); // Re-enable both buttons if (updateButton) { updateButton.disabled = false; updateButton.innerHTML = ' Check Again'; } const floatingButton = document.getElementById('floating-update-button'); if (floatingButton) { floatingButton.disabled = false; floatingButton.textContent = 'Check Again'; } // Update page to show current version without reloading loadSystemStatus(); // Double-check system status again after a delay to ensure version is updated setTimeout(() => { loadSystemStatus(); checkForUpdates(); // Run check again to update status text }, 2000); return; } // Show success notification showNotification('Update applied successfully. The page will reload in 30 seconds.', 'success'); // Update both buttons with countdown let secondsLeft = 30; // Function to update the countdown text function updateCountdown() { // Update original button if it exists if (updateButton) { updateButton.innerHTML = ` Reloading in ${secondsLeft}s...`; } // Update floating button if it exists const floatingButton = document.getElementById('floating-update-button'); if (floatingButton) { floatingButton.textContent = `Reloading in ${secondsLeft}s...`; } } // Initial text update updateCountdown(); // Start countdown const countdownInterval = setInterval(() => { secondsLeft--; updateCountdown(); if (secondsLeft <= 0) { clearInterval(countdownInterval); // Clear localStorage to ensure a clean reload localStorage.removeItem(UPDATE_KEY); localStorage.removeItem(CURRENT_VERSION_KEY); localStorage.removeItem(REMOTE_VERSION_KEY); // Also ensure floating notification is completely removed const floatingNotification = document.getElementById('floating-update-notification'); if (floatingNotification) { floatingNotification.style.display = 'none'; floatingNotification.removeAttribute('style'); } // Force a clean reload window.location.href = window.location.href.split('#')[0] + '?t=' + new Date().getTime(); } }, 1000); // Set a timer to reload the page after the service has time to restart setTimeout(() => { clearInterval(countdownInterval); // Clear localStorage to ensure a clean reload localStorage.removeItem(UPDATE_KEY); localStorage.removeItem(CURRENT_VERSION_KEY); localStorage.removeItem(REMOTE_VERSION_KEY); // Also ensure floating notification is completely removed const floatingNotification = document.getElementById('floating-update-notification'); if (floatingNotification) { floatingNotification.style.display = 'none'; floatingNotification.removeAttribute('style'); } // Force a clean reload with cache-busting parameter window.location.href = window.location.href.split('#')[0] + '?t=' + new Date().getTime(); }, 30000); } else { // Enable both buttons on failure if (updateButton) { updateButton.disabled = false; updateButton.innerHTML = ' Update Now'; } const floatingButton = document.getElementById('floating-update-button'); if (floatingButton) { floatingButton.disabled = false; floatingButton.textContent = 'Update Now'; } showNotification(data.message || 'Failed to apply update', 'danger'); } }) .catch(error => { clearTimeout(updateTimeoutId); console.error('Error applying update:', error); // Re-enable both buttons on error if (updateButton) { updateButton.disabled = false; updateButton.innerHTML = ' Update Now'; } const floatingButton = document.getElementById('floating-update-button'); if (floatingButton) { floatingButton.disabled = false; floatingButton.textContent = 'Update Now'; } // More specific error message based on the error type if (error.name === 'AbortError') { showNotification('Update request timed out. The server might still be processing the update.', 'warning'); } else if (error.message.includes('Failed to fetch') || error.message.includes('NetworkError')) { showNotification('Network error. Please check your connection and try again.', 'danger'); } else { showNotification(error.message || 'Failed to connect to server', 'danger'); } }); } // Event listeners if (refreshButton) { refreshButton.addEventListener('click', () => { loadSystemStatus(); checkForUpdates(); }); } if (updateButton) { updateButton.addEventListener('click', applyUpdate); } // Add handler for floating refresh button const floatingRefreshButton = document.getElementById('floating-refresh-button'); if (floatingRefreshButton) { floatingRefreshButton.addEventListener('click', () => { // Force a hard refresh of everything floatingRefreshButton.textContent = 'Refreshing...'; floatingRefreshButton.disabled = true; // Force reload system status loadSystemStatus(); // Force a check without the test parameter to get real status const realCheckUrl = `/api/system/check-updates?_=${new Date().getTime()}`; fetch(realCheckUrl, { headers: authHeaders() }) .then(response => response.json()) .then(data => { console.log('Manual refresh result:', data); if (data.status === 'success') { // Check if we're in test mode const isTestMode = localStorage.getItem('showUpdateButton') === 'true'; // If test mode is enabled but update says no update available, disable test mode if (isTestMode && data.data && !data.data.updateAvailable) { localStorage.setItem('showUpdateButton', 'false'); testToggle.innerText = 'Enable Test Update'; showNotification('Test mode has been disabled - no real update is available', 'info'); hideUpdateAlert(); showNotification(`Current version: ${data.data.currentVersion}. You are up to date.`, 'success'); } // Regular update handling else if (data.data && data.data.updateAvailable) { showUpdateAlert(data.data.currentVersion, data.data.remoteVersion); showNotification(`Update is available: ${data.data.currentVersion} → ${data.data.remoteVersion}`, 'info'); } else { hideUpdateAlert(); showNotification(`Current version: ${data.data.currentVersion}. You are up to date.`, 'success'); } } // Re-enable button floatingRefreshButton.textContent = 'Refresh Status'; floatingRefreshButton.disabled = false; }) .catch(error => { console.error('Error during manual refresh:', error); floatingRefreshButton.textContent = 'Refresh Status'; floatingRefreshButton.disabled = false; showNotification('Error checking update status', 'danger'); }); }); } // Test mode toggle (for developers) const testToggle = document.getElementById('toggle-test-update-button'); if (testToggle) { // Initialize based on current localStorage setting const isTestMode = localStorage.getItem('showUpdateButton') === 'true'; // If test mode is enabled but we have a version mismatch, update the stored version if (isTestMode && versionElement && versionElement.textContent) { const currentVersion = versionElement.textContent.trim(); if (localStorage.getItem(CURRENT_VERSION_KEY) !== currentVersion) { localStorage.setItem(CURRENT_VERSION_KEY, currentVersion); } } // Update toggle text testToggle.innerText = isTestMode ? 'Disable Test Update' : 'Enable Test Update'; // Add click handler testToggle.addEventListener('click', (e) => { e.preventDefault(); const currentSetting = localStorage.getItem('showUpdateButton') === 'true'; const newSetting = !currentSetting; localStorage.setItem('showUpdateButton', newSetting); testToggle.innerText = newSetting ? 'Disable Test Update' : 'Enable Test Update'; if (newSetting) { // Get the current version from the version element let currentVersion = '2.0.11'; // Default fallback if (versionElement && versionElement.textContent) { currentVersion = versionElement.textContent.trim(); } // If enabling test mode, force show update button showUpdateAlert(currentVersion, '2.1.0-test'); updateStatusElement.innerHTML = ' Update available (TEST MODE)'; // Add test mode indicator to floating notification const floatingNotification = document.getElementById('floating-update-notification'); if (floatingNotification) { const testBadge = document.createElement('div'); testBadge.style.backgroundColor = 'orange'; testBadge.style.color = 'black'; testBadge.style.padding = '3px 8px'; testBadge.style.borderRadius = '4px'; testBadge.style.fontWeight = 'bold'; testBadge.style.marginBottom = '5px'; testBadge.style.fontSize = '12px'; testBadge.textContent = 'TEST MODE - NOT A REAL UPDATE'; // Insert at the top of the notification floatingNotification.insertBefore(testBadge, floatingNotification.firstChild); } showNotification('TEST MODE ENABLED - This is not a real update', 'warning'); } else { // If disabling test mode, check for real updates hideUpdateAlert(); // Force a check without the test parameter to get real status const realCheckUrl = '/api/system/check-updates'; fetch(realCheckUrl, { headers: authHeaders() }) .then(response => response.json()) .then(data => { console.log('Real update check result:', data); if (data.status === 'success' && data.data && !data.data.updateAvailable) { showNotification('No actual updates are available.', 'info'); } else if (data.status === 'success' && data.data && data.data.updateAvailable) { showUpdateAlert(data.data.currentVersion, data.data.remoteVersion); showNotification(`A real update is available: ${data.data.currentVersion} → ${data.data.remoteVersion}`, 'info'); } }) .catch(error => console.error('Error checking for real updates:', error)); showNotification('Test update button disabled', 'info'); } }); } // Persistent update button - force display every second if update is available function forceShowUpdateButton() { const isUpdateAvailable = localStorage.getItem(UPDATE_KEY) === 'true'; if (isUpdateAvailable) { // Get the most current version let currentVersion = localStorage.getItem(CURRENT_VERSION_KEY); const remoteVersion = localStorage.getItem(REMOTE_VERSION_KEY); // If we have the version element on screen, use that as the source of truth if (versionElement && versionElement.textContent) { const displayedVersion = versionElement.textContent.trim(); // Update stored version if different if (displayedVersion !== currentVersion) { localStorage.setItem(CURRENT_VERSION_KEY, displayedVersion); currentVersion = displayedVersion; } } if (currentVersion && remoteVersion) { // Check floating notification const floatingNotification = document.getElementById('floating-update-notification'); if (floatingNotification && floatingNotification.style.display !== 'block') { console.log('Forcing floating update notification display'); // Set the version text const versionElement = document.getElementById('floating-update-version'); if (versionElement) { versionElement.textContent = `Version ${currentVersion} → ${remoteVersion}`; } // Apply strong styling - make sure to completely override any previous styles floatingNotification.setAttribute('style', ''); // Clear any previous styles first floatingNotification.setAttribute('style', ''); // Clear any previous styles first floatingNotification.setAttribute('style', 'display: block !important; ' + 'visibility: visible !important; ' + 'opacity: 1 !important; ' + 'position: fixed !important; ' + 'top: 20px !important; ' + 'right: 20px !important; ' + 'width: 300px !important; ' + 'padding: 15px !important; ' + 'background-color: #ff5555 !important; ' + 'color: white !important; ' + 'border: 3px solid #cc0000 !important; ' + 'border-radius: 5px !important; ' + 'box-shadow: 0 0 20px rgba(0,0,0,0.5) !important; ' + 'z-index: 10000 !important; ' + 'font-weight: bold !important; ' + 'text-align: center !important;' ); // Ensure button has correct event handler const updateButton = document.getElementById('floating-update-button'); if (updateButton) { // Remove any existing listeners updateButton.removeEventListener('click', applyUpdate); // Add new listener updateButton.addEventListener('click', applyUpdate); } } // Still try the original alert as a fallback try { const alertBox = updateAvailableDiv.querySelector('.alert'); if (alertBox && alertBox.style.display !== 'block') { alertBox.style.display = 'block'; updateAvailableDiv.style.display = 'block'; // Update message const spanElement = alertBox.querySelector('span'); if (spanElement) { spanElement.textContent = `A new version is available: ${currentVersion} → ${remoteVersion}`; } } } catch (e) { console.error('Error forcing original update button:', e); } } } } // Initialize loadSystemStatus(); // SUPER EMERGENCY FIX: Force hide and remove all update notifications const emergencyFix = () => { // Hard reset all update states localStorage.clear(); // Clear ALL localStorage to be absolutely safe // Find and destroy any floating notification elements document.querySelectorAll('[id*="notification"], [id*="update"], [class*="notification"], [class*="update"]').forEach(el => { try { if (el.id !== 'update-status' && !el.id.includes('refresh')) { el.style.cssText = 'display: none !important; visibility: hidden !important; opacity: 0 !important'; el.removeAttribute('style'); if (el.parentNode) { el.parentNode.removeChild(el); console.log('Element removed from DOM:', el.id || el.className || 'unnamed element'); } } } catch (e) { console.error('Error removing element:', e); } }); // Add a MutationObserver to keep killing any notification elements that might reappear if (!window._notificationKiller) { const observer = new MutationObserver(mutations => { mutations.forEach(mutation => { mutation.addedNodes.forEach(node => { if (node.nodeType === 1) { // Element node if ((node.id && (node.id.includes('notification') || node.id.includes('update'))) || (node.className && (node.className.includes('notification') || node.className.includes('update')))) { if (node.id !== 'update-status' && !node.id.includes('refresh')) { node.style.cssText = 'display: none !important'; if (node.parentNode) { node.parentNode.removeChild(node); console.log('Dynamically added notification killed'); } } } } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); window._notificationKiller = observer; } console.log('Super emergency notification cleanup complete'); }; // Run immediately emergencyFix(); // Run again after delays to ensure it works setTimeout(emergencyFix, 100); setTimeout(emergencyFix, 500); setTimeout(emergencyFix, 1000); // Set interval to refresh uptime every minute setInterval(loadSystemStatus, 60000); }