From 4e2d99d1bd0edde13791875120f4909ed625905f Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Thu, 24 Apr 2025 15:53:02 +0100 Subject: [PATCH] fix: Handle 'N/A' string in PBS GC status display --- src/public/app.js | 239 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 184 insertions(+), 55 deletions(-) diff --git a/src/public/app.js b/src/public/app.js index 96b0d95d6..305f58e62 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -31,6 +31,9 @@ })(); +// Helper function to sanitize strings for use in HTML IDs +const sanitizeForId = (str) => str.replace(/[^a-zA-Z0-9-]/g, '-'); + document.addEventListener('DOMContentLoaded', function() { // Guard clauses to ensure essential elements exist before proceeding const themeToggleButton = document.getElementById('theme-toggle-button'); @@ -145,6 +148,9 @@ document.addEventListener('DOMContentLoaded', function() { // let pbsConfigured = false; // Flag to track if PBS is configured // <--- END REMOVE + // Define initial limit for PBS task tables + const INITIAL_PBS_TASK_LIMIT = 5; + // --- Global Helper for Text Progress Bar --- const createProgressTextBarHTML = (percent, text, colorClass) => { const numericPercent = isNaN(parseInt(percent)) ? 0 : parseInt(percent); @@ -1475,13 +1481,18 @@ document.addEventListener('DOMContentLoaded', function() { }; const getPbsGcStatusText = (gcStatus) => { - if (!gcStatus || gcStatus === 'unknown') return 'Unknown'; + // Handle falsy values, 'unknown', or literal 'N/A' string + if (!gcStatus || gcStatus === 'unknown' || gcStatus === 'N/A') { + return 'Unknown'; + } + // Determine color based on known status keywords let colorClass = 'text-gray-600 dark:text-gray-400'; if (gcStatus.includes('error') || gcStatus.includes('failed')) { colorClass = 'text-red-500 dark:text-red-400'; } else if (gcStatus === 'OK') { colorClass = 'text-green-500 dark:text-green-400'; } + // Return the original status text with appropriate color return `${gcStatus}`; }; @@ -1521,36 +1532,78 @@ document.addEventListener('DOMContentLoaded', function() { } // --- 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!`); + function populatePbsTaskTable(parentSectionElement, fullTasksArray) { + if (!parentSectionElement) { + console.error("Parent section element not provided to populatePbsTaskTable."); return; } + // Find elements relative to the parent section + const tbody = parentSectionElement.querySelector('.pbs-task-tbody'); + const buttonContainer = parentSectionElement.querySelector('.pbs-toggle-button-container'); + + if (!tbody || !buttonContainer) { + // If elements aren't found *within the parent*, log an error + console.error(`Required child elements (.pbs-task-tbody or .pbs-toggle-button-container) not found within provided parent section.`, parentSectionElement); + return; + } + + // Ensure tbody has an ID if it doesn't (needed for button linking) + if (!tbody.id) { + const table = parentSectionElement.querySelector('table'); + tbody.id = table && table.id ? table.id.replace('-table-', '-tbody-') : `pbs-tbody-${Date.now()}-${Math.random()}`; + console.warn(`Assigned dynamic ID to tbody: ${tbody.id}`); + } + tbody.innerHTML = ''; // Clear previous content + buttonContainer.innerHTML = ''; // Clear previous button - // ---> REMOVE SORTING <--- - // const sortedTasks = sortPbsTasks(tasks, sortColumn, sortDirection); // Assumes sortPbsTasks is moved up - const tasksToDisplay = tasks || []; // Use raw tasks or empty array - // ---> END REMOVE <--- + const tasks = fullTasksArray || []; // Ensure tasks is an array + const totalTasks = tasks.length; + const limit = INITIAL_PBS_TASK_LIMIT; + const isCurrentlyExpanded = tbody.dataset.isExpanded === 'true'; // Check current state - if (!tasksToDisplay || tasksToDisplay.length === 0) { // Use tasksToDisplay + const tasksToDisplay = isCurrentlyExpanded ? tasks : tasks.slice(0, limit); + + // Store full data and state on the tbody + tbody.dataset.fullTasks = JSON.stringify(tasks); // Store all tasks + tbody.dataset.initialLimit = limit; + tbody.dataset.isExpanded = isCurrentlyExpanded ? 'true' : 'false'; + + if (tasksToDisplay.length === 0) { tbody.innerHTML = `No recent tasks found (last 7 days).`; - return; + } else { + tasksToDisplay.forEach(task => { + const row = document.createElement('tr'); + row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; + row.innerHTML = ` + ${task.id || 'N/A'} + ${getPbsStatusIcon(task.status)} + ${formatPbsTimestamp(task.startTime)} + ${formatDuration(task.duration)} + ${task.upid || 'N/A'} + `; + tbody.appendChild(row); + }); } - tasksToDisplay.forEach(task => { // Use tasksToDisplay - const row = document.createElement('tr'); - row.className = 'hover:bg-gray-50 dark:hover:bg-gray-700/50'; - row.innerHTML = ` - ${task.id || 'N/A'} - ${getPbsStatusIcon(task.status)} - ${formatPbsTimestamp(task.startTime)} - ${formatDuration(task.duration)} - ${task.upid || 'N/A'} - `; - tbody.appendChild(row); - }); + // Add Show More/Less button if needed + if (totalTasks > limit) { + const button = document.createElement('button'); + const isExpanded = tbody.dataset.isExpanded === 'true'; + const buttonText = isExpanded ? 'Show Less' : `Show More (${totalTasks - limit} older)`; + const iconSvg = isExpanded + ? '' + : ''; + + button.innerHTML = buttonText + iconSvg; + button.type = 'button'; + // Updated classes for button styling + button.className = 'pbs-toggle-button text-xs px-2 py-1 rounded border border-gray-300 dark:border-gray-600 bg-gray-100 dark:bg-gray-700 text-gray-700 dark:text-gray-300 hover:bg-gray-200 dark:hover:bg-gray-600 focus:outline-none focus:ring-1 focus:ring-blue-500'; + button.dataset.targetTbodyId = tbody.id; // Use the actual ID of the tbody we found/assigned + buttonContainer.appendChild(button); + } else { + tbody.dataset.isExpanded = 'false'; + } } // --- END NEW Function --- @@ -1595,7 +1648,10 @@ document.addEventListener('DOMContentLoaded', function() { return card; // Return the element }; - const createTaskTableHTML = (tableId, title, idColumnHeader) => ` + const createTaskTableHTML = (tableId, title, idColumnHeader) => { + const tbodyId = tableId.replace('-table-', '-tbody-'); + const toggleButtonContainerId = tableId.replace('-table', '-toggle-container'); + return `

