diff --git a/frontend/static/js/backup-restore.js b/frontend/static/js/backup-restore.js new file mode 100644 index 00000000..45728f6d --- /dev/null +++ b/frontend/static/js/backup-restore.js @@ -0,0 +1,606 @@ +/** + * Backup and Restore functionality for Huntarr + * Handles database backup creation, restoration, and management + */ + +const BackupRestore = { + initialized: false, + backupSettings: { + frequency: 3, + retention: 3 + }, + + initialize: function() { + if (this.initialized) { + console.log('[BackupRestore] Already initialized'); + return; + } + + console.log('[BackupRestore] Initializing backup/restore functionality'); + + this.bindEvents(); + this.loadSettings(); + this.loadBackupList(); + this.updateNextBackupTime(); + + this.initialized = true; + console.log('[BackupRestore] Initialization complete'); + }, + + bindEvents: function() { + // Backup frequency change + const frequencyInput = document.getElementById('backup-frequency'); + if (frequencyInput) { + frequencyInput.addEventListener('change', () => { + this.backupSettings.frequency = parseInt(frequencyInput.value) || 3; + this.saveSettings(); + this.updateNextBackupTime(); + }); + } + + // Backup retention change + const retentionInput = document.getElementById('backup-retention'); + if (retentionInput) { + retentionInput.addEventListener('change', () => { + this.backupSettings.retention = parseInt(retentionInput.value) || 3; + this.saveSettings(); + }); + } + + // Create manual backup + const createBackupBtn = document.getElementById('create-backup-btn'); + if (createBackupBtn) { + createBackupBtn.addEventListener('click', () => { + this.createManualBackup(); + }); + } + + // Restore backup selection + const restoreSelect = document.getElementById('restore-backup-select'); + if (restoreSelect) { + restoreSelect.addEventListener('change', () => { + this.handleRestoreSelection(); + }); + } + + // Restore confirmation input + const restoreConfirmation = document.getElementById('restore-confirmation'); + if (restoreConfirmation) { + restoreConfirmation.addEventListener('input', () => { + this.validateRestoreConfirmation(); + }); + } + + // Restore button + const restoreBtn = document.getElementById('restore-backup-btn'); + if (restoreBtn) { + restoreBtn.addEventListener('click', () => { + this.restoreBackup(); + }); + } + + // Delete confirmation input + const deleteConfirmation = document.getElementById('delete-confirmation'); + if (deleteConfirmation) { + deleteConfirmation.addEventListener('input', () => { + this.validateDeleteConfirmation(); + }); + } + + // Delete database button + const deleteBtn = document.getElementById('delete-database-btn'); + if (deleteBtn) { + deleteBtn.addEventListener('click', () => { + this.deleteDatabase(); + }); + } + }, + + loadSettings: function() { + console.log('[BackupRestore] Loading backup settings'); + + fetch('./api/backup/settings') + .then(response => response.json()) + .then(data => { + if (data.success) { + this.backupSettings = { + frequency: data.settings.frequency || 3, + retention: data.settings.retention || 3 + }; + + // Update UI + const frequencyInput = document.getElementById('backup-frequency'); + const retentionInput = document.getElementById('backup-retention'); + + if (frequencyInput) frequencyInput.value = this.backupSettings.frequency; + if (retentionInput) retentionInput.value = this.backupSettings.retention; + + this.updateNextBackupTime(); + } + }) + .catch(error => { + console.error('[BackupRestore] Error loading settings:', error); + this.showError('Failed to load backup settings'); + }); + }, + + saveSettings: function() { + console.log('[BackupRestore] Saving backup settings', this.backupSettings); + + fetch('./api/backup/settings', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify(this.backupSettings) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + console.log('[BackupRestore] Settings saved successfully'); + this.showSuccess('Backup settings saved'); + } else { + throw new Error(data.error || 'Failed to save settings'); + } + }) + .catch(error => { + console.error('[BackupRestore] Error saving settings:', error); + this.showError('Failed to save backup settings'); + }); + }, + + loadBackupList: function() { + console.log('[BackupRestore] Loading backup list'); + + const listContainer = document.getElementById('backup-list-container'); + const restoreSelect = document.getElementById('restore-backup-select'); + + if (listContainer) { + listContainer.innerHTML = '
Loading backup list...
'; + } + + if (restoreSelect) { + restoreSelect.innerHTML = ''; + } + + fetch('./api/backup/list') + .then(response => response.json()) + .then(data => { + if (data.success) { + this.renderBackupList(data.backups); + this.populateRestoreSelect(data.backups); + } else { + throw new Error(data.error || 'Failed to load backups'); + } + }) + .catch(error => { + console.error('[BackupRestore] Error loading backup list:', error); + if (listContainer) { + listContainer.innerHTML = '
Error loading backup list
'; + } + if (restoreSelect) { + restoreSelect.innerHTML = ''; + } + }); + }, + + renderBackupList: function(backups) { + const listContainer = document.getElementById('backup-list-container'); + if (!listContainer) return; + + if (!backups || backups.length === 0) { + listContainer.innerHTML = '
No backups available
'; + return; + } + + let html = ''; + backups.forEach(backup => { + const date = new Date(backup.timestamp); + const formattedDate = date.toLocaleString(); + const size = this.formatFileSize(backup.size); + + html += ` +
+
+
${backup.name}
+
+ Created: ${formattedDate} | Size: ${size} | Type: ${backup.type || 'Manual'} +
+
+
+ +
+
+ `; + }); + + listContainer.innerHTML = html; + }, + + populateRestoreSelect: function(backups) { + const restoreSelect = document.getElementById('restore-backup-select'); + if (!restoreSelect) return; + + if (!backups || backups.length === 0) { + restoreSelect.innerHTML = ''; + return; + } + + let html = ''; + backups.forEach(backup => { + const date = new Date(backup.timestamp); + const formattedDate = date.toLocaleString(); + const size = this.formatFileSize(backup.size); + + html += ``; + }); + + restoreSelect.innerHTML = html; + }, + + updateNextBackupTime: function() { + const nextBackupElement = document.getElementById('next-backup-time'); + if (!nextBackupElement) return; + + fetch('./api/backup/next-scheduled') + .then(response => response.json()) + .then(data => { + if (data.success && data.next_backup) { + const nextDate = new Date(data.next_backup); + nextBackupElement.innerHTML = ` ${nextDate.toLocaleString()}`; + } else { + nextBackupElement.innerHTML = ' Not scheduled'; + } + }) + .catch(error => { + console.error('[BackupRestore] Error getting next backup time:', error); + nextBackupElement.innerHTML = ' Error loading schedule'; + }); + }, + + createManualBackup: function() { + console.log('[BackupRestore] Creating manual backup'); + + const createBtn = document.getElementById('create-backup-btn'); + const progressContainer = document.getElementById('backup-progress'); + const progressFill = document.querySelector('.progress-fill'); + const progressText = document.querySelector('.progress-text'); + + if (createBtn) createBtn.disabled = true; + if (progressContainer) progressContainer.style.display = 'block'; + if (progressText) progressText.textContent = 'Creating backup...'; + + // Animate progress bar + let progress = 0; + const progressInterval = setInterval(() => { + progress += Math.random() * 15; + if (progress > 90) progress = 90; + if (progressFill) progressFill.style.width = progress + '%'; + }, 200); + + fetch('./api/backup/create', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + type: 'manual', + name: `Manual Backup ${new Date().toISOString().split('T')[0]}` + }) + }) + .then(response => response.json()) + .then(data => { + clearInterval(progressInterval); + + if (data.success) { + if (progressFill) progressFill.style.width = '100%'; + if (progressText) progressText.textContent = 'Backup created successfully!'; + + setTimeout(() => { + if (progressContainer) progressContainer.style.display = 'none'; + if (progressFill) progressFill.style.width = '0%'; + }, 2000); + + this.showSuccess(`Backup created successfully: ${data.backup_name}`); + this.loadBackupList(); // Refresh the list + } else { + throw new Error(data.error || 'Failed to create backup'); + } + }) + .catch(error => { + clearInterval(progressInterval); + console.error('[BackupRestore] Error creating backup:', error); + + if (progressContainer) progressContainer.style.display = 'none'; + if (progressFill) progressFill.style.width = '0%'; + + this.showError('Failed to create backup: ' + error.message); + }) + .finally(() => { + if (createBtn) createBtn.disabled = false; + }); + }, + + handleRestoreSelection: function() { + const restoreSelect = document.getElementById('restore-backup-select'); + const confirmationGroup = document.getElementById('restore-confirmation-group'); + const actionGroup = document.getElementById('restore-action-group'); + + if (!restoreSelect) return; + + if (restoreSelect.value) { + if (confirmationGroup) confirmationGroup.style.display = 'block'; + if (actionGroup) actionGroup.style.display = 'block'; + } else { + if (confirmationGroup) confirmationGroup.style.display = 'none'; + if (actionGroup) actionGroup.style.display = 'none'; + } + + this.validateRestoreConfirmation(); + }, + + validateRestoreConfirmation: function() { + const confirmationInput = document.getElementById('restore-confirmation'); + const restoreBtn = document.getElementById('restore-backup-btn'); + + if (!confirmationInput || !restoreBtn) return; + + const isValid = confirmationInput.value.toUpperCase() === 'RESTORE'; + restoreBtn.disabled = !isValid; + + if (isValid) { + restoreBtn.style.background = '#e74c3c'; + restoreBtn.style.cursor = 'pointer'; + } else { + restoreBtn.style.background = '#6b7280'; + restoreBtn.style.cursor = 'not-allowed'; + } + }, + + restoreBackup: function() { + const restoreSelect = document.getElementById('restore-backup-select'); + const confirmationInput = document.getElementById('restore-confirmation'); + + if (!restoreSelect || !confirmationInput) return; + + const backupId = restoreSelect.value; + const confirmation = confirmationInput.value.toUpperCase(); + + if (!backupId || confirmation !== 'RESTORE') { + this.showError('Please select a backup and type RESTORE to confirm'); + return; + } + + // Final confirmation dialog + if (!confirm('This will permanently overwrite your current database. Are you absolutely sure?')) { + return; + } + + console.log('[BackupRestore] Restoring backup:', backupId); + + const restoreBtn = document.getElementById('restore-backup-btn'); + if (restoreBtn) { + restoreBtn.disabled = true; + restoreBtn.innerHTML = ' Restoring...'; + } + + fetch('./api/backup/restore', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + backup_id: backupId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.showSuccess('Database restored successfully! Reloading page...'); + + // Reload the page after a short delay + setTimeout(() => { + window.location.reload(); + }, 2000); + } else { + throw new Error(data.error || 'Failed to restore backup'); + } + }) + .catch(error => { + console.error('[BackupRestore] Error restoring backup:', error); + this.showError('Failed to restore backup: ' + error.message); + }) + .finally(() => { + if (restoreBtn) { + restoreBtn.disabled = false; + restoreBtn.innerHTML = ' Restore Database'; + } + }); + }, + + validateDeleteConfirmation: function() { + const confirmationInput = document.getElementById('delete-confirmation'); + const actionGroup = document.getElementById('delete-action-group'); + const deleteBtn = document.getElementById('delete-database-btn'); + + if (!confirmationInput || !actionGroup || !deleteBtn) return; + + const isValid = confirmationInput.value.toLowerCase() === 'huntarr'; + + if (isValid) { + actionGroup.style.display = 'block'; + deleteBtn.disabled = false; + deleteBtn.style.background = '#e74c3c'; + deleteBtn.style.cursor = 'pointer'; + } else { + actionGroup.style.display = 'none'; + deleteBtn.disabled = true; + } + }, + + deleteDatabase: function() { + const confirmationInput = document.getElementById('delete-confirmation'); + + if (!confirmationInput || confirmationInput.value.toLowerCase() !== 'huntarr') { + this.showError('Please type "huntarr" to confirm database deletion'); + return; + } + + // Final confirmation dialog + if (!confirm('This will PERMANENTLY DELETE your entire Huntarr database. This action CANNOT be undone. Are you absolutely sure?')) { + return; + } + + console.log('[BackupRestore] Deleting database'); + + const deleteBtn = document.getElementById('delete-database-btn'); + if (deleteBtn) { + deleteBtn.disabled = true; + deleteBtn.innerHTML = ' Deleting...'; + } + + fetch('./api/backup/delete-database', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.showSuccess('Database deleted successfully! Redirecting to setup...'); + + // Redirect to setup after a short delay + setTimeout(() => { + window.location.href = './setup'; + }, 2000); + } else { + throw new Error(data.error || 'Failed to delete database'); + } + }) + .catch(error => { + console.error('[BackupRestore] Error deleting database:', error); + this.showError('Failed to delete database: ' + error.message); + }) + .finally(() => { + if (deleteBtn) { + deleteBtn.disabled = false; + deleteBtn.innerHTML = ' Delete Database'; + } + }); + }, + + deleteBackup: function(backupId) { + if (!confirm('Are you sure you want to delete this backup? This action cannot be undone.')) { + return; + } + + console.log('[BackupRestore] Deleting backup:', backupId); + + fetch('./api/backup/delete', { + method: 'POST', + headers: { + 'Content-Type': 'application/json' + }, + body: JSON.stringify({ + backup_id: backupId + }) + }) + .then(response => response.json()) + .then(data => { + if (data.success) { + this.showSuccess('Backup deleted successfully'); + this.loadBackupList(); // Refresh the list + } else { + throw new Error(data.error || 'Failed to delete backup'); + } + }) + .catch(error => { + console.error('[BackupRestore] Error deleting backup:', error); + this.showError('Failed to delete backup: ' + error.message); + }); + }, + + formatFileSize: function(bytes) { + if (bytes === 0) return '0 Bytes'; + + const k = 1024; + const sizes = ['Bytes', 'KB', 'MB', 'GB']; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + + return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; + }, + + showSuccess: function(message) { + this.showNotification(message, 'success'); + }, + + showError: function(message) { + this.showNotification(message, 'error'); + }, + + showNotification: function(message, type) { + // Create notification element + const notification = document.createElement('div'); + notification.className = `backup-notification ${type}`; + notification.innerHTML = ` + + ${message} + + `; + + // Add styles + notification.style.cssText = ` + position: fixed; + top: 20px; + right: 20px; + background: ${type === 'success' ? '#10b981' : '#e74c3c'}; + color: white; + padding: 12px 16px; + border-radius: 6px; + box-shadow: 0 4px 12px rgba(0,0,0,0.3); + z-index: 10000; + display: flex; + align-items: center; + gap: 8px; + max-width: 400px; + animation: slideIn 0.3s ease; + `; + + // Add animation styles + const style = document.createElement('style'); + style.textContent = ` + @keyframes slideIn { + from { transform: translateX(100%); opacity: 0; } + to { transform: translateX(0); opacity: 1; } + } + .backup-notification button { + background: none; + border: none; + color: white; + font-size: 18px; + cursor: pointer; + padding: 0; + margin-left: auto; + } + `; + document.head.appendChild(style); + + document.body.appendChild(notification); + + // Auto-remove after 5 seconds + setTimeout(() => { + if (notification.parentElement) { + notification.remove(); + } + }, 5000); + } +}; + +// Initialize when DOM is ready +document.addEventListener('DOMContentLoaded', function() { + // Don't auto-initialize - let the main UI handle it + console.log('[BackupRestore] Module loaded'); +}); \ No newline at end of file diff --git a/frontend/static/js/new-main.js b/frontend/static/js/new-main.js index 5678a5e8..74f7f878 100644 --- a/frontend/static/js/new-main.js +++ b/frontend/static/js/new-main.js @@ -86,7 +86,7 @@ let huntarrUI = { // Check which sidebar should be shown based on current section console.log(`[huntarrUI] Initialization - current section: ${this.currentSection}`); - if (this.currentSection === 'settings' || this.currentSection === 'scheduling' || this.currentSection === 'notifications' || this.currentSection === 'user') { + if (this.currentSection === 'settings' || this.currentSection === 'scheduling' || this.currentSection === 'notifications' || this.currentSection === 'backup-restore' || this.currentSection === 'user') { console.log('[huntarrUI] Initialization - showing settings sidebar'); this.showSettingsSidebar(); } else if (this.currentSection === 'requestarr' || this.currentSection === 'requestarr-history') { @@ -781,6 +781,21 @@ let huntarrUI = { // Initialize notifications settings if not already done this.initializeNotifications(); + } else if (section === 'backup-restore' && document.getElementById('backupRestoreSection')) { + document.getElementById('backupRestoreSection').classList.add('active'); + document.getElementById('backupRestoreSection').style.display = 'block'; + if (document.getElementById('settingsBackupRestoreNav')) document.getElementById('settingsBackupRestoreNav').classList.add('active'); + newTitle = 'Backup / Restore'; + this.currentSection = 'backup-restore'; + + // Switch to Settings sidebar for backup/restore + this.showSettingsSidebar(); + + // Set localStorage to maintain Settings sidebar preference + localStorage.setItem('huntarr-settings-sidebar', 'true'); + + // Initialize backup/restore functionality if not already done + this.initializeBackupRestore(); } else if (section === 'prowlarr' && document.getElementById('prowlarrSection')) { document.getElementById('prowlarrSection').classList.add('active'); document.getElementById('prowlarrSection').style.display = 'block'; @@ -5005,6 +5020,17 @@ let huntarrUI = { }); }, + initializeBackupRestore: function() { + console.log('[huntarrUI] initializeBackupRestore called'); + + // Initialize backup/restore functionality + if (typeof BackupRestore !== 'undefined') { + BackupRestore.initialize(); + } else { + console.error('[huntarrUI] BackupRestore module not loaded'); + } + }, + initializeProwlarr: function() { console.log('[huntarrUI] initializeProwlarr called'); diff --git a/frontend/templates/components/backup_restore_section.html b/frontend/templates/components/backup_restore_section.html new file mode 100644 index 00000000..c79b6e2e --- /dev/null +++ b/frontend/templates/components/backup_restore_section.html @@ -0,0 +1,390 @@ + + + + \ No newline at end of file diff --git a/frontend/templates/components/sidebar.html b/frontend/templates/components/sidebar.html index 1a650879..8fca74aa 100644 --- a/frontend/templates/components/sidebar.html +++ b/frontend/templates/components/sidebar.html @@ -293,6 +293,13 @@ Notifications + + + Backup / Restore + +