diff --git a/src/public/js/ui/dashboard.js b/src/public/js/ui/dashboard.js index 94569dc8f..b4daf7bf6 100644 --- a/src/public/js/ui/dashboard.js +++ b/src/public/js/ui/dashboard.js @@ -30,199 +30,164 @@ PulseApp.ui.dashboard = (() => { statusElementEl = document.getElementById('dashboard-status-text'); } - function refreshDashboardData() { - PulseApp.state.set('dashboardData', []); - let dashboardData = []; + function _calculateAverage(historyArray, key) { + if (!historyArray || historyArray.length === 0) return null; + const validEntries = historyArray.filter(entry => typeof entry[key] === 'number' && !isNaN(entry[key])); + if (validEntries.length === 0) return null; + const sum = validEntries.reduce((acc, curr) => acc + curr[key], 0); + return sum / validEntries.length; + } + function _calculateAverageRate(historyArray, key) { + if (!historyArray || historyArray.length < 2) return null; + const validHistory = historyArray.filter(entry => + typeof entry.timestamp === 'number' && !isNaN(entry.timestamp) && + typeof entry[key] === 'number' && !isNaN(entry[key]) + ); + + if (validHistory.length < 2) return null; + + const oldest = validHistory[0]; + const newest = validHistory[validHistory.length - 1]; + const valueDiff = newest[key] - oldest[key]; + const timeDiffSeconds = (newest.timestamp - oldest.timestamp) / 1000; + + if (timeDiffSeconds <= 0) return null; + if (valueDiff < 0) return null; + + return valueDiff / timeDiffSeconds; + } + + function _processSingleGuestData(guest) { + let avgCpu = 0, avgMem = 0, avgDisk = 0; + let avgDiskReadRate = null, avgDiskWriteRate = null, avgNetInRate = null, avgNetOutRate = null; + let avgMemoryPercent = 'N/A', avgDiskPercent = 'N/A'; + let effectiveMemorySource = 'host'; + let currentMemForAvg = 0; + let currentMemTotalForDisplay = guest.maxmem; + + const metricsData = PulseApp.state.get('metricsData') || []; + const metrics = metricsData.find(m => + m.id === guest.vmid && + m.type === guest.type && + m.node === guest.node && + m.endpointId === guest.endpointId + ); + const guestUniqueId = guest.id; + + const isDragging = PulseApp.ui.thresholds && PulseApp.ui.thresholds.isThresholdDragInProgress && PulseApp.ui.thresholds.isThresholdDragInProgress(); + const snapshot = guestMetricDragSnapshot[guestUniqueId]; + + if (isDragging && snapshot) { + avgDiskReadRate = snapshot.diskread; + avgDiskWriteRate = snapshot.diskwrite; + avgNetInRate = snapshot.netin; + avgNetOutRate = snapshot.netout; + + if (guest.status === STATUS_RUNNING && metrics && metrics.current) { + const currentDataPoint = { timestamp: Date.now(), ...metrics.current }; + PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); + const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; + avgCpu = _calculateAverage(history, 'cpu') ?? 0; + avgMem = _calculateAverage(history, 'mem') ?? 0; + avgDisk = _calculateAverage(history, 'disk') ?? 0; + } else { + PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); + } + } else { + if (guest.status === STATUS_RUNNING && metrics && metrics.current) { + let baseMemoryValue = metrics.current.mem; + currentMemTotalForDisplay = guest.maxmem; + effectiveMemorySource = 'host'; + + if (metrics.current.guest_mem_actual_used_bytes !== undefined && metrics.current.guest_mem_actual_used_bytes !== null) { + baseMemoryValue = metrics.current.guest_mem_actual_used_bytes; + effectiveMemorySource = 'guest'; + if (metrics.current.guest_mem_total_bytes !== undefined && metrics.current.guest_mem_total_bytes > 0) { + currentMemTotalForDisplay = metrics.current.guest_mem_total_bytes; + } + } + currentMemForAvg = baseMemoryValue; + + const currentDataPoint = { + timestamp: Date.now(), + ...metrics.current, + effective_mem: currentMemForAvg, + effective_mem_total: currentMemTotalForDisplay, + effective_mem_source: effectiveMemorySource + }; + PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); + const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; + + avgCpu = _calculateAverage(history, 'cpu') ?? 0; + avgMem = _calculateAverage(history, 'effective_mem') ?? 0; + avgDisk = _calculateAverage(history, 'disk') ?? 0; + avgDiskReadRate = _calculateAverageRate(history, 'diskread'); + avgDiskWriteRate = _calculateAverageRate(history, 'diskwrite'); + avgNetInRate = _calculateAverageRate(history, 'netin'); + avgNetOutRate = _calculateAverageRate(history, 'netout'); + } else { + PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); + } + } + + const historyForGuest = PulseApp.state.getDashboardHistory()[guestUniqueId]; + let finalMemTotalForPercent = guest.maxmem; + let finalMemSourceForTooltip = 'host'; + + if (historyForGuest && historyForGuest.length > 0) { + const lastHistoryEntry = historyForGuest[historyForGuest.length - 1]; + if (lastHistoryEntry.effective_mem_total !== undefined && lastHistoryEntry.effective_mem_total > 0) { + finalMemTotalForPercent = lastHistoryEntry.effective_mem_total; + } + if (lastHistoryEntry.effective_mem_source) { + finalMemSourceForTooltip = lastHistoryEntry.effective_mem_source; + } + } + + avgMemoryPercent = (finalMemTotalForPercent > 0 && typeof avgMem === 'number') ? Math.round(avgMem / finalMemTotalForPercent * 100) : 'N/A'; + avgDiskPercent = (guest.maxdisk > 0 && typeof avgDisk === 'number') ? Math.round(avgDisk / guest.maxdisk * 100) : 'N/A'; + + let rawHostReportedMem = null; + if (guest.status === STATUS_RUNNING && metrics && metrics.current && metrics.current.mem !== undefined) { + rawHostReportedMem = metrics.current.mem; + } + + return { + id: guest.vmid, + uniqueId: guestUniqueId, + vmid: guest.vmid, + name: guest.name || `${guest.type === 'qemu' ? GUEST_TYPE_VM : GUEST_TYPE_CT} ${guest.vmid}`, + node: guest.node, + type: guest.type === 'qemu' ? GUEST_TYPE_VM : GUEST_TYPE_CT, + status: guest.status, + cpu: avgCpu, + cpus: guest.cpus || 1, + memory: avgMemoryPercent, + memoryCurrent: avgMem, + memoryTotal: finalMemTotalForPercent, + memorySource: finalMemSourceForTooltip, + rawHostMemory: rawHostReportedMem, + disk: avgDiskPercent, + diskCurrent: avgDisk, + diskTotal: guest.maxdisk, + uptime: guest.status === STATUS_RUNNING ? guest.uptime : 0, + diskread: avgDiskReadRate, + diskwrite: avgDiskWriteRate, + netin: avgNetInRate, + netout: avgNetOutRate + }; + } + + function _setDashboardColumnWidths(dashboardData) { let maxNameLength = 0; let maxUptimeLength = 0; - function calculateAverage(historyArray, key) { - if (!historyArray || historyArray.length === 0) return null; - const validEntries = historyArray.filter(entry => typeof entry[key] === 'number' && !isNaN(entry[key])); - if (validEntries.length === 0) return null; - const sum = validEntries.reduce((acc, curr) => acc + curr[key], 0); - return sum / validEntries.length; - } - - function calculateAverageRate(historyArray, key) { - if (!historyArray || historyArray.length < 2) return null; - const validHistory = historyArray.filter(entry => - typeof entry.timestamp === 'number' && !isNaN(entry.timestamp) && - typeof entry[key] === 'number' && !isNaN(entry[key]) - ); - - if (validHistory.length < 2) return null; - - const oldest = validHistory[0]; - const newest = validHistory[validHistory.length - 1]; - const valueDiff = newest[key] - oldest[key]; - const timeDiffSeconds = (newest.timestamp - oldest.timestamp) / 1000; - - if (timeDiffSeconds <= 0) { - return null; - } - - if (valueDiff < 0) { - return null; - } - - return valueDiff / timeDiffSeconds; - } - - const processGuest = (guest, type) => { - let avgCpu = 0, avgMem = 0, avgDisk = 0; - let avgDiskReadRate = null, avgDiskWriteRate = null, avgNetInRate = null, avgNetOutRate = null; - let avgMemoryPercent = 'N/A', avgDiskPercent = 'N/A'; - let effectiveMemorySource = 'host'; // 'host' or 'guest' - let currentMemForAvg = 0; - let currentMemTotalForDisplay = guest.maxmem; - - const metricsData = PulseApp.state.get('metricsData') || []; - const metrics = metricsData.find(m => - m.id === guest.vmid && - m.type === guest.type && - m.node === guest.node && - m.endpointId === guest.endpointId - ); - - const guestUniqueId = guest.id; - - // Check for drag state and apply snapshot if active - const isDragging = PulseApp.ui.thresholds && PulseApp.ui.thresholds.isThresholdDragInProgress && PulseApp.ui.thresholds.isThresholdDragInProgress(); - const snapshot = guestMetricDragSnapshot[guestUniqueId]; - - if (isDragging && snapshot) { - avgDiskReadRate = snapshot.diskread; - avgDiskWriteRate = snapshot.diskwrite; - avgNetInRate = snapshot.netin; - avgNetOutRate = snapshot.netout; - - // For other metrics, continue with live data or defaults if snapshot doesn't cover them - // or if we decide to only snapshot I/O rates. - // For now, let other metrics be calculated as usual even during drag. - if (guest.status === 'running' && metrics && metrics.current) { - const currentDataPoint = { - timestamp: Date.now(), - ...metrics.current - }; - PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); - const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; - avgCpu = calculateAverage(history, 'cpu') ?? 0; - avgMem = calculateAverage(history, 'mem') ?? 0; - avgDisk = calculateAverage(history, 'disk') ?? 0; - // I/O rates are already set from snapshot if dragging - } else { - PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); - // If not running, CPU/Mem/Disk also go to their defaults (0 or N/A) - } - - } else { - // Original logic if not dragging or no snapshot - if (guest.status === 'running' && metrics && metrics.current) { - let baseMemoryValue = metrics.current.mem; // Default to host memory - currentMemTotalForDisplay = guest.maxmem; // Default to host allocated max memory - effectiveMemorySource = 'host'; - - // Check if guest agent memory data is available and valid - if (metrics.current.guest_mem_actual_used_bytes !== undefined && metrics.current.guest_mem_actual_used_bytes !== null) { - baseMemoryValue = metrics.current.guest_mem_actual_used_bytes; - effectiveMemorySource = 'guest'; - // If guest provides its own total, use it, otherwise stick to guest.maxmem - if (metrics.current.guest_mem_total_bytes !== undefined && metrics.current.guest_mem_total_bytes > 0) { - currentMemTotalForDisplay = metrics.current.guest_mem_total_bytes; - } - // console.log(`VM ${guest.vmid}: Using GUEST memory: ${baseMemoryValue / (1024*1024)} MB / ${currentMemTotalForDisplay / (1024*1024)} MB`); - } else { - // console.log(`VM ${guest.vmid}: Using HOST memory: ${baseMemoryValue / (1024*1024)} MB / ${currentMemTotalForDisplay / (1024*1024)} MB`); - } - currentMemForAvg = baseMemoryValue; - - const currentDataPoint = { - timestamp: Date.now(), - ...metrics.current, - effective_mem: currentMemForAvg, // Store the memory value actually used for averaging - effective_mem_total: currentMemTotalForDisplay, // Store total used for percentage calc - effective_mem_source: effectiveMemorySource // Store the source for tooltip - }; - PulseApp.state.updateDashboardHistory(guestUniqueId, currentDataPoint); - const history = PulseApp.state.getDashboardHistory()[guestUniqueId] || []; - - avgCpu = calculateAverage(history, 'cpu') ?? 0; - avgMem = calculateAverage(history, 'effective_mem') ?? 0; // Average the memory value we decided to use - avgDisk = calculateAverage(history, 'disk') ?? 0; - avgDiskReadRate = calculateAverageRate(history, 'diskread'); - avgDiskWriteRate = calculateAverageRate(history, 'diskwrite'); - avgNetInRate = calculateAverageRate(history, 'netin'); - avgNetOutRate = calculateAverageRate(history, 'netout'); - } else { - PulseApp.state.clearDashboardHistoryEntry(guestUniqueId); - // Rates remain null (will be N/A), others default - } - } - - // Use the total memory that corresponds to the source of avgMem for percentage calculation - // If history has entries, the last one should have effective_mem_total and effective_mem_source - const historyForGuest = PulseApp.state.getDashboardHistory()[guestUniqueId]; - let finalMemTotalForPercent = guest.maxmem; // Fallback - let finalMemSourceForTooltip = 'host'; // Fallback - - if (historyForGuest && historyForGuest.length > 0) { - const lastHistoryEntry = historyForGuest[historyForGuest.length - 1]; - if (lastHistoryEntry.effective_mem_total !== undefined && lastHistoryEntry.effective_mem_total > 0) { - finalMemTotalForPercent = lastHistoryEntry.effective_mem_total; - } - if (lastHistoryEntry.effective_mem_source) { - finalMemSourceForTooltip = lastHistoryEntry.effective_mem_source; - } - } - - avgMemoryPercent = (finalMemTotalForPercent > 0 && typeof avgMem === 'number') ? Math.round(avgMem / finalMemTotalForPercent * 100) : 'N/A'; - avgDiskPercent = (guest.maxdisk > 0 && typeof avgDisk === 'number') ? Math.round(avgDisk / guest.maxdisk * 100) : 'N/A'; - - const name = guest.name || `${guest.type === 'qemu' ? 'VM' : 'CT'} ${guest.vmid}`; + dashboardData.forEach(guest => { const uptimeFormatted = PulseApp.utils.formatUptime(guest.uptime); - if (name.length > maxNameLength) maxNameLength = name.length; + if (guest.name.length > maxNameLength) maxNameLength = guest.name.length; if (uptimeFormatted.length > maxUptimeLength) maxUptimeLength = uptimeFormatted.length; - - let rawHostReportedMem = null; - if (guest.status === 'running' && metrics && metrics.current && metrics.current.mem !== undefined) { - rawHostReportedMem = metrics.current.mem; - } - - dashboardData.push({ - id: guest.vmid, - uniqueId: guestUniqueId, - vmid: guest.vmid, - name: name, - node: guest.node, - type: guest.type === 'qemu' ? 'VM' : 'CT', - status: guest.status, - cpu: avgCpu, - cpus: guest.cpus || 1, - memory: avgMemoryPercent, - memoryCurrent: avgMem, // This is now potentially guest actual used or host used - memoryTotal: finalMemTotalForPercent, // This is now potentially guest total or host allocated - memorySource: finalMemSourceForTooltip, // Add source for tooltip generation - rawHostMemory: rawHostReportedMem, // Store host raw memory for tooltip comparison - disk: avgDiskPercent, - diskCurrent: avgDisk, - diskTotal: guest.maxdisk, - uptime: guest.status === 'running' ? guest.uptime : 0, - diskread: avgDiskReadRate, - diskwrite: avgDiskWriteRate, - netin: avgNetInRate, - netout: avgNetOutRate - }); - }; - - const vmsData = PulseApp.state.get('vmsData') || []; - const containersData = PulseApp.state.get('containersData') || []; - - vmsData.forEach(vm => processGuest(vm, 'qemu')); - containersData.forEach(ct => processGuest(ct, 'lxc')); - - PulseApp.state.set('dashboardData', dashboardData); + }); const nameColWidth = Math.min(Math.max(maxNameLength * 8 + 16, 100), 300); const uptimeColWidth = Math.max(maxUptimeLength * 7 + 16, 80); @@ -233,22 +198,24 @@ PulseApp.ui.dashboard = (() => { } } - function updateDashboardTable() { - if (!tableBodyEl || !statusElementEl) { - console.error('Dashboard table body or status element not found/initialized!'); - return; - } + function refreshDashboardData() { + PulseApp.state.set('dashboardData', []); + let dashboardData = []; - refreshDashboardData(); + const vmsData = PulseApp.state.get('vmsData') || []; + const containersData = PulseApp.state.get('containersData') || []; - const dashboardData = PulseApp.state.get('dashboardData') || []; - const filterGuestType = PulseApp.state.get('filterGuestType'); - const filterStatus = PulseApp.state.get('filterStatus'); - const thresholdState = PulseApp.state.getThresholdState(); + vmsData.forEach(vm => dashboardData.push(_processSingleGuestData(vm))); + containersData.forEach(ct => dashboardData.push(_processSingleGuestData(ct))); + + PulseApp.state.set('dashboardData', dashboardData); + _setDashboardColumnWidths(dashboardData); + } + function _filterDashboardData(dashboardData, searchInput, filterGuestType, filterStatus, thresholdState) { const textSearchTerms = searchInput ? searchInput.value.toLowerCase().split(',').map(term => term.trim()).filter(term => term) : []; - let filteredData = dashboardData.filter(guest => { + return dashboardData.filter(guest => { const typeMatch = filterGuestType === FILTER_ALL || (filterGuestType === FILTER_VM && guest.type === GUEST_TYPE_VM) || (filterGuestType === FILTER_LXC && guest.type === GUEST_TYPE_CT); @@ -280,80 +247,54 @@ PulseApp.ui.dashboard = (() => { thresholdsMet = false; break; } - if (!(guestValue >= state.value)) { thresholdsMet = false; break; } } } - return typeMatch && statusMatch && searchMatch && thresholdsMet; }); + } - const sortStateMain = PulseApp.state.getSortState('main'); - let sortedData = PulseApp.utils.sortData(filteredData, sortStateMain.column, sortStateMain.direction, 'main'); - - let visibleCount = 0; + function _renderGroupedByNode(tableBody, sortedData, createRowFn) { + const nodeGroups = {}; let visibleNodes = new Set(); - const groupByNode = PulseApp.state.get('groupByNode'); + let visibleCount = 0; - if (groupByNode) { - const nodeGroups = {}; - sortedData.forEach(guest => { - const nodeName = guest.node || 'Unknown Node'; - if (!nodeGroups[nodeName]) nodeGroups[nodeName] = []; - nodeGroups[nodeName].push(guest); + sortedData.forEach(guest => { + const nodeName = guest.node || 'Unknown Node'; + if (!nodeGroups[nodeName]) nodeGroups[nodeName] = []; + nodeGroups[nodeName].push(guest); + }); + + tableBody.innerHTML = ''; + + Object.keys(nodeGroups).sort().forEach(nodeName => { + visibleNodes.add(nodeName.toLowerCase()); + const nodeHeaderRow = document.createElement('tr'); + nodeHeaderRow.className = 'node-header bg-gray-100 dark:bg-gray-700/80 font-semibold text-gray-700 dark:text-gray-300 text-xs'; + nodeHeaderRow.innerHTML = `