Recent ${title} Tasks

@@ -1608,11 +1664,14 @@ document.addEventListener('DOMContentLoaded', function() { - +
UPID
-
`; + +
+ `; + }; // ---> REMOVE: setupPbsSortListener function definition <--- /* @@ -1624,7 +1683,8 @@ document.addEventListener('DOMContentLoaded', function() { // Create/Update content for each PBS instance pbsArray.forEach((pbsInstance, index) => { - const instanceId = pbsInstance.pbsEndpointId || `instance-${index}`; + const rawInstanceId = pbsInstance.pbsEndpointId || `instance-${index}`; + const instanceId = sanitizeForId(rawInstanceId); // Use sanitized ID for elements const instanceName = pbsInstance.pbsInstanceName || `PBS Instance ${index + 1}`; const instanceElementId = `pbs-instance-${instanceId}`; currentInstanceIds.add(instanceElementId); @@ -1638,16 +1698,19 @@ document.addEventListener('DOMContentLoaded', function() { let statusColorClass = 'text-gray-600 dark:text-gray-400'; switch (pbsInstance.status) { case 'configured': - statusText = `Configured (${pbsInstance.nodeName || '...'}), attempting connection...`; + // statusText = `Configured (${pbsInstance.nodeName || '...'}), attempting connection...`; // Original + statusText = `Configured, attempting connection...`; // Simplified statusColorClass = 'text-gray-600 dark:text-gray-400'; break; case 'ok': - statusText = `Status: OK (${pbsInstance.nodeName || 'Unknown Node'})`; + // statusText = `Status: OK (${pbsInstance.nodeName || 'Unknown Node'})`; // Original + statusText = `Status: OK`; // Simplified 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 + // statusText = `Error connecting (${pbsInstance.errorMessage || 'Check Pulse logs.'})`; // Original + statusText = `Error: ${pbsInstance.errorMessage || 'Connection failed'}`; // Simplified statusColorClass = 'text-red-600 dark:text-red-400'; break; // 'unconfigured' is handled by the empty array check earlier @@ -1708,18 +1771,18 @@ document.addEventListener('DOMContentLoaded', function() { // 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 <--- + const backupSection = detailsContainer.querySelector('.pbs-task-section[data-task-type="backup"]'); + if (backupSection) populatePbsTaskTable(backupSection, pbsInstance.backupTasks?.recentTasks); + + const verifySection = detailsContainer.querySelector('.pbs-task-section[data-task-type="verify"]'); + if (verifySection) populatePbsTaskTable(verifySection, pbsInstance.verificationTasks?.recentTasks); + + const syncSection = detailsContainer.querySelector('.pbs-task-section[data-task-type="sync"]'); + if (syncSection) populatePbsTaskTable(syncSection, pbsInstance.syncTasks?.recentTasks); + + const pruneGcSection = detailsContainer.querySelector('.pbs-task-section[data-task-type="prunegc"]'); + if (pruneGcSection) populatePbsTaskTable(pruneGcSection, pbsInstance.pruneTasks?.recentTasks); + } else { // If details are hidden, ensure tables show the status message const backupTbody = document.getElementById(`pbs-recent-backup-tasks-tbody-${instanceId}`); @@ -1768,7 +1831,17 @@ document.addEventListener('DOMContentLoaded', function() {

Datastores

- + + + + + + + + + + +
Name Path Used Available Total Usage GC Status
NamePathUsedAvailableTotalUsageGC Status
`; @@ -1786,15 +1859,26 @@ document.addEventListener('DOMContentLoaded', function() { // Create Task Sections const recentBackupTasksSection = document.createElement('div'); + recentBackupTasksSection.className = 'pbs-task-section'; // Add class + recentBackupTasksSection.dataset.taskType = 'backup'; // Add data attribute recentBackupTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-backup-tasks-table-${instanceId}`, 'Backup', 'Guest'); detailsContainer.appendChild(recentBackupTasksSection); + const recentVerifyTasksSection = document.createElement('div'); + recentVerifyTasksSection.className = 'pbs-task-section'; // Add class + recentVerifyTasksSection.dataset.taskType = 'verify'; // Add data attribute recentVerifyTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-verify-tasks-table-${instanceId}`, 'Verification', 'Guest/Group'); detailsContainer.appendChild(recentVerifyTasksSection); + const recentSyncTasksSection = document.createElement('div'); + recentSyncTasksSection.className = 'pbs-task-section'; // Add class + recentSyncTasksSection.dataset.taskType = 'sync'; // Add data attribute recentSyncTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-sync-tasks-table-${instanceId}`, 'Sync', 'Job ID'); detailsContainer.appendChild(recentSyncTasksSection); + const recentPruneGcTasksSection = document.createElement('div'); + recentPruneGcTasksSection.className = 'pbs-task-section'; // Add class + recentPruneGcTasksSection.dataset.taskType = 'prunegc'; // Add data attribute recentPruneGcTasksSection.innerHTML = createTaskTableHTML(`pbs-recent-prunegc-tasks-table-${instanceId}`, 'Prune/GC', 'Datastore/Group'); detailsContainer.appendChild(recentPruneGcTasksSection); @@ -1826,19 +1910,29 @@ document.addEventListener('DOMContentLoaded', function() { } 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); + const backupSection = detailsContainer.querySelector('.pbs-task-section[data-task-type="backup"]'); + if (backupSection) populatePbsTaskTable(backupSection, pbsInstance.backupTasks?.recentTasks); + + const verifySection = detailsContainer.querySelector('.pbs-task-section[data-task-type="verify"]'); + if (verifySection) populatePbsTaskTable(verifySection, pbsInstance.verificationTasks?.recentTasks); + + const syncSection = detailsContainer.querySelector('.pbs-task-section[data-task-type="sync"]'); + if (syncSection) populatePbsTaskTable(syncSection, pbsInstance.syncTasks?.recentTasks); + + const pruneGcSection = detailsContainer.querySelector('.pbs-task-section[data-task-type="prunegc"]'); + if (pruneGcSection) populatePbsTaskTable(pruneGcSection, pbsInstance.pruneTasks?.recentTasks); - // ---> REMOVE: Sort listener setup calls <--- - } 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}`; - } + } 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}`; + } } // End Upsert Logic @@ -1855,4 +1949,39 @@ document.addEventListener('DOMContentLoaded', function() { } // --- End Update PBS Info Function --- + // --- PBS Task Table Toggle Listener --- + const pbsInstancesContainer = document.getElementById('pbs-instances-container'); + if (pbsInstancesContainer) { + pbsInstancesContainer.addEventListener('click', (event) => { + const button = event.target.closest('.pbs-toggle-button'); + if (!button) return; // Click wasn't on a toggle button + + const targetTbodyId = button.dataset.targetTbodyId; + const tbody = document.getElementById(targetTbodyId); + if (!tbody) { + console.error("Target tbody not found for toggle button:", targetTbodyId); + return; + } + + // Find the parent section containing this tbody + const parentSection = tbody.closest('.pbs-task-section'); + if (!parentSection) { + console.error("Parent task section (.pbs-task-section) not found for tbody:", targetTbodyId); + return; + } + + const isCurrentlyExpanded = tbody.dataset.isExpanded === 'true'; + const fullTasks = JSON.parse(tbody.dataset.fullTasks || '[]'); + + // Toggle the expanded state *before* re-populating + tbody.dataset.isExpanded = isCurrentlyExpanded ? 'false' : 'true'; + + // Re-populate the table using the parent section and full task list + populatePbsTaskTable(parentSection, fullTasks); + }); + } else { + console.warn("PBS instances container not found, toggle functionality will not work."); + } + // --- End PBS Task Table Toggle Listener --- + }); // End DOMContentLoaded \ No newline at end of file