/** * 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() { fetch('/api/system/status', { 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'; let updateCheckInProgress = false; // Function to show update alert function showUpdateAlert(currentVersion, remoteVersion) { // Set status text in the system status panel updateStatusElement.innerHTML = ' Update available'; // Try to show the original alert box, but don't rely on it 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); } // Show the floating notification (our new approach) try { const floatingNotification = document.getElementById('floating-update-notification'); if (floatingNotification) { // Set text const versionElement = document.getElementById('floating-update-version'); if (versionElement) { versionElement.textContent = `Version ${currentVersion} → ${remoteVersion}`; } // Show with inline styles to force display floatingNotification.style.display = 'block'; // Make absolutely sure it's visible 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;' ); // Set up update button click 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); } console.log('Floating update notification shown:', currentVersion, '->', remoteVersion); } else { console.error('Floating notification element not found!'); } } catch (e) { console.error('Error showing floating notification:', e); } // 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); } // Hide floating notification try { const floatingNotification = document.getElementById('floating-update-notification'); if (floatingNotification) { floatingNotification.style.display = 'none'; } } catch (e) { console.error('Error hiding floating notification:', e); } // Clear localStorage localStorage.removeItem(UPDATE_KEY); localStorage.removeItem(CURRENT_VERSION_KEY); localStorage.removeItem(REMOTE_VERSION_KEY); } // 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 url = testMode ? '/api/system/check-updates?test=true' : '/api/system/check-updates'; // 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(); } } 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; } // 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') { // Hide update notification immediately after successful update hideUpdateAlert(); // 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); // Force a clean reload window.location.href = window.location.href.split('#')[0]; } }, 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); // Force a clean reload window.location.href = window.location.href.split('#')[0]; }, 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); } // 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'; // 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) { // If enabling test mode, force show update button showUpdateAlert('2.0.10', '2.1.0-test'); updateStatusElement.innerHTML = ' Update available'; showNotification('Test update button enabled', 'info'); } else { // If disabling test mode, check for real updates hideUpdateAlert(); checkForUpdates(); 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) { const currentVersion = localStorage.getItem(CURRENT_VERSION_KEY); const remoteVersion = localStorage.getItem(REMOTE_VERSION_KEY); 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 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(); checkForUpdates(); // Force check the update button status every second setInterval(forceShowUpdateButton, 1000); // Set interval to refresh uptime every minute setInterval(loadSystemStatus, 60000); }