diff --git a/src/public/js/ui/backups.js b/src/public/js/ui/backups.js index 88446cc4d..c68ea2f78 100644 --- a/src/public/js/ui/backups.js +++ b/src/public/js/ui/backups.js @@ -29,47 +29,13 @@ PulseApp.ui.backups = (() => { } } - function updateBackupsTab() { - const tableContainer = document.getElementById('backups-table-container'); - const tableBody = document.getElementById('backups-overview-tbody'); - const loadingMsg = document.getElementById('backups-loading-message'); - const noDataMsg = document.getElementById('backups-no-data-message'); - const statusTextElement = document.getElementById('backups-status-text'); - - if (!tableContainer || !tableBody || !loadingMsg || !noDataMsg || !statusTextElement) { - console.error("UI elements for Backups tab not found!"); - return; - } - + function _getInitialBackupData() { const vmsData = PulseApp.state.get('vmsData') || []; const containersData = PulseApp.state.get('containersData') || []; const pbsDataArray = PulseApp.state.get('pbsDataArray') || []; const initialDataReceived = PulseApp.state.get('initialDataReceived'); - const allGuests = [...vmsData, ...containersData]; - if (!initialDataReceived) { - loadingMsg.classList.remove('hidden'); - tableContainer.classList.add('hidden'); - noDataMsg.classList.add('hidden'); - return; - } - - if (allGuests.length === 0) { - loadingMsg.classList.add('hidden'); - tableContainer.classList.add('hidden'); - noDataMsg.textContent = "No Proxmox guests (VMs/Containers) found."; - noDataMsg.classList.remove('hidden'); - return; - } - - loadingMsg.classList.add('hidden'); - - const backupStatusByGuest = []; - const now = Math.floor(Date.now() / 1000); - const sevenDaysAgo = now - (7 * 24 * 60 * 60); - const threeDaysAgo = now - (3 * 24 * 60 * 60); - const allRecentBackupTasks = pbsDataArray.flatMap(pbs => (pbs.backupTasks?.recentTasks || []).map(task => ({ ...task, @@ -91,75 +57,74 @@ PulseApp.ui.backups = (() => { ) ); - allGuests.forEach(guest => { - const guestId = String(guest.vmid); - const guestTypePve = guest.type === 'qemu' ? 'vm' : 'ct'; + return { allGuests, initialDataReceived, allRecentBackupTasks, allSnapshots }; + } - const guestSnapshots = allSnapshots.filter(snap => - String(snap.backupVMID) === guestId && snap.backupType === guestTypePve - ); - const totalBackups = guestSnapshots.length; - const latestSnapshot = guestSnapshots.reduce((latest, snap) => { - return (!latest || (snap['backup-time'] && snap['backup-time'] > latest['backup-time'])) ? snap : latest; - }, null); - const latestSnapshotTime = latestSnapshot ? latestSnapshot['backup-time'] : null; + function _determineGuestBackupStatus(guest, allSnapshots, allRecentBackupTasks) { + const guestId = String(guest.vmid); + const guestTypePve = guest.type === 'qemu' ? 'vm' : 'ct'; + const now = Math.floor(Date.now() / 1000); + const sevenDaysAgo = now - (7 * 24 * 60 * 60); + const threeDaysAgo = now - (3 * 24 * 60 * 60); - const guestTasks = allRecentBackupTasks.filter(task => - task.guestId === guestId && task.guestTypePbs === guestTypePve - ); - const latestTask = guestTasks.reduce((latest, task) => { - return (!latest || (task.startTime && task.startTime > latest.startTime)) ? task : latest; - }, null); + const guestSnapshots = allSnapshots.filter(snap => + String(snap.backupVMID) === guestId && snap.backupType === guestTypePve + ); + const totalBackups = guestSnapshots.length; + const latestSnapshot = guestSnapshots.reduce((latest, snap) => { + return (!latest || (snap['backup-time'] && snap['backup-time'] > latest['backup-time'])) ? snap : latest; + }, null); + const latestSnapshotTime = latestSnapshot ? latestSnapshot['backup-time'] : null; - let healthStatus = 'none'; - let displayTimestamp = latestSnapshotTime; + const guestTasks = allRecentBackupTasks.filter(task => + task.guestId === guestId && task.guestTypePbs === guestTypePve + ); + const latestTask = guestTasks.reduce((latest, task) => { + return (!latest || (task.startTime && task.startTime > latest.startTime)) ? task : latest; + }, null); - if (latestTask) { - displayTimestamp = latestTask.startTime; - if (latestTask.status === 'OK') { - if (latestTask.startTime >= threeDaysAgo) { - healthStatus = 'ok'; - } else if (latestTask.startTime >= sevenDaysAgo) { - healthStatus = 'stale'; - } else { - healthStatus = 'old'; - } - } else { - healthStatus = 'failed'; - } - } else if (latestSnapshotTime) { - if (latestSnapshotTime >= threeDaysAgo) { - healthStatus = 'ok'; - } else if (latestSnapshotTime >= sevenDaysAgo) { - healthStatus = 'stale'; - } else { - healthStatus = 'old'; - } + let healthStatus = 'none'; + let displayTimestamp = latestSnapshotTime; + + if (latestTask) { + displayTimestamp = latestTask.startTime; + if (latestTask.status === 'OK') { + if (latestTask.startTime >= threeDaysAgo) healthStatus = 'ok'; + else if (latestTask.startTime >= sevenDaysAgo) healthStatus = 'stale'; + else healthStatus = 'old'; } else { - healthStatus = 'none'; - displayTimestamp = null; + healthStatus = 'failed'; } + } else if (latestSnapshotTime) { + if (latestSnapshotTime >= threeDaysAgo) healthStatus = 'ok'; + else if (latestSnapshotTime >= sevenDaysAgo) healthStatus = 'stale'; + else healthStatus = 'old'; + } else { + healthStatus = 'none'; + displayTimestamp = null; + } - backupStatusByGuest.push({ - guestName: guest.name || `Guest ${guest.vmid}`, - guestId: guest.vmid, - guestType: guest.type === 'qemu' ? 'VM' : 'LXC', - node: guest.node, - guestPveStatus: guest.status, - latestBackupTime: displayTimestamp, - pbsInstanceName: latestSnapshot?.pbsInstanceName || latestTask?.pbsInstanceName || 'N/A', - datastoreName: latestSnapshot?.datastoreName || 'N/A', - totalBackups: totalBackups, - backupHealthStatus: healthStatus - }); - }); + return { + guestName: guest.name || `Guest ${guest.vmid}`, + guestId: guest.vmid, + guestType: guest.type === 'qemu' ? 'VM' : 'LXC', + node: guest.node, + guestPveStatus: guest.status, + latestBackupTime: displayTimestamp, + pbsInstanceName: latestSnapshot?.pbsInstanceName || latestTask?.pbsInstanceName || 'N/A', + datastoreName: latestSnapshot?.datastoreName || 'N/A', + totalBackups: totalBackups, + backupHealthStatus: healthStatus + }; + } + function _filterBackupData(backupStatusByGuest, backupsSearchInput) { const currentBackupsSearchTerm = backupsSearchInput ? backupsSearchInput.value.toLowerCase() : ''; const backupsSearchTerms = currentBackupsSearchTerm.split(',').map(term => term.trim()).filter(term => term); const backupsFilterHealth = PulseApp.state.get('backupsFilterHealth'); const backupsFilterGuestType = PulseApp.state.get('backupsFilterGuestType'); - const filteredBackupStatus = backupStatusByGuest.filter(item => { + return backupStatusByGuest.filter(item => { const healthMatch = (backupsFilterHealth === 'all') || (backupsFilterHealth === 'ok' && (item.backupHealthStatus === 'ok' || item.backupHealthStatus === 'stale')) || (backupsFilterHealth === 'warning' && (item.backupHealthStatus === 'old')) || @@ -173,98 +138,134 @@ PulseApp.ui.backups = (() => { if (!typeMatch) return false; if (backupsSearchTerms.length > 0) { - const nameMatch = backupsSearchTerms.some(term => + return backupsSearchTerms.some(term => (item.guestName?.toLowerCase() || '').includes(term) || (item.node?.toLowerCase() || '').includes(term) || (item.guestId?.toString() || '').includes(term) ); - if (!nameMatch) return false; } - return true; }); + } + + function _renderBackupTableRow(guestStatus) { + const row = document.createElement('tr'); + row.className = `transition-all duration-150 ease-out hover:bg-gray-100 dark:hover:bg-gray-700 hover:shadow-md hover:-translate-y-px ${guestStatus.guestPveStatus === 'stopped' ? 'opacity-60 grayscale' : ''}`; + + const latestBackupFormatted = guestStatus.latestBackupTime + ? PulseApp.utils.formatPbsTimestamp(guestStatus.latestBackupTime) + : 'No backups found'; + + let healthIndicator = ''; + switch (guestStatus.backupHealthStatus) { + case 'ok': healthIndicator = '●'; break; + case 'stale': healthIndicator = '●'; break; + case 'failed': healthIndicator = '✖'; break; + case 'old': healthIndicator = '●'; break; + case 'none': healthIndicator = '-'; break; + } + + const typeIconClass = guestStatus.guestType === 'VM' + ? 'vm-icon bg-blue-100 dark:bg-blue-900/50 text-blue-700 dark:text-blue-300 px-1.5 py-0.5 font-medium' + : 'ct-icon bg-green-100 dark:bg-green-900/50 text-green-700 dark:text-green-300 px-1.5 py-0.5 font-medium'; + const typeIcon = ``; + + row.innerHTML = ` +