fix: resolve guest identification issues in backups tab

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 <noreply@anthropic.com>
This commit is contained in:
courtmanr@gmail.com
2025-05-31 09:29:47 +01:00
parent c0e988303f
commit ef7ad49c3b
3 changed files with 357 additions and 70 deletions
+26 -2
View File
@@ -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 => {
+133 -19
View File
@@ -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
+198 -49
View File
@@ -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) {