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) {