Skip to content

Commit ef7ad49

Browse files
rcourtmanclaude
andcommitted
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>
1 parent c0e9883 commit ef7ad49

File tree

3 files changed

+357
-70
lines changed

3 files changed

+357
-70
lines changed

src/public/js/ui/backup-summary-cards.js

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ PulseApp.ui = PulseApp.ui || {};
22

33
PulseApp.ui.backupSummaryCards = (() => {
44

5-
function calculateBackupStatistics(backupData, guestId) {
5+
function calculateBackupStatistics(backupData, guestId, guestNode, guestEndpointId) {
66
const now = Date.now();
77
const stats = {
88
lastBackup: { time: null, type: null, status: 'none' },
@@ -18,7 +18,31 @@ PulseApp.ui.backupSummaryCards = (() => {
1818
if (!backupData[type]) return;
1919

2020
const items = guestId
21-
? backupData[type].filter(item => item.vmid == guestId)
21+
? backupData[type].filter(item => {
22+
// Match vmid
23+
const itemVmid = item.vmid || item['backup-id'] || item.backupVMID;
24+
if (itemVmid != guestId) return false;
25+
26+
// For PBS backups (centralized), don't filter by node
27+
if (type === 'pbsSnapshots') return true;
28+
29+
// For PVE backups and snapshots (node-specific), match node/endpoint
30+
const itemNode = item.node;
31+
const itemEndpoint = item.endpointId;
32+
33+
// Match by node if available
34+
if (guestNode && itemNode) {
35+
return itemNode === guestNode;
36+
}
37+
38+
// Match by endpointId if available
39+
if (guestEndpointId && itemEndpoint) {
40+
return itemEndpoint === guestEndpointId;
41+
}
42+
43+
// If no node/endpoint info available, include it (fallback)
44+
return true;
45+
})
2246
: backupData[type];
2347

2448
items.forEach(item => {

src/public/js/ui/backups.js

Lines changed: 133 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -425,11 +425,25 @@ PulseApp.ui.backups = (() => {
425425
function _determineGuestBackupStatus(guest, guestSnapshots, guestTasks, dayBoundaries, threeDaysAgo, sevenDaysAgo) {
426426
const guestId = String(guest.vmid);
427427

428-
// Get guest snapshots from pveBackups
428+
// Get guest snapshots from pveBackups - use node-aware filtering
429429
const pveBackups = PulseApp.state.get('pveBackups') || {};
430430
const allSnapshots = pveBackups.guestSnapshots || [];
431431
const guestSnapshotCount = allSnapshots
432-
.filter(snap => parseInt(snap.vmid, 10) === parseInt(guest.vmid, 10))
432+
.filter(snap => {
433+
// Match vmid
434+
if (parseInt(snap.vmid, 10) !== parseInt(guest.vmid, 10)) return false;
435+
436+
// For VM/CT snapshots, match by node/endpoint if available
437+
if (guest.node && snap.node) {
438+
return snap.node === guest.node;
439+
}
440+
if (guest.endpointId && snap.endpointId) {
441+
return snap.endpointId === guest.endpointId;
442+
}
443+
444+
// Fallback: include if no node info available
445+
return true;
446+
})
433447
.length;
434448

435449
// Use pre-filtered data instead of filtering large arrays
@@ -558,10 +572,22 @@ PulseApp.ui.backups = (() => {
558572
// Check for VM/CT snapshots on this day (if we have that data)
559573
const pveBackups = PulseApp.state.get('pveBackups') || {};
560574
const allSnapshots = pveBackups.guestSnapshots || [];
561-
const guestDaySnapshots = allSnapshots.filter(snap =>
562-
parseInt(snap.vmid, 10) === parseInt(guestId, 10) &&
563-
snap.snaptime >= day.start && snap.snaptime < day.end
564-
);
575+
const guestDaySnapshots = allSnapshots.filter(snap => {
576+
// Match vmid and time
577+
if (parseInt(snap.vmid, 10) !== parseInt(guestId, 10)) return false;
578+
if (!(snap.snaptime >= day.start && snap.snaptime < day.end)) return false;
579+
580+
// Match by node/endpoint if available
581+
if (guest.node && snap.node) {
582+
return snap.node === guest.node;
583+
}
584+
if (guest.endpointId && snap.endpointId) {
585+
return snap.endpointId === guest.endpointId;
586+
}
587+
588+
// Fallback: include if no node info available
589+
return true;
590+
});
565591

566592
if (guestDaySnapshots.length > 0) {
567593
backupTypes.add('snapshot');
@@ -729,7 +755,13 @@ PulseApp.ui.backups = (() => {
729755

730756
// Calendar date filter - only show guests that had backups on the selected date
731757
if (calendarDateFilter && calendarDateFilter.guestIds && calendarDateFilter.guestIds.length > 0) {
732-
const guestIdMatch = calendarDateFilter.guestIds.includes(item.guestId.toString());
758+
// Create unique key for this guest item
759+
const nodeIdentifier = item.node || item.endpointId || '';
760+
const itemUniqueKey = nodeIdentifier ? `${item.guestId}-${nodeIdentifier}` : item.guestId.toString();
761+
762+
// Check if this guest's unique key or simple vmid is in the calendar filter
763+
const guestIdMatch = calendarDateFilter.guestIds.includes(itemUniqueKey) ||
764+
calendarDateFilter.guestIds.includes(item.guestId.toString());
733765
if (!guestIdMatch) return false;
734766
}
735767

@@ -1043,7 +1075,17 @@ PulseApp.ui.backups = (() => {
10431075
if (pveBackups?.storageBackups && Array.isArray(pveBackups.storageBackups)) {
10441076
pveBackups.storageBackups.forEach(backup => {
10451077
pveStorageBackups.push({
1046-
...backup,
1078+
'backup-time': backup.ctime,
1079+
backupType: _extractBackupTypeFromVolid(backup.volid, backup.vmid),
1080+
backupVMID: backup.vmid,
1081+
vmid: backup.vmid, // Ensure vmid is preserved for filtering
1082+
size: backup.size,
1083+
protected: backup.protected,
1084+
storage: backup.storage,
1085+
volid: backup.volid,
1086+
format: backup.format,
1087+
node: backup.node,
1088+
endpointId: backup.endpointId,
10471089
source: 'pve'
10481090
});
10491091
});
@@ -1084,8 +1126,12 @@ PulseApp.ui.backups = (() => {
10841126
backupTasks: [...pbsBackupTasks, ...pveBackupTasks]
10851127
};
10861128

1087-
// Create calendar respecting current table filters
1088-
const filteredGuestIds = filteredBackupStatus.map(guest => guest.guestId.toString());
1129+
// Create calendar respecting current table filters - use unique guest identifiers
1130+
const filteredGuestIds = filteredBackupStatus.map(guest => {
1131+
// Create unique identifier including node/endpoint to handle guests with same vmid on different nodes
1132+
const nodeIdentifier = guest.node || guest.endpointId || '';
1133+
return nodeIdentifier ? `${guest.guestId}-${nodeIdentifier}` : guest.guestId.toString();
1134+
});
10891135
// Get detail card for callback
10901136
const detailCardContainer = document.getElementById('backup-detail-card');
10911137
let onDateSelect = null;
@@ -1146,7 +1192,27 @@ PulseApp.ui.backups = (() => {
11461192
const itemDateKey = utcDate.toISOString().split('T')[0];
11471193

11481194
const vmid = item.vmid || item['backup-id'] || item.backupVMID;
1149-
return vmid == guestId && itemDateKey === dateKey;
1195+
if (vmid != guestId || itemDateKey !== dateKey) return false;
1196+
1197+
// For PBS backups (centralized), don't filter by node
1198+
if (source === 'pbsSnapshots') return true;
1199+
1200+
// For PVE backups and snapshots (node-specific), match node/endpoint
1201+
const itemNode = item.node;
1202+
const itemEndpoint = item.endpointId;
1203+
1204+
// Match by node if available
1205+
if (guest.node && itemNode) {
1206+
return itemNode === guest.node;
1207+
}
1208+
1209+
// Match by endpointId if available
1210+
if (guest.endpointId && itemEndpoint) {
1211+
return itemEndpoint === guest.endpointId;
1212+
}
1213+
1214+
// If no node/endpoint info available, include it (fallback)
1215+
return true;
11501216
});
11511217

11521218
if (dayBackups.length > 0) {
@@ -1162,13 +1228,37 @@ PulseApp.ui.backups = (() => {
11621228
let hasFailures = false;
11631229
if (backupData.backupTasks) {
11641230
const dayTasks = backupData.backupTasks.filter(task => {
1165-
if (!task.starttime || task.vmid != guestId) return false;
1231+
if (!task.starttime) return false;
1232+
1233+
// Match vmid
1234+
const taskVmid = task.vmid || task.guestId;
1235+
if (taskVmid != guestId) return false;
11661236

11671237
const date = new Date(task.starttime * 1000);
11681238
const utcDate = new Date(Date.UTC(date.getUTCFullYear(), date.getUTCMonth(), date.getUTCDate()));
11691239
const taskDateKey = utcDate.toISOString().split('T')[0];
11701240

1171-
return taskDateKey === dateKey && task.status !== 'OK';
1241+
if (taskDateKey !== dateKey || task.status === 'OK') return false;
1242+
1243+
// For PBS tasks (centralized), don't filter by node
1244+
if (task.source === 'pbs') return true;
1245+
1246+
// For PVE tasks (node-specific), match node/endpoint
1247+
const taskNode = task.node;
1248+
const taskEndpoint = task.endpointId;
1249+
1250+
// Match by node if available
1251+
if (guest.node && taskNode) {
1252+
return taskNode === guest.node;
1253+
}
1254+
1255+
// Match by endpointId if available
1256+
if (guest.endpointId && taskEndpoint) {
1257+
return taskEndpoint === guest.endpointId;
1258+
}
1259+
1260+
// If no node/endpoint info available, include it (fallback)
1261+
return true;
11721262
});
11731263

11741264
hasFailures = dayTasks.length > 0;
@@ -1431,8 +1521,12 @@ PulseApp.ui.backups = (() => {
14311521

14321522
// Create and display calendar heatmap with detail card
14331523
if (calendarContainer && PulseApp.ui.calendarHeatmap && PulseApp.ui.backupDetailCard) {
1434-
// Get filtered guest IDs for calendar filtering
1435-
const filteredGuestIds = filteredBackupStatus.map(guest => guest.guestId.toString());
1524+
// Get filtered guest IDs for calendar filtering - use unique guest identifiers
1525+
const filteredGuestIds = filteredBackupStatus.map(guest => {
1526+
// Create unique identifier including node/endpoint to handle guests with same vmid on different nodes
1527+
const nodeIdentifier = guest.node || guest.endpointId || '';
1528+
return nodeIdentifier ? `${guest.guestId}-${nodeIdentifier}` : guest.guestId.toString();
1529+
});
14361530

14371531
// Get detail card container
14381532
const detailCardContainer = document.getElementById('backup-detail-card');
@@ -1465,8 +1559,20 @@ PulseApp.ui.backups = (() => {
14651559
if (dateData) {
14661560
// Apply current table filters to the selected date's data
14671561
const filteredDateBackups = dateData.backups.filter(backup => {
1468-
// Find the guest in filteredBackupStatus
1469-
const guestInFiltered = filteredBackupStatus.find(g => g.guestId.toString() === backup.vmid.toString());
1562+
// Find the guest in filteredBackupStatus using unique identification
1563+
const guestInFiltered = filteredBackupStatus.find(g => {
1564+
// First try exact vmid match for simple cases
1565+
if (g.guestId.toString() === backup.vmid.toString()) {
1566+
// If backup has node info, ensure it matches
1567+
if (backup.node || backup.endpointId) {
1568+
return (backup.node && g.node === backup.node) ||
1569+
(backup.endpointId && g.endpointId === backup.endpointId) ||
1570+
(!backup.node && !backup.endpointId);
1571+
}
1572+
return true;
1573+
}
1574+
return false;
1575+
});
14701576
return guestInFiltered !== undefined;
14711577
});
14721578

@@ -1499,8 +1605,16 @@ PulseApp.ui.backups = (() => {
14991605

15001606
// Update calendar date filter for table
15011607
if (dateData && dateData.backups && dateData.backups.length > 0) {
1502-
// Extract guest IDs from the selected date's backup data
1503-
const guestIds = dateData.backups.map(backup => backup.vmid.toString());
1608+
// Extract unique guest identifiers from the selected date's backup data
1609+
const guestIds = dateData.backups.map(backup => {
1610+
// Use unique key if available, fall back to vmid
1611+
if (backup.uniqueKey) {
1612+
return backup.uniqueKey;
1613+
}
1614+
// Create unique key from available data
1615+
const nodeIdentifier = backup.node || backup.endpointId || '';
1616+
return nodeIdentifier ? `${backup.vmid}-${nodeIdentifier}` : backup.vmid.toString();
1617+
});
15041618
PulseApp.state.set('calendarDateFilter', {
15051619
date: dateData.date,
15061620
guestIds: guestIds

0 commit comments

Comments
 (0)