Skip to content

Commit 90d6245

Browse files
rcourtmanclaude
andcommitted
feat: enhance backup management with interactive calendar and improved failure tracking
- Add comprehensive PBS task failure detection and processing - Implement interactive backup calendar heatmap with detail cards - Enhance backup filtering with guest type and health status options - Improve keyboard navigation and search functionality across tabs - Add backup detail card component for granular backup information - Update test coverage for enhanced backup data fetching - Clean up debug logging and improve console output clarity 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
1 parent da491b9 commit 90d6245

17 files changed

+2853
-536
lines changed

data/acknowledgements.json

Lines changed: 482 additions & 0 deletions
Large diffs are not rendered by default.

server/dataFetcher.js

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,55 @@ async function fetchAllPbsTasksForProcessing({ client, config }, nodeName) {
549549
}
550550
});
551551

552+
// Add individual guest failure tasks from real backup tasks that didn't match synthetic runs
553+
// These represent failed backup attempts where no snapshot was created
554+
realBackupTasks.forEach(task => {
555+
if (!usedUPIDs.has(task.upid) && task.status !== 'OK') {
556+
// Extract guest info from worker_id (format: "datastore:backup-type/backup-id")
557+
if (task.worker_id) {
558+
const parts = task.worker_id.split(':');
559+
if (parts.length >= 2) {
560+
const guestPart = parts[1];
561+
const guestMatch = guestPart.match(/^([^/]+)\/(.+)$/);
562+
if (guestMatch) {
563+
const guestType = guestMatch[1];
564+
const guestId = guestMatch[2];
565+
const datastoreName = parts[0] || 'unknown';
566+
567+
// Create a failed backup task entry
568+
const failedBackupRun = {
569+
type: 'backup',
570+
status: task.status,
571+
starttime: task.starttime,
572+
endtime: task.endtime,
573+
node: nodeName,
574+
guest: `${guestType}/${guestId}`,
575+
guestType: guestType,
576+
guestId: guestId,
577+
upid: task.upid,
578+
comment: task.comment || '',
579+
size: 0, // No snapshot created
580+
owner: task.user || 'unknown',
581+
datastore: datastoreName,
582+
// PBS-specific fields
583+
pbsBackupRun: true,
584+
backupDate: new Date(task.starttime * 1000).toISOString().split('T')[0],
585+
snapshotCount: 0, // Failed, so no snapshots
586+
protected: false,
587+
// Failure details
588+
failureTask: true,
589+
exitcode: task.exitcode,
590+
user: task.user
591+
};
592+
593+
enhancedBackupRuns.push(failedBackupRun);
594+
usedUPIDs.add(task.upid);
595+
}
596+
}
597+
}
598+
}
599+
});
600+
552601
// Add enhanced synthetic backup runs and non-backup admin tasks
553602
allBackupTasks.push(...enhancedBackupRuns);
554603
allBackupTasks.push(...nonBackupAdminTasks);
@@ -570,6 +619,23 @@ async function fetchAllPbsTasksForProcessing({ client, config }, nodeName) {
570619

571620
const deduplicatedTasks = Array.from(finalTasksMap.values());
572621

622+
// Debug logging for PBS task processing
623+
const failedTasks = deduplicatedTasks.filter(task => task.status !== 'OK');
624+
if (failedTasks.length > 0) {
625+
console.log(`[PBS Tasks Debug] Found ${failedTasks.length} failed tasks for ${config.name}:`,
626+
failedTasks.map(task => ({
627+
guestId: task.guestId,
628+
guestType: task.guestType,
629+
status: task.status,
630+
starttime: task.starttime,
631+
upid: task.upid,
632+
failureTask: task.failureTask || false
633+
}))
634+
);
635+
} else {
636+
console.log(`[PBS Tasks Debug] No failed tasks found for ${config.name}. Total tasks: ${deduplicatedTasks.length}`);
637+
}
638+
573639
// console.log(`[DataFetcher] Final task count for ${config.name}: ${allBackupTasks.length} -> ${deduplicatedTasks.length} (removed ${allBackupTasks.length - deduplicatedTasks.length} duplicates)`); // Removed verbose log
574640

575641
return {

server/pbsUtils.js

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function processPbsTasks(allTasks) {
5353
upid: task.upid,
5454
node: task.node,
5555
type: task.worker_type || task.type,
56-
id: task.worker_id || task.id,
56+
id: task.worker_id || task.id || task.guest, // Include guest as fallback for ID
5757
status: task.status,
5858
startTime: task.starttime,
5959
endTime: task.endtime,
@@ -63,7 +63,10 @@ function processPbsTasks(allTasks) {
6363
exitStatus: task.exitstatus,
6464
saved: task.saved || false,
6565
guest: task.guest || task.worker_id,
66-
pbsBackupRun: task.pbsBackupRun
66+
pbsBackupRun: task.pbsBackupRun,
67+
// Add guest identification fields
68+
guestId: task.guestId,
69+
guestType: task.guestType
6770
});
6871

6972
const sortTasksDesc = (a, b) => (b.startTime || 0) - (a.startTime || 0);
@@ -84,6 +87,7 @@ function processPbsTasks(allTasks) {
8487
const recentVerifyTasks = getRecentTasksList(taskResults.verify.list, createDetailedTask, sortTasksDesc);
8588
const recentSyncTasks = getRecentTasksList(taskResults.sync.list, createDetailedTask, sortTasksDesc);
8689
const recentPruneGcTasks = getRecentTasksList(taskResults.pruneGc.list, createDetailedTask, sortTasksDesc);
90+
8791

8892
// Helper function to create the summary object
8993
const createSummary = (category) => ({

server/tests/dataFetcher.test.js

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,7 @@ describe('Data Fetcher', () => {
109109
expect(result.vms).toEqual([]);
110110
expect(result.containers).toEqual([]);
111111
expect(result.pbs).toEqual([]); // PBS fetch should still run
112-
expect(consoleLogSpy).toHaveBeenCalledWith("[DataFetcher] Discovery cycle completed. Found: 0 PVE nodes, 0 VMs, 0 CTs, 0 PBS instances.");
112+
expect(consoleLogSpy).toHaveBeenCalledWith("[DataFetcher] Discovery cycle completed. Found: 0 PVE nodes, 0 VMs, 0 CTs, 0 PBS instances, 0 PVE backup tasks, 0 guest snapshots.");
113113
expect(mockPbsFunction).toHaveBeenCalled(); // Ensure PBS was still called
114114

115115
consoleLogSpy.mockRestore();
@@ -132,20 +132,27 @@ describe('Data Fetcher', () => {
132132
.mockResolvedValueOnce({ data: { data: { cpu: 0.1, mem: 2 * 1024**3, rootfs: { total: 100*1024**3, used: 20*1024**3 }, uptime: 12345 } } }) // 4. /nodes/mock-node/status
133133
.mockResolvedValueOnce({ data: { data: [ { storage: 'local-lvm', type: 'lvmthin', content: 'images,rootdir', total: 500*1024**3, used: 150*1024**3 } ] } }) // 5. /nodes/mock-node/storage
134134
.mockResolvedValueOnce({ data: { data: [ { vmid: vmId, name: 'test-vm', status: 'running', cpu: 0.5, mem: 1 * 1024**3, maxmem: 2 * 1024**3, maxdisk: 32*1024**3 } ] } }) // 6. /nodes/mock-node/qemu
135-
.mockResolvedValueOnce({ data: { data: [ { vmid: ctId, name: 'test-ct', status: 'running', cpu: 0.2, mem: 512 * 1024**2, maxmem: 1 * 1024**3, maxdisk: 8*1024**3 } ] } }); // 7. /nodes/mock-node/lxc
135+
.mockResolvedValueOnce({ data: { data: [ { vmid: ctId, name: 'test-ct', status: 'running', cpu: 0.2, mem: 512 * 1024**2, maxmem: 1 * 1024**3, maxdisk: 8*1024**3 } ] } }) // 7. /nodes/mock-node/lxc
136+
// Additional calls for backup data
137+
.mockResolvedValueOnce({ data: { data: [] } }) // 8. /nodes/mock-node/tasks (backup tasks)
138+
.mockResolvedValueOnce({ data: { data: [] } }) // 9. /nodes/mock-node/qemu/100/snapshot (VM snapshots)
139+
.mockResolvedValueOnce({ data: { data: [] } }); // 10. /nodes/mock-node/lxc/101/snapshot (CT snapshots)
136140

137141
// Act: Call function with the clients provided by the (mocked) default setup
138142
const result = await fetchDiscoveryData(mockPveApiClient, mockPbsApiClient);
139143

140144
// Assert
141-
expect(mockPveClientInstance.get).toHaveBeenCalledTimes(7); // Updated to 7
145+
expect(mockPveClientInstance.get).toHaveBeenCalledTimes(10); // Updated to 10 for backup calls
142146
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(1, '/cluster/status');
143147
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(2, '/nodes'); // For standaloneNodeName
144148
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(3, '/nodes'); // Main nodes call
145149
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(4, `/nodes/${nodeName}/status`);
146150
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(5, `/nodes/${nodeName}/storage`);
147151
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(6, `/nodes/${nodeName}/qemu`);
148152
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(7, `/nodes/${nodeName}/lxc`);
153+
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(8, `/nodes/${nodeName}/tasks`, { params: { typefilter: 'vzdump', limit: 1000 } });
154+
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(9, `/nodes/${nodeName}/qemu/${vmId}/snapshot`);
155+
expect(mockPveClientInstance.get).toHaveBeenNthCalledWith(10, `/nodes/${nodeName}/lxc/${ctId}/snapshot`);
149156

150157
// Assert Nodes
151158
expect(result.nodes).toHaveLength(1);
@@ -189,6 +196,12 @@ describe('Data Fetcher', () => {
189196
id: `${endpointId}-${nodeName}-${ctId}` // Check constructed ID
190197
});
191198

199+
// Assert PVE Backups
200+
expect(result.pveBackups).toBeDefined();
201+
expect(result.pveBackups.backupTasks).toEqual([]);
202+
expect(result.pveBackups.storageBackups).toEqual([]);
203+
expect(result.pveBackups.guestSnapshots).toEqual([]);
204+
192205
// Assert PBS (should be empty)
193206
expect(result.pbs).toBeDefined();
194207
expect(result.pbs).toHaveLength(0);
@@ -289,7 +302,8 @@ describe('Data Fetcher', () => {
289302
.mockResolvedValueOnce({ data: { data: { cpu: 0.1, uptime: 10 } } }) // 4. /nodes/node-pve2/status
290303
.mockResolvedValueOnce({ data: { data: [] } }) // 5. /nodes/node-pve2/storage
291304
.mockResolvedValueOnce({ data: { data: [] } }) // 6. /nodes/node-pve2/qemu
292-
.mockResolvedValueOnce({ data: { data: [] } }); // 7. /nodes/node-pve2/lxc <<< SHOULD BE EMPTY
305+
.mockResolvedValueOnce({ data: { data: [] } }) // 7. /nodes/node-pve2/lxc <<< SHOULD BE EMPTY
306+
.mockResolvedValueOnce({ data: { data: [] } }); // 8. /nodes/node-pve2/tasks (backup tasks)
293307

294308
const result = await fetchDiscoveryData(mockClients, {}); // Pass custom clients
295309

@@ -300,7 +314,7 @@ describe('Data Fetcher', () => {
300314
// );
301315

302316
// Assert mockPveClientInstance2 (successful endpoint)
303-
expect(mockPveClientInstance2.get).toHaveBeenCalledTimes(7);
317+
expect(mockPveClientInstance2.get).toHaveBeenCalledTimes(8);
304318

305319
// Should still return data from the successful endpoint (pve2)
306320
expect(result.nodes).toHaveLength(1);
@@ -700,7 +714,8 @@ describe('Data Fetcher', () => {
700714
.mockRejectedValueOnce(statusFetchError) // 4. /nodes/${nodeName}/status << THIS FAILS
701715
.mockResolvedValueOnce({ data: { data: [] } }) // 5. /nodes/${nodeName}/storage (subsequent calls should still be mocked)
702716
.mockResolvedValueOnce({ data: { data: [] } }) // 6. /nodes/${nodeName}/qemu
703-
.mockResolvedValueOnce({ data: { data: [] } }); // 7. /nodes/${nodeName}/lxc
717+
.mockResolvedValueOnce({ data: { data: [] } }) // 7. /nodes/${nodeName}/lxc
718+
.mockResolvedValueOnce({ data: { data: [] } }); // 8. /nodes/${nodeName}/tasks (backup tasks)
704719

705720
const result = await fetchDiscoveryData(mockPveApiClient, mockPbsApiClient);
706721

@@ -801,7 +816,12 @@ describe('Data Fetcher', () => {
801816
nodes: [],
802817
vms: [],
803818
containers: [],
804-
pbs: []
819+
pbs: [],
820+
pveBackups: {
821+
backupTasks: [],
822+
guestSnapshots: [],
823+
storageBackups: []
824+
}
805825
});
806826

807827
consoleErrorSpy.mockRestore();

0 commit comments

Comments
 (0)