From ef7ad49c3b1ec7fca3a12e09ce09ce17ad24cda8 Mon Sep 17 00:00:00 2001 From: "courtmanr@gmail.com" Date: Sat, 31 May 2025 09:29:47 +0100 Subject: [PATCH] fix: resolve guest identification issues in backups tab MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixed guest lookup logic to properly handle guests with the same vmid on different nodes by implementing unique guest identification throughout the backup data pipeline: - Updated guest filtering to use vmid-node combinations instead of simple vmid matching - Enhanced PBS vs PVE backup system awareness for node-specific vs centralized filtering - Fixed calendar heatmap data structures to prevent guest data mixing - Improved calendar date filtering to use unique guest identifiers - Added node-aware filtering for backup tasks and snapshot detection This resolves issues where the filtered summary and calendar would incorrectly display or mix data from guests sharing the same vmid across different nodes. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- src/public/js/ui/backup-summary-cards.js | 28 ++- src/public/js/ui/backups.js | 152 ++++++++++++-- src/public/js/ui/calendar-heatmap.js | 247 ++++++++++++++++++----- 3 files changed, 357 insertions(+), 70 deletions(-) diff --git a/src/public/js/ui/backup-summary-cards.js b/src/public/js/ui/backup-summary-cards.js index 5a8cc3cca..d8197e029 100644 --- a/src/public/js/ui/backup-summary-cards.js +++ b/src/public/js/ui/backup-summary-cards.js @@ -2,7 +2,7 @@ PulseApp.ui = PulseApp.ui || {}; PulseApp.ui.backupSummaryCards = (() => { - function calculateBackupStatistics(backupData, guestId) { + function calculateBackupStatistics(backupData, guestId, guestNode, guestEndpointId) { const now = Date.now(); const stats = { lastBackup: { time: null, type: null, status: 'none' }, @@ -18,7 +18,31 @@ PulseApp.ui.backupSummaryCards = (() => { if (!backupData[type]) return; const items = guestId - ? backupData[type].filter(item => item.vmid == guestId) + ? backupData[type].filter(item => { + // Match vmid + const itemVmid = item.vmid || item['backup-id'] || item.backupVMID; + if (itemVmid != guestId) return false; + + // For PBS backups (centralized), don't filter by node + if (type === 'pbsSnapshots') return true; + + // For PVE backups and snapshots (node-specific), match node/endpoint + const itemNode = item.node; + const itemEndpoint = item.endpointId; + + // Match by node if available + if (guestNode && itemNode) { + return itemNode === guestNode; + } + + // Match by endpointId if available + if (guestEndpointId && itemEndpoint) { + return itemEndpoint === guestEndpointId; + } + + // If no node/endpoint info available, include it (fallback) + return true; + }) : backupData[type]; items.forEach(item => { diff --git a/src/public/js/ui/backups.js b/src/public/js/ui/backups.js index 037122935..5eed1817a 100644 --- a/src/public/js/ui/backups.js +++ b/src/public/js/ui/backups.js @@ -425,11 +425,25 @@ PulseApp.ui.backups = (() => { function _determineGuestBackupStatus(guest, guestSnapshots, guestTasks, dayBoundaries, threeDaysAgo, sevenDaysAgo) { const guestId = String(guest.vmid); - // Get guest snapshots from pveBackups + // Get guest snapshots from pveBackups - use node-aware filtering const pveBackups = PulseApp.state.get('pveBackups') || {}; const allSnapshots = pveBackups.guestSnapshots || []; const guestSnapshotCount = allSnapshots - .filter(snap => parseInt(snap.vmid, 10) === parseInt(guest.vmid, 10)) + .filter(snap => { + // Match vmid + if (parseInt(snap.vmid, 10) !== parseInt(guest.vmid, 10)) return false; + + // For VM/CT snapshots, match by node/endpoint if available + if (guest.node && snap.node) { + return snap.node === guest.node; + } + if (guest.endpointId && snap.endpointId) { + return snap.endpointId === guest.endpointId; + } + + // Fallback: include if no node info available + return true; + }) .length; // Use pre-filtered data instead of filtering large arrays @@ -558,10 +572,22 @@ PulseApp.ui.backups = (() => { // Check for VM/CT snapshots on this day (if we have that data) const pveBackups = PulseApp.state.get('pveBackups') || {}; const allSnapshots = pveBackups.guestSnapshots || []; - const guestDaySnapshots = allSnapshots.filter(snap => - parseInt(snap.vmid, 10) === parseInt(guestId, 10) && - snap.snaptime >= day.start && snap.snaptime < day.end - ); + const guestDaySnapshots = allSnapshots.filter(snap => { + // Match vmid and time + if (parseInt(snap.vmid, 10) !== parseInt(guestId, 10)) return false; + if (!(snap.snaptime >= day.start && snap.snaptime < day.end)) return false; + + // Match by node/endpoint if available + if (guest.node && snap.node) { + return snap.node === guest.node; + } + if (guest.endpointId && snap.endpointId) { + return snap.endpointId === guest.endpointId; + } + + // Fallback: include if no node info available + return true; + }); if (guestDaySnapshots.length > 0) { backupTypes.add('snapshot'); @@ -729,7 +755,13 @@ PulseApp.ui.backups = (() => { // Calendar date filter - only show guests that had backups on the selected date if (calendarDateFilter && calendarDateFilter.guestIds && calendarDateFilter.guestIds.length > 0) { - const guestIdMatch = calendarDateFilter.guestIds.includes(item.guestId.toString()); + // Create unique key for this guest item + const nodeIdentifier = item.node || item.endpointId || ''; + const itemUniqueKey = nodeIdentifier ? `${item.guestId}-${nodeIdentifier}` : item.guestId.toString(); + + // Check if this guest's unique key or simple vmid is in the calendar filter + const guestIdMatch = calendarDateFilter.guestIds.includes(itemUniqueKey) || + calendarDateFilter.guestIds.includes(item.guestId.toString()); if (!guestIdMatch) return false; } @@ -1043,7 +1075,17 @@ PulseApp.ui.backups = (() => { if (pveBackups?.storageBackups && Array.isArray(pveBackups.storageBackups)) { pveBackups.storageBackups.forEach(backup => { pveStorageBackups.push({ - ...backup, + 'backup-time': backup.ctime, + backupType: _extractBackupTypeFromVolid(backup.volid, backup.vmid), + backupVMID: backup.vmid, + vmid: backup.vmid, // Ensure vmid is preserved for filtering + size: backup.size, + protected: backup.protected, + storage: backup.storage, + volid: backup.volid, + format: backup.format, + node: backup.node, + endpointId: backup.endpointId, source: 'pve' }); }); @@ -1084,8 +1126,12 @@ PulseApp.ui.backups = (() => { backupTasks: [...pbsBackupTasks, ...pveBackupTasks] }; - // Create calendar respecting current table filters - const filteredGuestIds = filteredBackupStatus.map(guest => guest.guestId.toString()); + // Create calendar respecting current table filters - use unique guest identifiers + const filteredGuestIds = filteredBackupStatus.map(guest => { + // Create unique identifier including node/endpoint to handle guests with same vmid on different nodes + const nodeIdentifier = guest.node || guest.endpointId || ''; + return nodeIdentifier ? `${guest.guestId}-${nodeIdentifier}` : guest.guestId.toString(); + }); // Get detail card for callback const detailCardContainer = document.getElementById('backup-detail-card'); let onDateSelect = null; @@ -1146,7 +1192,27 @@ PulseApp.ui.backups = (() => { const itemDateKey = utcDate.toISOString().split('T')[0]; const vmid = item.vmid || item['backup-id'] || item.backupVMID; - return vmid == guestId && itemDateKey === dateKey; + if (vmid != guestId || itemDateKey !== dateKey) return false; + + // For PBS backups (centralized), don't filter by node + if (source === 'pbsSnapshots') return true; + + // For PVE backups and snapshots (node-specific), match node/endpoint + const itemNode = item.node; + const itemEndpoint = item.endpointId; + + // Match by node if available + if (guest.node && itemNode) { + return itemNode === guest.node; + } + + // Match by endpointId if available + if (guest.endpointId && itemEndpoint) { + return itemEndpoint === guest.endpointId; + } + + // If no node/endpoint info available, include it (fallback) + return true; }); if (dayBackups.length > 0) { @@ -1162,13 +1228,37 @@ PulseApp.ui.backups = (() => { let hasFailures = false; if (backupData.backupTasks) { const dayTasks = backupData.backupTasks.filter(task => { - if (!task.starttime || task.vmid != guestId) return false; + if (!task.starttime) return false; + + // Match vmid + const taskVmid = task.vmid || task.guestId; + if (taskVmid != guestId) return false; const date = new Date(task.starttime * 1000); const utcDate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate())); const taskDateKey = utcDate.toISOString().split('T')[0]; - return taskDateKey === dateKey && task.status !== 'OK'; + if (taskDateKey !== dateKey || task.status === 'OK') return false; + + // For PBS tasks (centralized), don't filter by node + if (task.source === 'pbs') return true; + + // For PVE tasks (node-specific), match node/endpoint + const taskNode = task.node; + const taskEndpoint = task.endpointId; + + // Match by node if available + if (guest.node && taskNode) { + return taskNode === guest.node; + } + + // Match by endpointId if available + if (guest.endpointId && taskEndpoint) { + return taskEndpoint === guest.endpointId; + } + + // If no node/endpoint info available, include it (fallback) + return true; }); hasFailures = dayTasks.length > 0; @@ -1431,8 +1521,12 @@ PulseApp.ui.backups = (() => { // Create and display calendar heatmap with detail card if (calendarContainer && PulseApp.ui.calendarHeatmap && PulseApp.ui.backupDetailCard) { - // Get filtered guest IDs for calendar filtering - const filteredGuestIds = filteredBackupStatus.map(guest => guest.guestId.toString()); + // Get filtered guest IDs for calendar filtering - use unique guest identifiers + const filteredGuestIds = filteredBackupStatus.map(guest => { + // Create unique identifier including node/endpoint to handle guests with same vmid on different nodes + const nodeIdentifier = guest.node || guest.endpointId || ''; + return nodeIdentifier ? `${guest.guestId}-${nodeIdentifier}` : guest.guestId.toString(); + }); // Get detail card container const detailCardContainer = document.getElementById('backup-detail-card'); @@ -1465,8 +1559,20 @@ PulseApp.ui.backups = (() => { if (dateData) { // Apply current table filters to the selected date's data const filteredDateBackups = dateData.backups.filter(backup => { - // Find the guest in filteredBackupStatus - const guestInFiltered = filteredBackupStatus.find(g => g.guestId.toString() === backup.vmid.toString()); + // Find the guest in filteredBackupStatus using unique identification + const guestInFiltered = filteredBackupStatus.find(g => { + // First try exact vmid match for simple cases + if (g.guestId.toString() === backup.vmid.toString()) { + // If backup has node info, ensure it matches + if (backup.node || backup.endpointId) { + return (backup.node && g.node === backup.node) || + (backup.endpointId && g.endpointId === backup.endpointId) || + (!backup.node && !backup.endpointId); + } + return true; + } + return false; + }); return guestInFiltered !== undefined; }); @@ -1499,8 +1605,16 @@ PulseApp.ui.backups = (() => { // Update calendar date filter for table if (dateData && dateData.backups && dateData.backups.length > 0) { - // Extract guest IDs from the selected date's backup data - const guestIds = dateData.backups.map(backup => backup.vmid.toString()); + // Extract unique guest identifiers from the selected date's backup data + const guestIds = dateData.backups.map(backup => { + // Use unique key if available, fall back to vmid + if (backup.uniqueKey) { + return backup.uniqueKey; + } + // Create unique key from available data + const nodeIdentifier = backup.node || backup.endpointId || ''; + return nodeIdentifier ? `${backup.vmid}-${nodeIdentifier}` : backup.vmid.toString(); + }); PulseApp.state.set('calendarDateFilter', { date: dateData.date, guestIds: guestIds diff --git a/src/public/js/ui/calendar-heatmap.js b/src/public/js/ui/calendar-heatmap.js index 9768ee7d2..6c35923b7 100644 --- a/src/public/js/ui/calendar-heatmap.js +++ b/src/public/js/ui/calendar-heatmap.js @@ -15,6 +15,85 @@ PulseApp.ui.calendarHeatmap = (() => { return PulseApp.state.get('backupsFilterBackupType') || 'all'; } + // Helper function to filter tasks by guest with node awareness + function filterTasksByGuest(tasks, guestId) { + if (!guestId) return tasks; + + return tasks.filter(task => { + // Match vmid + const taskVmid = task.vmid || task.guestId; + if (taskVmid != guestId) return false; + + // For single guest filtering, we need to get the guest node info + const vmsData = PulseApp.state.get('vmsData') || []; + const containersData = PulseApp.state.get('containersData') || []; + const allGuests = [...vmsData, ...containersData]; + const guest = allGuests.find(g => g.vmid == guestId); + + if (!guest) return true; // Fallback if guest not found + + // For PBS tasks (centralized), don't filter by node + if (task.source === 'pbs') return true; + + // For PVE tasks (node-specific), match node/endpoint + const taskNode = task.node; + const taskEndpoint = task.endpointId; + + // Match by node if available + if (guest.node && taskNode) { + return taskNode === guest.node; + } + + // Match by endpointId if available + if (guest.endpointId && taskEndpoint) { + return taskEndpoint === guest.endpointId; + } + + // If no node/endpoint info available, include it (fallback) + return true; + }); + } + + // Helper function to generate unique guest key including node information + function generateUniqueGuestKey(vmid, backupItem) { + const itemNode = backupItem.node; + const itemEndpoint = backupItem.endpointId; + + // Create unique key with node/endpoint information + if (itemNode) { + return `${vmid}-${itemNode}`; + } + if (itemEndpoint) { + return `${vmid}-${itemEndpoint}`; + } + + // Fallback to simple vmid if no node info + return vmid.toString(); + } + + // Helper function to extract vmid from unique guest key + function extractVmidFromGuestKey(guestKey) { + // If it contains a dash, take the part before the first dash + const dashIndex = guestKey.indexOf('-'); + return dashIndex !== -1 ? guestKey.substring(0, dashIndex) : guestKey; + } + + // Helper function to check if a guest (with potential node info) is in the filtered list + function isGuestInFilteredList(vmid, backupItem, filteredGuestIds) { + if (!filteredGuestIds || filteredGuestIds.length === 0) return true; + + // Generate the unique key for this guest + const uniqueKey = generateUniqueGuestKey(vmid, backupItem); + + // Check if this unique key is in the filtered list + if (filteredGuestIds.includes(uniqueKey)) return true; + + // Also check for simple vmid match (backward compatibility) + if (filteredGuestIds.includes(vmid.toString())) return true; + + return false; + } + const CSS_CLASSES = { CALENDAR_CONTAINER: 'calendar-heatmap-container max-w-4xl mx-auto', MONTH_LABEL: 'text-xs text-gray-600 dark:text-gray-400 font-medium mb-1', @@ -400,10 +479,23 @@ PulseApp.ui.calendarHeatmap = (() => { const allGuests = [...vmsData, ...containersData]; const guestLookup = {}; allGuests.forEach(guest => { - guestLookup[guest.vmid] = { + // Create unique key for guest lookup + const nodeIdentifier = guest.node || guest.endpointId || ''; + const uniqueKey = nodeIdentifier ? `${guest.vmid}-${nodeIdentifier}` : guest.vmid.toString(); + guestLookup[uniqueKey] = { name: guest.name, - type: guest.type === 'qemu' ? 'VM' : 'CT' + type: guest.type === 'qemu' ? 'VM' : 'CT', + vmid: guest.vmid, + node: guest.node, + endpointId: guest.endpointId }; + // Also add simple vmid lookup as fallback for backward compatibility + if (!guestLookup[guest.vmid]) { + guestLookup[guest.vmid] = { + name: guest.name, + type: guest.type === 'qemu' ? 'VM' : 'CT' + }; + } }); // Group backups by guest and date for this month only @@ -436,22 +528,28 @@ PulseApp.ui.calendarHeatmap = (() => { // Apply filtering logic if (guestId && vmid != guestId) return; - if (filteredGuestIds && !filteredGuestIds.includes(vmid.toString())) return; + if (filteredGuestIds && !isGuestInFilteredList(vmid, item, filteredGuestIds)) return; - if (!backupsByGuestAndDate[vmid]) { - backupsByGuestAndDate[vmid] = {}; + // Use unique guest key that includes node information + const uniqueGuestKey = generateUniqueGuestKey(vmid, item); + + if (!backupsByGuestAndDate[uniqueGuestKey]) { + backupsByGuestAndDate[uniqueGuestKey] = {}; } - if (!backupsByGuestAndDate[vmid][dateKey]) { - backupsByGuestAndDate[vmid][dateKey] = { + if (!backupsByGuestAndDate[uniqueGuestKey][dateKey]) { + backupsByGuestAndDate[uniqueGuestKey][dateKey] = { date: utcDate, types: new Set(), - backups: [] + backups: [], + vmid: vmid, // Store original vmid for lookups + node: item.node, + endpointId: item.endpointId }; } - backupsByGuestAndDate[vmid][dateKey].types.add(source); - backupsByGuestAndDate[vmid][dateKey].backups.push({ + backupsByGuestAndDate[uniqueGuestKey][dateKey].types.add(source); + backupsByGuestAndDate[uniqueGuestKey][dateKey].backups.push({ type: source, time: date.toLocaleTimeString(), // Keep original timestamp for display name: item.volid || item.name || item['backup-id'] || 'Backup' @@ -460,7 +558,7 @@ PulseApp.ui.calendarHeatmap = (() => { }); // Process backup days and group by date - Object.entries(backupsByGuestAndDate).forEach(([vmid, dateData]) => { + Object.entries(backupsByGuestAndDate).forEach(([uniqueGuestKey, dateData]) => { Object.keys(dateData).forEach(dateKey => { if (!monthData[dateKey]) { monthData[dateKey] = { @@ -470,12 +568,18 @@ PulseApp.ui.calendarHeatmap = (() => { }; } - const guestInfo = guestLookup[vmid] || { name: `Unknown-${vmid}`, type: 'VM' }; + // Extract vmid from unique key for guest lookup + const vmid = dateData[dateKey].vmid || extractVmidFromGuestKey(uniqueGuestKey); + // Use unique key for guest lookup, fall back to vmid if not found + const guestInfo = guestLookup[uniqueGuestKey] || guestLookup[vmid] || { name: `Unknown-${vmid}`, type: 'VM' }; monthData[dateKey].guests.push({ vmid: vmid, + uniqueKey: uniqueGuestKey, // Include unique key for proper identification name: guestInfo.name, type: guestInfo.type, + node: dateData[dateKey].node, + endpointId: dateData[dateKey].endpointId, types: Array.from(dateData[dateKey].types), backupCount: dateData[dateKey].backups.length }); @@ -488,9 +592,7 @@ PulseApp.ui.calendarHeatmap = (() => { // Process backup tasks for failure detection if (backupData.backupTasks) { - const tasks = guestId - ? backupData.backupTasks.filter(task => task.vmid == guestId) - : backupData.backupTasks; + const tasks = filterTasksByGuest(backupData.backupTasks, guestId); tasks.forEach(task => { if (!task.starttime || task.starttime <= 0) return; @@ -631,9 +733,11 @@ PulseApp.ui.calendarHeatmap = (() => { // Apply filtering logic if (guestId && vmid != guestId) return; - if (filteredGuestIds && !filteredGuestIds.includes(vmid.toString())) return; + if (filteredGuestIds && !isGuestInFilteredList(vmid, item, filteredGuestIds)) return; - stats.activeGuests.add(vmid); + // Track unique guests using node-aware keys + const uniqueGuestKey = generateUniqueGuestKey(vmid, item); + stats.activeGuests.add(uniqueGuestKey); daysWithData.add(dateKey); stats.totalBackups++; @@ -826,10 +930,23 @@ PulseApp.ui.calendarHeatmap = (() => { const allGuests = [...vmsData, ...containersData]; const guestLookup = {}; allGuests.forEach(guest => { - guestLookup[guest.vmid] = { + // Create unique key for guest lookup + const nodeIdentifier = guest.node || guest.endpointId || ''; + const uniqueKey = nodeIdentifier ? `${guest.vmid}-${nodeIdentifier}` : guest.vmid.toString(); + guestLookup[uniqueKey] = { name: guest.name, - type: guest.type === 'qemu' ? 'VM' : 'CT' + type: guest.type === 'qemu' ? 'VM' : 'CT', + vmid: guest.vmid, + node: guest.node, + endpointId: guest.endpointId }; + // Also add simple vmid lookup as fallback for backward compatibility + if (!guestLookup[guest.vmid]) { + guestLookup[guest.vmid] = { + name: guest.name, + type: guest.type === 'qemu' ? 'VM' : 'CT' + }; + } }); // Group all backups by guest and date (no year restriction) @@ -861,22 +978,28 @@ PulseApp.ui.calendarHeatmap = (() => { if (guestId && vmid != guestId) return; // Skip if filtered guest list is provided and this guest is not in it - if (filteredGuestIds && !filteredGuestIds.includes(vmid.toString())) return; + if (filteredGuestIds && !isGuestInFilteredList(vmid, item, filteredGuestIds)) return; - if (!backupsByGuestAndDate[vmid]) { - backupsByGuestAndDate[vmid] = {}; + // Use unique guest key that includes node information + const uniqueGuestKey = generateUniqueGuestKey(vmid, item); + + if (!backupsByGuestAndDate[uniqueGuestKey]) { + backupsByGuestAndDate[uniqueGuestKey] = {}; } - if (!backupsByGuestAndDate[vmid][dateKey]) { - backupsByGuestAndDate[vmid][dateKey] = { + if (!backupsByGuestAndDate[uniqueGuestKey][dateKey]) { + backupsByGuestAndDate[uniqueGuestKey][dateKey] = { date: utcDate, types: new Set(), - backups: [] + backups: [], + vmid: vmid, // Store original vmid for lookups + node: item.node, + endpointId: item.endpointId }; } - backupsByGuestAndDate[vmid][dateKey].types.add(source); - backupsByGuestAndDate[vmid][dateKey].backups.push({ + backupsByGuestAndDate[uniqueGuestKey][dateKey].types.add(source); + backupsByGuestAndDate[uniqueGuestKey][dateKey].backups.push({ type: source, time: date.toLocaleTimeString(), // Keep original timestamp for display name: item.volid || item.name || item['backup-id'] || 'Backup' @@ -885,7 +1008,7 @@ PulseApp.ui.calendarHeatmap = (() => { }); // Process all backup days and group by date - Object.entries(backupsByGuestAndDate).forEach(([vmid, dateData]) => { + Object.entries(backupsByGuestAndDate).forEach(([uniqueGuestKey, dateData]) => { Object.keys(dateData).forEach(dateKey => { // Initialize day data if not exists if (!allData[dateKey]) { @@ -896,12 +1019,18 @@ PulseApp.ui.calendarHeatmap = (() => { }; } - const guestInfo = guestLookup[vmid] || { name: `Unknown-${vmid}`, type: 'VM' }; + // Extract vmid from unique key for guest lookup + const vmid = dateData[dateKey].vmid || extractVmidFromGuestKey(uniqueGuestKey); + // Use unique key for guest lookup, fall back to vmid if not found + const guestInfo = guestLookup[uniqueGuestKey] || guestLookup[vmid] || { name: `Unknown-${vmid}`, type: 'VM' }; allData[dateKey].guests.push({ vmid: vmid, + uniqueKey: uniqueGuestKey, // Include unique key for proper identification name: guestInfo.name, type: guestInfo.type, + node: dateData[dateKey].node, + endpointId: dateData[dateKey].endpointId, types: Array.from(dateData[dateKey].types), backupCount: dateData[dateKey].backups.length }); @@ -914,9 +1043,7 @@ PulseApp.ui.calendarHeatmap = (() => { // Process backup tasks for failure detection if (backupData.backupTasks) { - const tasks = guestId - ? backupData.backupTasks.filter(task => task.vmid == guestId) - : backupData.backupTasks; + const tasks = filterTasksByGuest(backupData.backupTasks, guestId); tasks.forEach(task => { if (!task.starttime || task.starttime <= 0) return; @@ -982,10 +1109,23 @@ PulseApp.ui.calendarHeatmap = (() => { const allGuests = [...vmsData, ...containersData]; const guestLookup = {}; allGuests.forEach(guest => { - guestLookup[guest.vmid] = { + // Create unique key for guest lookup + const nodeIdentifier = guest.node || guest.endpointId || ''; + const uniqueKey = nodeIdentifier ? `${guest.vmid}-${nodeIdentifier}` : guest.vmid.toString(); + guestLookup[uniqueKey] = { name: guest.name, - type: guest.type === 'qemu' ? 'VM' : 'CT' + type: guest.type === 'qemu' ? 'VM' : 'CT', + vmid: guest.vmid, + node: guest.node, + endpointId: guest.endpointId }; + // Also add simple vmid lookup as fallback for backward compatibility + if (!guestLookup[guest.vmid]) { + guestLookup[guest.vmid] = { + name: guest.name, + type: guest.type === 'qemu' ? 'VM' : 'CT' + }; + } }); // Group all backups by guest and date @@ -1025,23 +1165,28 @@ PulseApp.ui.calendarHeatmap = (() => { if (guestId && vmid != guestId) return; // Skip if filtered guest list is provided and this guest is not in it - if (filteredGuestIds && !filteredGuestIds.includes(vmid.toString())) return; + if (filteredGuestIds && !isGuestInFilteredList(vmid, item, filteredGuestIds)) return; + // Use unique guest key that includes node information + const uniqueGuestKey = generateUniqueGuestKey(vmid, item); - if (!backupsByGuestAndDate[vmid]) { - backupsByGuestAndDate[vmid] = {}; + if (!backupsByGuestAndDate[uniqueGuestKey]) { + backupsByGuestAndDate[uniqueGuestKey] = {}; } - if (!backupsByGuestAndDate[vmid][dateKey]) { - backupsByGuestAndDate[vmid][dateKey] = { + if (!backupsByGuestAndDate[uniqueGuestKey][dateKey]) { + backupsByGuestAndDate[uniqueGuestKey][dateKey] = { date: utcDate, types: new Set(), - backups: [] + backups: [], + vmid: vmid, // Store original vmid for lookups + node: item.node, + endpointId: item.endpointId }; } - backupsByGuestAndDate[vmid][dateKey].types.add(source); - backupsByGuestAndDate[vmid][dateKey].backups.push({ + backupsByGuestAndDate[uniqueGuestKey][dateKey].types.add(source); + backupsByGuestAndDate[uniqueGuestKey][dateKey].backups.push({ type: source, time: date.toLocaleTimeString(), // Keep original timestamp for display name: item.volid || item.name || item['backup-id'] || 'Backup' @@ -1050,7 +1195,7 @@ PulseApp.ui.calendarHeatmap = (() => { }); // Process all backup days and determine retention markers - Object.entries(backupsByGuestAndDate).forEach(([vmid, dateData]) => { + Object.entries(backupsByGuestAndDate).forEach(([uniqueGuestKey, dateData]) => { const sortedDates = Object.keys(dateData).sort().reverse(); // Most recent first let lastDaily = null; @@ -1106,10 +1251,13 @@ PulseApp.ui.calendarHeatmap = (() => { yearData[dateKey].guestsByRetention[retentionLevel] = []; } - const guestInfo = guestLookup[vmid] || { name: `Unknown-${vmid}`, type: 'VM' }; + // Extract vmid from unique key for guest lookup + const vmid = dateData[dateKey].vmid || extractVmidFromGuestKey(uniqueGuestKey); + // Use unique key for guest lookup, fall back to vmid if not found + const guestInfo = guestLookup[uniqueGuestKey] || guestLookup[vmid] || { name: `Unknown-${vmid}`, type: 'VM' }; - // Check if this guest already exists in this retention level - const existingGuestIndex = yearData[dateKey].guestsByRetention[retentionLevel].findIndex(g => g.vmid === vmid); + // Check if this guest already exists in this retention level using unique key + const existingGuestIndex = yearData[dateKey].guestsByRetention[retentionLevel].findIndex(g => g.uniqueKey === uniqueGuestKey); if (existingGuestIndex >= 0) { // Merge backup types if guest already exists @@ -1121,8 +1269,11 @@ PulseApp.ui.calendarHeatmap = (() => { // Add new guest yearData[dateKey].guestsByRetention[retentionLevel].push({ vmid: vmid, + uniqueKey: uniqueGuestKey, // Include unique key for proper identification name: guestInfo.name, type: guestInfo.type, + node: dateData[dateKey].node, + endpointId: dateData[dateKey].endpointId, types: Array.from(dateData[dateKey].types), backupCount: dateData[dateKey].backups.length }); @@ -1136,9 +1287,7 @@ PulseApp.ui.calendarHeatmap = (() => { // Process backup tasks for failure detection if (backupData.backupTasks) { - const tasks = guestId - ? backupData.backupTasks.filter(task => task.vmid == guestId) - : backupData.backupTasks; + const tasks = filterTasksByGuest(backupData.backupTasks, guestId); tasks.forEach(task => { if (!task.starttime || task.starttime <= 0) {