mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-22 03:04:03 +00:00
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:
@@ -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
@@ -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
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user