From 6b6de575bc07d4cc70b1525fab4fb0ae49d375ba Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Tue, 29 Apr 2025 16:46:12 +0100 Subject: [PATCH] feat(ui): Improve PBS tab layout and health status --- README.md | 19 +++++++++-- src/public/app.js | 87 ++++++++++++++++++++++++++++++++++++++--------- 2 files changed, 87 insertions(+), 19 deletions(-) diff --git a/README.md b/README.md index 75a251696..1f3f60a4e 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ A lightweight monitoring application for Proxmox VE that displays real-time stat - [License](#-license) - [Trademark Notice](#trademark-notice) - [Support](#-support) +- [Troubleshooting](#-troubleshooting) ## 🛠️ Configuration @@ -80,7 +81,7 @@ A lightweight monitoring application for Proxmox VE that displays real-time stat Pulse needs to query task lists specific to the PBS node (e.g., `/api2/json/nodes/{nodeName}/tasks`). It attempts to discover this node name automatically by querying the `/api2/json/nodes` endpoint first. However, **this endpoint is typically restricted for API tokens** (returning a 403 Forbidden error), even for tokens with high privileges, unless the `Sys.Audit` permission is explicitly granted on the root path (`/`). - Therefore, **setting `PBS_NODE_NAME` in your `.env` file is the standard and recommended way** to ensure Pulse can correctly query the task endpoints for your PBS instance when using API token authentication. If it's not set and automatic discovery fails due to permissions, Pulse will be unable to fetch task data. + Therefore, **setting `PBS_NODE_NAME` in your `.env` file is the standard and recommended way** to ensure Pulse can correctly query the task endpoints for your PBS instance when using API token authentication. If it's not set and automatic discovery fails due to permissions, Pulse will be unable to fetch task data. **If you are experiencing issues where PBS tasks (backups, verifications, etc.) are not appearing in Pulse, verifying that `PBS_NODE_NAME` is set correctly in your `.env` file (matching the output of `hostname` on the PBS server) should be your first troubleshooting step.** **How to find your PBS Node Name:** 1. **SSH:** Log into your PBS server via SSH and run the command `hostname`. The output is the value needed for `PBS_NODE_NAME`. @@ -405,4 +406,18 @@ If you encounter any issues or have questions, please file an issue on the [GitH If you find this project useful, consider supporting its development: -[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/rcourtman) \ No newline at end of file +[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/rcourtman) + +## ❓ Troubleshooting + +If you encounter issues connecting to your Proxmox VE or Proxmox Backup Server instances, check the following: + +* **Pulse Application Logs:** Examine the logs from the Pulse container (`docker logs pulse_monitor`) or the systemd service (`sudo journalctl -u pulse-monitor.service -f`) for specific error messages (e.g., connection refused, 401 Unauthorized, 403 Forbidden, timeout). +* **`.env` Configuration:** Double-check all relevant environment variables in your `.env` file for accuracy: + * `PROXMOX_HOST`, `PROXMOX_TOKEN_ID`, `PROXMOX_TOKEN_SECRET` + * `PROXMOX_ALLOW_SELF_SIGNED_CERTS` (especially if using self-signed certificates) + * `PBS_HOST`, `PBS_TOKEN_ID`, `PBS_TOKEN_SECRET` + * `PBS_ALLOW_SELF_SIGNED_CERTS` + * **`PBS_NODE_NAME`**: As mentioned above, ensure this exactly matches the output of `hostname` run on your PBS server. This is the most common cause of missing PBS task data. +* **Network Connectivity:** Ensure the machine or container running Pulse can reach the specified Proxmox VE and PBS host addresses and ports (usually `8006` for PVE, `8007` for PBS). Firewalls or network configuration might block access. +* **API Token Permissions:** Verify that the API tokens used have the recommended roles assigned correctly in the Proxmox VE / PBS web interface (`PVEAuditor` for PVE, `Audit` for PBS, both assigned at path `/` with propagation enabled). \ No newline at end of file diff --git a/src/public/app.js b/src/public/app.js index 2550b00a2..fadc3190e 100644 --- a/src/public/app.js +++ b/src/public/app.js @@ -113,7 +113,7 @@ document.addEventListener('DOMContentLoaded', function() { activeContent.classList.remove('hidden'); // If switching to dashboard, re-apply filter (in case data updated while on another tab) if (tabId === 'main') { - applyDashboardFilters(); + updateDashboardTable(); // Renamed: Use the function that handles dashboard rendering/filtering } // ---> ADDED: Trigger Backups Tab update if switching to it <--- if (tabId === 'backups') { @@ -1946,7 +1946,6 @@ document.addEventListener('DOMContentLoaded', function() { // Helper functions defined here or accessible in scope const createSummaryCard = (type, title, summaryData) => { const card = document.createElement('div'); - card.className = 'border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-100/50 dark:bg-gray-700/50'; const summary = summaryData?.summary || {}; const ok = summary.ok ?? '-'; const failed = summary.failed ?? '-'; @@ -1954,6 +1953,11 @@ document.addEventListener('DOMContentLoaded', function() { const lastOk = formatPbsTimestamp(summary.lastOk); const lastFailed = formatPbsTimestamp(summary.lastFailed); const failedStyle = (failed > 0) ? 'font-bold text-red-600 dark:text-red-400' : 'text-red-600 dark:text-red-400 font-semibold'; + + // Add highlighting class if there are failures + const highlightClass = (failed > 0) ? 'border-l-4 border-red-500 dark:border-red-400' : 'border-l-4 border-transparent'; // Use transparent border normally + card.className = `border border-gray-200 dark:border-gray-700 rounded p-3 bg-gray-100/50 dark:bg-gray-700/50 ${highlightClass}`; + card.innerHTML = `

${title} (7d)

@@ -2008,7 +2012,7 @@ document.addEventListener('DOMContentLoaded', function() { currentInstanceIds.add(instanceElementId); let instanceWrapper = document.getElementById(instanceElementId); - let detailsContainer, dsTableBody, statusElement; + let detailsContainer, dsTableBody, instanceTitleElement; // Determine Status Text and Detail Visibility let statusText = 'Loading...'; @@ -2037,15 +2041,66 @@ document.addEventListener('DOMContentLoaded', function() { break; } + // --- START: Calculate Overall Health --- + let overallHealth = 'ok'; // Assume ok initially + let healthTitle = 'OK'; + if (pbsInstance.status === 'error') { + overallHealth = 'error'; + healthTitle = `Error: ${pbsInstance.errorMessage || 'Connection failed'}`; + } else if (pbsInstance.status !== 'ok') { + overallHealth = 'warning'; // Configured or unknown status + healthTitle = 'Connecting or unknown status'; + } else { + // Check datastores + const highUsageDatastore = (pbsInstance.datastores || []).find(ds => { + const totalBytes = ds.total || 0; + const usedBytes = ds.used || 0; + const usagePercent = totalBytes > 0 ? Math.round((usedBytes / totalBytes) * 100) : 0; + return usagePercent > 85; // Warning threshold + }); + if (highUsageDatastore) { + overallHealth = 'warning'; + healthTitle = `Warning: Datastore ${highUsageDatastore.name} usage high (${Math.round((highUsageDatastore.used / highUsageDatastore.total) * 100)}%)`; + } + + // Check task failures (only if status is still ok or warning) + if (overallHealth !== 'error') { + const hasFailures = [ + pbsInstance.backupTasks, + pbsInstance.verificationTasks, + pbsInstance.syncTasks, + pbsInstance.pruneTasks + ].some(taskGroup => (taskGroup?.summary?.failed ?? 0) > 0); + + if (hasFailures) { + overallHealth = 'error'; // Treat any failure in the last 7 days as an error state for the badge + healthTitle = 'Error: One or more recent tasks failed'; + } + } + } + // --- END: Calculate Overall Health --- + + // Helper to generate health badge HTML + const createHealthBadgeHTML = (health, title) => { + let colorClass = 'bg-gray-400 dark:bg-gray-500'; // Default/unknown + if (health === 'ok') colorClass = 'bg-green-500'; + else if (health === 'warning') colorClass = 'bg-yellow-500'; + else if (health === 'error') colorClass = 'bg-red-500'; + // Log the health status being used for the badge + console.log(`[PBS Health Badge - ${instanceName}] Health: ${health}, Title: ${title}, Class: ${colorClass}`); + return ``; + }; + if (instanceWrapper) { // Instance Exists: Update - statusElement = instanceWrapper.querySelector(`#pbs-status-${instanceId}`); + console.log(`[PBS Update - ${instanceName}] Found existing wrapper:`, instanceWrapper); detailsContainer = instanceWrapper.querySelector(`#pbs-details-${instanceId}`); + instanceTitleElement = instanceWrapper.querySelector('h3'); // Find the existing h3 + console.log(`[PBS Update - ${instanceName}] Found title element:`, instanceTitleElement); - // Update status text and color - if (statusElement) { - statusElement.textContent = statusText; - statusElement.className = `text-sm ${statusColorClass}`; + // Update health badge and instance name in title + if (instanceTitleElement) { + instanceTitleElement.innerHTML = `${createHealthBadgeHTML(overallHealth, healthTitle)}${instanceName}`; } // Update contents IF the details container exists @@ -2124,17 +2179,14 @@ document.addEventListener('DOMContentLoaded', function() { instanceWrapper.id = instanceElementId; // Header + console.log(`[PBS Create - ${instanceName}] Creating new wrapper.`); 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); + instanceTitleElement = document.createElement('h3'); // Assign to instanceTitleElement + instanceTitleElement.className = 'text-lg font-semibold text-gray-800 dark:text-gray-200 flex items-center'; // Corrected: Use instanceTitleElement + // Add health badge and instance name + instanceTitleElement.innerHTML = `${createHealthBadgeHTML(overallHealth, healthTitle)}${instanceName}`; + headerDiv.appendChild(instanceTitleElement); instanceWrapper.appendChild(headerDiv); // Details Container @@ -2169,6 +2221,7 @@ document.addEventListener('DOMContentLoaded', function() { 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'; + console.log(`[PBS Create - ${instanceName}] Adding summary cards. Backup failures: ${pbsInstance.backupTasks?.summary?.failed ?? 'N/A'}`); summariesSection.appendChild(createSummaryCard('backup', 'Backups', pbsInstance.backupTasks)); summariesSection.appendChild(createSummaryCard('verify', 'Verification', pbsInstance.verificationTasks)); summariesSection.appendChild(createSummaryCard('sync', 'Sync', pbsInstance.syncTasks));