diff --git a/src/public/app.js b/src/public/app.js index 339e69a34..fc7ad09d1 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -121,20 +121,18 @@ document.addEventListener('DOMContentLoaded', function() { let containersData = []; let metricsData = []; let dashboardData = []; - // ---> CHANGE: pbsData becomes pbsDataArray - // let pbsData = {}; // Add state for PBS data - let pbsDataArray = []; // Holds data for multiple PBS instances - // <--- END CHANGE + let pbsDataArray = []; // Load saved sort state from localStorage or use defaults const savedSortState = JSON.parse(localStorage.getItem('pulseSortState')) || {}; const sortState = { nodes: { column: null, direction: 'asc', ...(savedSortState.nodes || {}) }, - main: { column: 'id', direction: 'asc', ...(savedSortState.main || {}) }, - // Add default sort states for PBS task tables - pbsBackup: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsBackup || {}) }, - pbsVerify: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsVerify || {}) }, - pbsSync: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsSync || {}) }, - pbsPruneGc: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsPruneGc || {}) } + main: { column: 'id', direction: 'asc', ...(savedSortState.main || {}) } + // ---> REMOVE: PBS sort state <--- + // pbsBackup: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsBackup || {}) }, + // pbsVerify: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsVerify || {}) }, + // pbsSync: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsSync || {}) }, + // pbsPruneGc: { column: 'startTime', direction: 'desc', ...(savedSortState.pbsPruneGc || {}) } + // ---> END REMOVE <--- }; let groupByNode = true; // Default view let filterGuestType = 'all'; // Default filter @@ -401,44 +399,13 @@ document.addEventListener('DOMContentLoaded', function() { }); } - // --- NEW PBS Task Sorting Function --- + // --- NEW PBS Task Sorting Function --- // ---> REMOVE FUNCTION <--- + /* function sortPbsTasks(data, column, direction) { - if (!column || !data) return data || []; - const dataToSort = [...data]; - - return dataToSort.sort((a, b) => { - let valueA = a ? a[column] : null; - let valueB = b ? b[column] : null; - - // Handle specific types for comparison - if (column === 'startTime' || column === 'endTime' || column === 'duration') { - valueA = Number(valueA) || 0; - valueB = Number(valueB) || 0; - return direction === 'asc' ? valueA - valueB : valueB - valueA; - } else if (column === 'id') { // Guest ID/Job ID/Datastore - valueA = String(valueA || '').toLowerCase(); - valueB = String(valueB || '').toLowerCase(); - return direction === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); - } else if (column === 'status') { - // Simple status sort (OK > running > Error/Other > null/undefined) - const getStatusValue = (status) => { - if (status === 'OK') return 0; - if (status === 'running') return 1; - if (!status) return 3; - return 2; // Other error statuses - }; - valueA = getStatusValue(a?.status); - valueB = getStatusValue(b?.status); - return direction === 'asc' ? valueA - valueB : valueB - valueA; - } else { - // Default string comparison - valueA = String(valueA || '').toLowerCase(); - valueB = String(valueB || '').toLowerCase(); - return direction === 'asc' ? valueA.localeCompare(valueB) : valueB.localeCompare(valueA); - } - }); + // ... function content ... } - // --- END NEW PBS Task Sorting Function --- + */ + // --- END NEW PBS Task Sorting Function --- // ---> END REMOVE <--- // --- Data Update/Display Functions --- function updateNodesTable(nodes, skipSorting = false) { @@ -1464,7 +1431,7 @@ document.addEventListener('DOMContentLoaded', function() { }); // --- End fetch version --- - // --- Helper Functions for PBS Tab --- + // --- Helper Functions for PBS Tab --- // MOVED UP const formatPbsTimestamp = (ts) => { if (!ts) return 'Never'; try { @@ -1553,8 +1520,8 @@ document.addEventListener('DOMContentLoaded', function() { } } - // --- NEW Function to Populate a PBS Task Table --- - function populatePbsTaskTable(tbodyId, tasks, sortColumn, sortDirection) { + // --- NEW Function to Populate a PBS Task Table --- // MOVED UP & MODIFIED + function populatePbsTaskTable(tbodyId, tasks) { // Remove sortColumn, sortDirection const tbody = document.getElementById(tbodyId); if (!tbody) { console.error(`PBS Task Table Body #${tbodyId} not found!`); @@ -1562,14 +1529,17 @@ document.addEventListener('DOMContentLoaded', function() { } tbody.innerHTML = ''; // Clear previous content - const sortedTasks = sortPbsTasks(tasks, sortColumn, sortDirection); // Assumes sortPbsTasks is moved up + // ---> REMOVE SORTING <--- + // const sortedTasks = sortPbsTasks(tasks, sortColumn, sortDirection); // Assumes sortPbsTasks is moved up + const tasksToDisplay = tasks || []; // Use raw tasks or empty array + // ---> END REMOVE <--- - if (!sortedTasks || sortedTasks.length === 0) { + if (!tasksToDisplay || tasksToDisplay.length === 0) { // Use tasksToDisplay tbody.innerHTML = `No recent tasks found (last 7 days).`; return; } - sortedTasks.forEach(task => { + tasksToDisplay.forEach(task => { // Use tasksToDisplay const row = document.createElement('tr'); row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; row.innerHTML = ` @@ -1592,16 +1562,15 @@ document.addEventListener('DOMContentLoaded', function() { return; } - // ---> FIX: Remove initial loading message if it exists <--- + // Remove initial loading message if it exists const loadingMessage = document.getElementById('pbs-loading-message'); if (loadingMessage) { loadingMessage.remove(); } - // ---> END FIX <--- + + console.log('[updatePbsInfo] Processing PBS array:', pbsArray); - console.log('[updatePbsInfo] Processing PBS array:', pbsArray); // Keep this log - - const currentInstanceIds = new Set(); // Keep track of instances processed in this run + const currentInstanceIds = new Set(); // Helper functions defined here or accessible in scope const createSummaryCard = (type, title, summaryData) => { @@ -1628,14 +1597,18 @@ document.addEventListener('DOMContentLoaded', function() { const createTaskTableHTML = (tableId, title, idColumnHeader) => `

Recent ${title} Tasks

-
+ // ---> REMOVE: max-h and overflow <--- +
+ // ---> END REMOVE <--- - - - - + // ---> REMOVE: sortable classes <--- + + + + + // ---> END REMOVE <--- @@ -1645,286 +1618,248 @@ document.addEventListener('DOMContentLoaded', function() {
${idColumnHeader}StatusStart TimeDuration${idColumnHeader}StatusStart TimeDurationUPID
`; - const setupPbsSortListener = (tableId, sortStateKey, pbsInstance) => { // Pass pbsInstance here - const tableElement = document.getElementById(tableId); - if (!tableElement) { - console.error(`PBS Task Table #${tableId} not found for sort listener setup!`); - return; - } + // ---> REMOVE: setupPbsSortListener function definition <--- + /* + const setupPbsSortListener = (tableId, sortStateKey, pbsInstance) => { + // ... function content ... + }; + */ + // ---> END REMOVE <--- - tableElement.querySelectorAll('th.sortable').forEach(th => { - if (th.dataset.listenerAttached === 'true') return; - - th.addEventListener('click', () => { - const column = th.getAttribute('data-sort'); - if (!column) return; - - const currentSort = sortState[sortStateKey]; - if (currentSort.column === column) { - currentSort.direction = currentSort.direction === 'asc' ? 'desc' : 'asc'; - } else { - currentSort.column = column; - currentSort.direction = 'asc'; - } - let tasks; - switch (sortStateKey) { - case 'pbsBackup': tasks = pbsInstance.backupTasks?.recentTasks; break; - case 'pbsVerify': tasks = pbsInstance.verificationTasks?.recentTasks; break; - case 'pbsSync': tasks = pbsInstance.syncTasks?.recentTasks; break; - case 'pbsPruneGc': tasks = pbsInstance.pruneTasks?.recentTasks; break; - default: tasks = []; - } - - const tbodyId = tableId.replace('-table-', '-tbody-'); - populatePbsTaskTable(tbodyId, tasks, currentSort.column, currentSort.direction); - updateSortUI(tableId, th, sortStateKey); // Pass key on click - }); - th.dataset.listenerAttached = 'true'; - }); - - // Call for initial UI state: - const currentSortConfig = sortState[sortStateKey]; - if (!currentSortConfig || !currentSortConfig.column) { - console.warn(`Initial sort column not found for state key '${sortStateKey}'. Skipping initial sort UI update for table ${tableId}.`); - return; - } - const initialHeader = tableElement.querySelector(`th[data-sort="${currentSortConfig.column}"]`); - // ---> CONFIRM: Pass the sortStateKey explicitly <--- - if (initialHeader) updateSortUI(tableId, initialHeader, sortStateKey); - // ---> END CONFIRM <--- - }; - - - // --- Create/Update content for each PBS instance --- + // Create/Update content for each PBS instance pbsArray.forEach((pbsInstance, index) => { - const instanceId = pbsInstance.pbsEndpointId || `instance-${index}`; - const instanceName = pbsInstance.pbsInstanceName || `PBS Instance ${index + 1}`; - const instanceElementId = `pbs-instance-${instanceId}`; - currentInstanceIds.add(instanceElementId); // Track this ID + const instanceId = pbsInstance.pbsEndpointId || `instance-${index}`; + const instanceName = pbsInstance.pbsInstanceName || `PBS Instance ${index + 1}`; + const instanceElementId = `pbs-instance-${instanceId}`; + currentInstanceIds.add(instanceElementId); - let instanceWrapper = document.getElementById(instanceElementId); - let detailsContainer, dsTableBody, statusElement; + let instanceWrapper = document.getElementById(instanceElementId); + let detailsContainer, dsTableBody, statusElement; - // Determine Status Text and Detail Visibility - let statusText = 'Loading...'; - let showDetails = false; - let statusColorClass = 'text-gray-600 dark:text-gray-400'; - switch (pbsInstance.status) { - case 'configured': - statusText = `Configured (${pbsInstance.nodeName || '...'}), attempting connection...`; - statusColorClass = 'text-gray-600 dark:text-gray-400'; - break; - case 'ok': - statusText = `Status: OK (${pbsInstance.nodeName || 'Unknown Node'})`; - statusColorClass = 'text-green-600 dark:text-green-400'; - showDetails = true; - break; - case 'error': - statusText = `Error connecting (${pbsInstance.errorMessage || 'Check Pulse logs.'})`; // Try to show specific error - statusColorClass = 'text-red-600 dark:text-red-400'; - break; - // 'unconfigured' is handled by the empty array check earlier - default: - statusText = `Status: ${pbsInstance.status || 'Unknown'}`; - break; - } + // Determine Status Text and Detail Visibility + let statusText = 'Loading...'; + let showDetails = false; + let statusColorClass = 'text-gray-600 dark:text-gray-400'; + switch (pbsInstance.status) { + case 'configured': + statusText = `Configured (${pbsInstance.nodeName || '...'}), attempting connection...`; + statusColorClass = 'text-gray-600 dark:text-gray-400'; + break; + case 'ok': + statusText = `Status: OK (${pbsInstance.nodeName || 'Unknown Node'})`; + statusColorClass = 'text-green-600 dark:text-green-400'; + showDetails = true; + break; + case 'error': + statusText = `Error connecting (${pbsInstance.errorMessage || 'Check Pulse logs.'})`; // Try to show specific error + statusColorClass = 'text-red-600 dark:text-red-400'; + break; + // 'unconfigured' is handled by the empty array check earlier + default: + statusText = `Status: ${pbsInstance.status || 'Unknown'}`; + break; + } - if (instanceWrapper) { - // --- Instance Exists: Update --- - statusElement = instanceWrapper.querySelector(`#pbs-status-${instanceId}`); - detailsContainer = instanceWrapper.querySelector(`#pbs-details-${instanceId}`); + if (instanceWrapper) { + // Instance Exists: Update + statusElement = instanceWrapper.querySelector(`#pbs-status-${instanceId}`); + detailsContainer = instanceWrapper.querySelector(`#pbs-details-${instanceId}`); - // Update status text and color - if (statusElement) { - statusElement.textContent = statusText; - statusElement.className = `text-sm ${statusColorClass}`; // Update classes - } - - if (detailsContainer) { - // Update Datastore Table Body - dsTableBody = detailsContainer.querySelector(`#pbs-ds-tbody-${instanceId}`); - if (dsTableBody) { - dsTableBody.innerHTML = ''; // Clear existing rows - if (showDetails && pbsInstance.datastores) { - if (pbsInstance.datastores.length === 0) { - dsTableBody.innerHTML = `No PBS datastores found or accessible.`; - } else { - pbsInstance.datastores.forEach(ds => { - const totalBytes = ds.total || 0; - const usedBytes = ds.used || 0; - const availableBytes = (ds.available !== null && ds.available !== undefined) ? ds.available : (totalBytes > 0 ? totalBytes - usedBytes : 0); - const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0; - const usageColor = getUsageColor(usagePercent); - const usageText = totalBytes > 0 ? `${usagePercent}% (${formatBytes(usedBytes)} of ${formatBytes(totalBytes)})` : 'N/A'; - const gcStatusHtml = getPbsGcStatusText(ds.gcStatus); - const row = document.createElement('tr'); - row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; - row.innerHTML = `${ds.name || 'N/A'} ${ds.path || 'N/A'} ${formatBytes(usedBytes)} ${formatBytes(availableBytes)} ${totalBytes > 0 ? formatBytes(totalBytes) : 'N/A'} ${totalBytes > 0 ? createProgressTextBarHTML(usagePercent, usageText, usageColor) : '-'} ${gcStatusHtml}`; - dsTableBody.appendChild(row); - }); - } - } else { // Not OK status, show status text in table - dsTableBody.innerHTML = `${statusText}`; - } - } - - // Update Task Summary Cards - const summariesSection = detailsContainer.querySelector(`#pbs-summaries-section-${instanceId}`); - if (summariesSection) { - summariesSection.innerHTML = ''; // Clear existing cards - summariesSection.appendChild(createSummaryCard('backup', 'Backups', pbsInstance.backupTasks)); - summariesSection.appendChild(createSummaryCard('verify', 'Verification', pbsInstance.verificationTasks)); - summariesSection.appendChild(createSummaryCard('sync', 'Sync', pbsInstance.syncTasks)); - summariesSection.appendChild(createSummaryCard('prune', 'Prune/GC', pbsInstance.pruneTasks)); - } - - // Update Task Tables - if (showDetails) { - populatePbsTaskTable(`pbs-recent-backup-tasks-tbody-${instanceId}`, pbsInstance.backupTasks?.recentTasks, sortState.pbsBackup.column, sortState.pbsBackup.direction); - populatePbsTaskTable(`pbs-recent-verify-tasks-tbody-${instanceId}`, pbsInstance.verificationTasks?.recentTasks, sortState.pbsVerify.column, sortState.pbsVerify.direction); - populatePbsTaskTable(`pbs-recent-sync-tasks-tbody-${instanceId}`, pbsInstance.syncTasks?.recentTasks, sortState.pbsSync.column, sortState.pbsSync.direction); - populatePbsTaskTable(`pbs-recent-prunegc-tasks-tbody-${instanceId}`, pbsInstance.pruneTasks?.recentTasks, sortState.pbsPruneGc.column, sortState.pbsPruneGc.direction); - // Re-apply/Ensure sort listeners exist - setupPbsSortListener(`pbs-recent-backup-tasks-table-${instanceId}`, 'pbsBackup', pbsInstance); - setupPbsSortListener(`pbs-recent-verify-tasks-table-${instanceId}`, 'pbsVerify', pbsInstance); - setupPbsSortListener(`pbs-recent-sync-tasks-table-${instanceId}`, 'pbsSync', pbsInstance); - setupPbsSortListener(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'pbsPruneGc', pbsInstance); - } else { - // If details are hidden, ensure tables show the status message - const backupTbody = document.getElementById(`pbs-recent-backup-tasks-tbody-${instanceId}`); - if (backupTbody) backupTbody.innerHTML = `${statusText}`; - const verifyTbody = document.getElementById(`pbs-recent-verify-tasks-tbody-${instanceId}`); - if (verifyTbody) verifyTbody.innerHTML = `${statusText}`; - const syncTbody = document.getElementById(`pbs-recent-sync-tasks-tbody-${instanceId}`); - if (syncTbody) syncTbody.innerHTML = `${statusText}`; - const pruneTbody = document.getElementById(`pbs-recent-prunegc-tasks-tbody-${instanceId}`); - if (pruneTbody) pruneTbody.innerHTML = `${statusText}`; - } - - // Set visibility of the details container - detailsContainer.classList.toggle('hidden', !showDetails); - } // end if(detailsContainer) - - } else { - // --- Instance Doesn't Exist: Create --- - instanceWrapper = document.createElement('div'); - instanceWrapper.className = 'pbs-instance-section border border-gray-200 dark:border-gray-700 rounded p-4 mb-4 bg-gray-50/30 dark:bg-gray-800/30'; - instanceWrapper.id = instanceElementId; - - // Header - const headerDiv = document.createElement('div'); - headerDiv.className = 'flex justify-between items-center mb-3'; - const instanceTitle = document.createElement('h3'); - instanceTitle.className = 'text-lg font-semibold text-gray-800 dark:text-gray-200'; - instanceTitle.textContent = instanceName; - statusElement = document.createElement('div'); - statusElement.className = `text-sm ${statusColorClass}`; - statusElement.id = `pbs-status-${instanceId}`; + // Update status text and color + if (statusElement) { statusElement.textContent = statusText; - headerDiv.appendChild(instanceTitle); - headerDiv.appendChild(statusElement); - instanceWrapper.appendChild(headerDiv); + statusElement.className = `text-sm ${statusColorClass}`; + } - // Details Container - detailsContainer = document.createElement('div'); - detailsContainer.className = `pbs-instance-details space-y-4 ${showDetails ? '' : 'hidden'}`; - detailsContainer.id = `pbs-details-${instanceId}`; + // Update contents IF the details container exists + if (detailsContainer) { + // Update Datastore Table Body + dsTableBody = detailsContainer.querySelector(`#pbs-ds-tbody-${instanceId}`); + if (dsTableBody) { + dsTableBody.innerHTML = ''; + if (showDetails && pbsInstance.datastores) { + if (pbsInstance.datastores.length === 0) { + dsTableBody.innerHTML = `No PBS datastores found or accessible.`; + } else { + pbsInstance.datastores.forEach(ds => { + const totalBytes = ds.total || 0; + const usedBytes = ds.used || 0; + const availableBytes = (ds.available !== null && ds.available !== undefined) ? ds.available : (totalBytes > 0 ? totalBytes - usedBytes : 0); + const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0; + const usageColor = getUsageColor(usagePercent); + const usageText = totalBytes > 0 ? `${usagePercent}% (${formatBytes(usedBytes)} of ${formatBytes(totalBytes)})` : 'N/A'; + const gcStatusHtml = getPbsGcStatusText(ds.gcStatus); + const row = document.createElement('tr'); + row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; + row.innerHTML = `${ds.name || 'N/A'} ${ds.path || 'N/A'} ${formatBytes(usedBytes)} ${formatBytes(availableBytes)} ${totalBytes > 0 ? formatBytes(totalBytes) : 'N/A'} ${totalBytes > 0 ? createProgressTextBarHTML(usagePercent, usageText, usageColor) : '-'} ${gcStatusHtml}`; + dsTableBody.appendChild(row); + }); + } + } else { + dsTableBody.innerHTML = `${statusText}`; + } + } - // Datastores Section - const dsSection = document.createElement('div'); - dsSection.id = `pbs-ds-section-${instanceId}`; - dsSection.innerHTML = ` -

Datastores

-
- - - -
Name Path Used Available Total Usage GC Status
-
`; - detailsContainer.appendChild(dsSection); + // Update Task Summary Cards + const summariesSection = detailsContainer.querySelector(`#pbs-summaries-section-${instanceId}`); + if (summariesSection) { + summariesSection.innerHTML = ''; // Clear existing cards + summariesSection.appendChild(createSummaryCard('backup', 'Backups', pbsInstance.backupTasks)); + summariesSection.appendChild(createSummaryCard('verify', 'Verification', pbsInstance.verificationTasks)); + summariesSection.appendChild(createSummaryCard('sync', 'Sync', pbsInstance.syncTasks)); + summariesSection.appendChild(createSummaryCard('prune', 'Prune/GC', pbsInstance.pruneTasks)); + } - // Task Summaries Section - const summariesSection = document.createElement('div'); - summariesSection.id = `pbs-summaries-section-${instanceId}`; - summariesSection.className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'; - summariesSection.appendChild(createSummaryCard('backup', 'Backups', pbsInstance.backupTasks)); - summariesSection.appendChild(createSummaryCard('verify', 'Verification', pbsInstance.verificationTasks)); - summariesSection.appendChild(createSummaryCard('sync', 'Sync', pbsInstance.syncTasks)); - summariesSection.appendChild(createSummaryCard('prune', 'Prune/GC', pbsInstance.pruneTasks)); - detailsContainer.appendChild(summariesSection); + // Update Task Tables + if (showDetails) { + // ---> REMOVE: Sorting parameters <--- + populatePbsTaskTable(`pbs-recent-backup-tasks-tbody-${instanceId}`, pbsInstance.backupTasks?.recentTasks); + populatePbsTaskTable(`pbs-recent-verify-tasks-tbody-${instanceId}`, pbsInstance.verificationTasks?.recentTasks); + populatePbsTaskTable(`pbs-recent-sync-tasks-tbody-${instanceId}`, pbsInstance.syncTasks?.recentTasks); + populatePbsTaskTable(`pbs-recent-prunegc-tasks-tbody-${instanceId}`, pbsInstance.pruneTasks?.recentTasks); + // ---> END REMOVE <--- + // ---> REMOVE: Sort listener setup calls <--- + // setupPbsSortListener(`pbs-recent-backup-tasks-table-${instanceId}`, 'pbsBackup', pbsInstance); + // setupPbsSortListener(`pbs-recent-verify-tasks-table-${instanceId}`, 'pbsVerify', pbsInstance); + // setupPbsSortListener(`pbs-recent-sync-tasks-table-${instanceId}`, 'pbsSync', pbsInstance); + // setupPbsSortListener(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'pbsPruneGc', pbsInstance); + // ---> END REMOVE <--- + } else { + // If details are hidden, ensure tables show the status message + const backupTbody = document.getElementById(`pbs-recent-backup-tasks-tbody-${instanceId}`); + if (backupTbody) backupTbody.innerHTML = `${statusText}`; + const verifyTbody = document.getElementById(`pbs-recent-verify-tasks-tbody-${instanceId}`); + if (verifyTbody) verifyTbody.innerHTML = `${statusText}`; + const syncTbody = document.getElementById(`pbs-recent-sync-tasks-tbody-${instanceId}`); + if (syncTbody) syncTbody.innerHTML = `${statusText}`; + const pruneTbody = document.getElementById(`pbs-recent-prunegc-tasks-tbody-${instanceId}`); + if (pruneTbody) pruneTbody.innerHTML = `${statusText}`; + } - // Create Task Sections - const recentBackupTasksSection = document.createElement('div'); - recentBackupTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-backup-tasks-table-${instanceId}`, 'Backup', 'Guest'); - detailsContainer.appendChild(recentBackupTasksSection); - const recentVerifyTasksSection = document.createElement('div'); - recentVerifyTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-verify-tasks-table-${instanceId}`, 'Verification', 'Guest/Group'); - detailsContainer.appendChild(recentVerifyTasksSection); - const recentSyncTasksSection = document.createElement('div'); - recentSyncTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-sync-tasks-table-${instanceId}`, 'Sync', 'Job ID'); - detailsContainer.appendChild(recentSyncTasksSection); - const recentPruneGcTasksSection = document.createElement('div'); - recentPruneGcTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'Prune/GC', 'Datastore/Group'); - detailsContainer.appendChild(recentPruneGcTasksSection); + // Toggle visibility at the end + detailsContainer.classList.toggle('hidden', !showDetails); + } // End if(detailsContainer) - instanceWrapper.appendChild(detailsContainer); - container.appendChild(instanceWrapper); // Append new instance to DOM + } else { + // Instance Doesn't Exist: Create + instanceWrapper = document.createElement('div'); + instanceWrapper.className = 'pbs-instance-section border border-gray-200 dark:border-gray-700 rounded p-4 mb-4 bg-gray-50/30 dark:bg-gray-800/30'; + instanceWrapper.id = instanceElementId; - // Populate tables and attach listeners AFTER appending - dsTableBody = instanceWrapper.querySelector(`#pbs-ds-tbody-${instanceId}`); // Find the newly created body - if (dsTableBody) { - if (showDetails && pbsInstance.datastores) { - if (pbsInstance.datastores.length === 0) { dsTableBody.innerHTML = `No PBS datastores found or accessible.`; } - else { - // Populate rows - pbsInstance.datastores.forEach(ds => { - const totalBytes = ds.total || 0; - const usedBytes = ds.used || 0; - const availableBytes = (ds.available !== null && ds.available !== undefined) ? ds.available : (totalBytes > 0 ? totalBytes - usedBytes : 0); - const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0; - const usageColor = getUsageColor(usagePercent); - const usageText = totalBytes > 0 ? `${usagePercent}% (${formatBytes(usedBytes)} of ${formatBytes(totalBytes)})` : 'N/A'; - const gcStatusHtml = getPbsGcStatusText(ds.gcStatus); - const row = document.createElement('tr'); - row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; - row.innerHTML = `${ds.name || 'N/A'} ${ds.path || 'N/A'} ${formatBytes(usedBytes)} ${formatBytes(availableBytes)} ${totalBytes > 0 ? formatBytes(totalBytes) : 'N/A'} ${totalBytes > 0 ? createProgressTextBarHTML(usagePercent, usageText, usageColor) : '-'} ${gcStatusHtml}`; - dsTableBody.appendChild(row); - }); - } - } else { dsTableBody.innerHTML = `${statusText}`; } - } + // Header + const headerDiv = document.createElement('div'); + headerDiv.className = 'flex justify-between items-center mb-3'; + const instanceTitle = document.createElement('h3'); + instanceTitle.className = 'text-lg font-semibold text-gray-800 dark:text-gray-200'; + instanceTitle.textContent = instanceName; + statusElement = document.createElement('div'); + statusElement.className = `text-sm ${statusColorClass}`; + statusElement.id = `pbs-status-${instanceId}`; + statusElement.textContent = statusText; + headerDiv.appendChild(instanceTitle); + headerDiv.appendChild(statusElement); + instanceWrapper.appendChild(headerDiv); - if (showDetails) { - populatePbsTaskTable(`pbs-recent-backup-tasks-tbody-${instanceId}`, pbsInstance.backupTasks?.recentTasks, sortState.pbsBackup.column, sortState.pbsBackup.direction); - populatePbsTaskTable(`pbs-recent-verify-tasks-tbody-${instanceId}`, pbsInstance.verificationTasks?.recentTasks, sortState.pbsVerify.column, sortState.pbsVerify.direction); - populatePbsTaskTable(`pbs-recent-sync-tasks-tbody-${instanceId}`, pbsInstance.syncTasks?.recentTasks, sortState.pbsSync.column, sortState.pbsSync.direction); - populatePbsTaskTable(`pbs-recent-prunegc-tasks-tbody-${instanceId}`, pbsInstance.pruneTasks?.recentTasks, sortState.pbsPruneGc.column, sortState.pbsPruneGc.direction); + // Details Container + detailsContainer = document.createElement('div'); + detailsContainer.className = `pbs-instance-details space-y-4 ${showDetails ? '' : 'hidden'}`; + detailsContainer.id = `pbs-details-${instanceId}`; - setupPbsSortListener(`pbs-recent-backup-tasks-table-${instanceId}`, 'pbsBackup', pbsInstance); - setupPbsSortListener(`pbs-recent-verify-tasks-table-${instanceId}`, 'pbsVerify', pbsInstance); - setupPbsSortListener(`pbs-recent-sync-tasks-table-${instanceId}`, 'pbsSync', pbsInstance); - setupPbsSortListener(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'pbsPruneGc', pbsInstance); - } else { - // Ensure initial status message is present if details start hidden - const backupTbody = document.getElementById(`pbs-recent-backup-tasks-tbody-${instanceId}`); if (backupTbody) backupTbody.innerHTML = `${statusText}`; - const verifyTbody = document.getElementById(`pbs-recent-verify-tasks-tbody-${instanceId}`); if (verifyTbody) verifyTbody.innerHTML = `${statusText}`; - const syncTbody = document.getElementById(`pbs-recent-sync-tasks-tbody-${instanceId}`); if (syncTbody) syncTbody.innerHTML = `${statusText}`; - const pruneTbody = document.getElementById(`pbs-recent-prunegc-tasks-tbody-${instanceId}`); if (pruneTbody) pruneTbody.innerHTML = `${statusText}`; - } + // Datastores Section + const dsSection = document.createElement('div'); + dsSection.id = `pbs-ds-section-${instanceId}`; + dsSection.innerHTML = ` +

Datastores

+
+ + + +
Name Path Used Available Total Usage GC Status
+
`; + detailsContainer.appendChild(dsSection); - } // End Upsert Logic + // Task Summaries Section + const summariesSection = document.createElement('div'); + summariesSection.id = `pbs-summaries-section-${instanceId}`; + summariesSection.className = 'grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4'; + summariesSection.appendChild(createSummaryCard('backup', 'Backups', pbsInstance.backupTasks)); + summariesSection.appendChild(createSummaryCard('verify', 'Verification', pbsInstance.verificationTasks)); + summariesSection.appendChild(createSummaryCard('sync', 'Sync', pbsInstance.syncTasks)); + summariesSection.appendChild(createSummaryCard('prune', 'Prune/GC', pbsInstance.pruneTasks)); + detailsContainer.appendChild(summariesSection); - }); // End forEach pbsInstance + // Create Task Sections + const recentBackupTasksSection = document.createElement('div'); + recentBackupTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-backup-tasks-table-${instanceId}`, 'Backup', 'Guest'); + detailsContainer.appendChild(recentBackupTasksSection); + const recentVerifyTasksSection = document.createElement('div'); + recentVerifyTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-verify-tasks-table-${instanceId}`, 'Verification', 'Guest/Group'); + detailsContainer.appendChild(recentVerifyTasksSection); + const recentSyncTasksSection = document.createElement('div'); + recentSyncTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-sync-tasks-table-${instanceId}`, 'Sync', 'Job ID'); + detailsContainer.appendChild(recentSyncTasksSection); + const recentPruneGcTasksSection = document.createElement('div'); + recentPruneGcTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'Prune/GC', 'Datastore/Group'); + detailsContainer.appendChild(recentPruneGcTasksSection); - // --- Remove Orphaned Instances --- - container.querySelectorAll('.pbs-instance-section').forEach(el => { - if (!currentInstanceIds.has(el.id)) { - console.log(`Removing orphaned PBS instance element: ${el.id}`); - el.remove(); - } - }); + instanceWrapper.appendChild(detailsContainer); + container.appendChild(instanceWrapper); // Append new instance to DOM - } - // --- End Update PBS Info Function --- + // Populate tables and attach listeners AFTER appending + dsTableBody = instanceWrapper.querySelector(`#pbs-ds-tbody-${instanceId}`); // Find the newly created body + if (dsTableBody) { + if (showDetails && pbsInstance.datastores) { + if (pbsInstance.datastores.length === 0) { dsTableBody.innerHTML = `No PBS datastores found or accessible.`; } + else { + // Populate rows + pbsInstance.datastores.forEach(ds => { + const totalBytes = ds.total || 0; + const usedBytes = ds.used || 0; + const availableBytes = (ds.available !== null && ds.available !== undefined) ? ds.available : (totalBytes > 0 ? totalBytes - usedBytes : 0); + const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0; + const usageColor = getUsageColor(usagePercent); + const usageText = totalBytes > 0 ? `${usagePercent}% (${formatBytes(usedBytes)} of ${formatBytes(totalBytes)})` : 'N/A'; + const gcStatusHtml = getPbsGcStatusText(ds.gcStatus); + const row = document.createElement('tr'); + row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; + row.innerHTML = `${ds.name || 'N/A'} ${ds.path || 'N/A'} ${formatBytes(usedBytes)} ${formatBytes(availableBytes)} ${totalBytes > 0 ? formatBytes(totalBytes) : 'N/A'} ${totalBytes > 0 ? createProgressTextBarHTML(usagePercent, usageText, usageColor) : '-'} ${gcStatusHtml}`; + dsTableBody.appendChild(row); + }); + } + } else { dsTableBody.innerHTML = `${statusText}`; } + } + + if (showDetails) { + populatePbsTaskTable(`pbs-recent-backup-tasks-tbody-${instanceId}`, pbsInstance.backupTasks?.recentTasks); + populatePbsTaskTable(`pbs-recent-verify-tasks-tbody-${instanceId}`, pbsInstance.verificationTasks?.recentTasks); + populatePbsTaskTable(`pbs-recent-sync-tasks-tbody-${instanceId}`, pbsInstance.syncTasks?.recentTasks); + populatePbsTaskTable(`pbs-recent-prunegc-tasks-tbody-${instanceId}`, pbsInstance.pruneTasks?.recentTasks); + + setupPbsSortListener(`pbs-recent-backup-tasks-table-${instanceId}`, 'pbsBackup', pbsInstance); + setupPbsSortListener(`pbs-recent-verify-tasks-table-${instanceId}`, 'pbsVerify', pbsInstance); + setupPbsSortListener(`pbs-recent-sync-tasks-table-${instanceId}`, 'pbsSync', pbsInstance); + setupPbsSortListener(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'pbsPruneGc', pbsInstance); + } else { + // Ensure initial status message is present if details start hidden + const backupTbody = document.getElementById(`pbs-recent-backup-tasks-tbody-${instanceId}`); if (backupTbody) backupTbody.innerHTML = `${statusText}`; + const verifyTbody = document.getElementById(`pbs-recent-verify-tasks-tbody-${instanceId}`); if (verifyTbody) verifyTbody.innerHTML = `${statusText}`; + const syncTbody = document.getElementById(`pbs-recent-sync-tasks-tbody-${instanceId}`); if (syncTbody) syncTbody.innerHTML = `${statusText}`; + const pruneTbody = document.getElementById(`pbs-recent-prunegc-tasks-tbody-${instanceId}`); if (pruneTbody) pruneTbody.innerHTML = `${statusText}`; + } + + } // End Upsert Logic + + }); // End forEach pbsInstance + + // --- Remove Orphaned Instances --- + container.querySelectorAll('.pbs-instance-section').forEach(el => { + if (!currentInstanceIds.has(el.id)) { + console.log(`Removing orphaned PBS instance element: ${el.id}`); + el.remove(); + } + }); + +} +// --- End Update PBS Info Function --- }); // End DOMContentLoaded \ No newline at end of file