Files
pulse/server/dataFetcherFixes.js
T
courtmanr@gmail.com cbd6964ac8 fix: improve backup health monitoring and add comprehensive validation
Enhanced backup health status calculations with more accurate age-based
categorization and improved UI filtering. Added comprehensive backup data
validation test suite and ground truth testing framework.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-06-02 15:20:59 +01:00

178 lines
5.7 KiB
JavaScript

/**
* Fixes for dataFetcher.js to address ground truth discrepancies
*/
/**
* Deduplicates guests across multiple endpoints
* @param {Array} guests - Array of VM or container objects
* @returns {Array} Deduplicated array
*/
function deduplicateGuests(guests) {
const seen = new Map();
return guests.filter(guest => {
// Create a unique key based on VMID and node
// This prevents counting the same guest multiple times
const key = `${guest.vmid}-${guest.node}`;
if (seen.has(key)) {
console.log(`[DataFetcher] Duplicate guest found: VMID ${guest.vmid} on node ${guest.node}`);
return false;
}
seen.set(key, true);
return true;
});
}
/**
* Fixed version of fetchPveDiscoveryData with deduplication
*/
async function fetchPveDiscoveryDataFixed(currentApiClients) {
const pveEndpointIds = Object.keys(currentApiClients);
let allNodes = [], allVms = [], allContainers = [];
if (pveEndpointIds.length === 0) {
return { nodes: [], vms: [], containers: [] };
}
const pvePromises = pveEndpointIds.map(endpointId => {
if (!currentApiClients[endpointId]) {
console.error(`[DataFetcher] No client found for endpoint: ${endpointId}`);
return Promise.resolve({ nodes: [], vms: [], containers: [] });
}
const { client: apiClientInstance, config } = currentApiClients[endpointId];
return fetchDataForPveEndpoint(endpointId, apiClientInstance, config);
});
const pveResults = await Promise.all(pvePromises);
// Aggregate results from all endpoints
pveResults.forEach(result => {
if (result) {
allNodes.push(...(result.nodes || []));
allVms.push(...(result.vms || []));
allContainers.push(...(result.containers || []));
}
});
// Deduplicate guests before returning
const deduplicatedVms = deduplicateGuests(allVms);
const deduplicatedContainers = deduplicateGuests(allContainers);
console.log(`[DataFetcher] Guest deduplication: VMs ${allVms.length} -> ${deduplicatedVms.length}, Containers ${allContainers.length} -> ${deduplicatedContainers.length}`);
return {
nodes: allNodes,
vms: deduplicatedVms,
containers: deduplicatedContainers
};
}
/**
* Improved PBS backup counting that correctly identifies backup runs
*/
function improvedBackupCounting(pbsData) {
const backupsByGuest = new Map();
const backupsByDate = new Map();
if (!pbsData || !pbsData[0]?.datastores) {
return { totalBackups: 0, uniqueBackupRuns: 0, backupsByGuest };
}
pbsData[0].datastores.forEach(ds => {
(ds.snapshots || []).forEach(snap => {
const guestKey = `${snap['backup-type']}/${snap['backup-id']}`;
const dateKey = new Date(snap['backup-time'] * 1000).toISOString().split('T')[0];
const runKey = `${guestKey}:${dateKey}`;
// Count by guest
if (!backupsByGuest.has(guestKey)) {
backupsByGuest.set(guestKey, []);
}
backupsByGuest.get(guestKey).push(snap);
// Count unique backup runs (one per guest per day)
backupsByDate.set(runKey, snap);
});
});
return {
totalBackups: Array.from(backupsByGuest.values()).reduce((sum, backups) => sum + backups.length, 0),
uniqueBackupRuns: backupsByDate.size,
backupsByGuest
};
}
/**
* Validates backup ages and identifies missing backups
*/
function validateBackupAgesImproved(pbsData, expectedGuests) {
const now = Date.now();
const twentyFourHours = 24 * 60 * 60 * 1000;
const results = {
guestsWithRecentBackups: new Set(),
guestsWithoutRecentBackups: new Set(),
backupAgesByGuest: new Map(),
issues: []
};
// Initialize all expected guests as missing
expectedGuests.forEach(guestId => {
results.guestsWithoutRecentBackups.add(guestId);
});
if (pbsData && pbsData[0]?.datastores) {
pbsData[0].datastores.forEach(ds => {
(ds.snapshots || []).forEach(snap => {
const guestId = snap['backup-id'];
const backupTime = snap['backup-time'] * 1000;
const ageMs = now - backupTime;
// Track most recent backup for each guest
if (!results.backupAgesByGuest.has(guestId) ||
backupTime > results.backupAgesByGuest.get(guestId).time) {
results.backupAgesByGuest.set(guestId, {
time: backupTime,
ageHours: ageMs / (1000 * 60 * 60),
ageReadable: formatAge(ageMs)
});
}
// Check if backup is recent (within 24 hours)
if (ageMs < twentyFourHours) {
results.guestsWithRecentBackups.add(guestId);
results.guestsWithoutRecentBackups.delete(guestId);
}
});
});
}
// Identify specific issues
if (results.guestsWithoutRecentBackups.has('102')) {
results.issues.push('VM 102 has no recent backup despite being in backup job');
}
return results;
}
/**
* Format age in human-readable format
*/
function formatAge(ageMs) {
const hours = Math.floor(ageMs / (1000 * 60 * 60));
const days = Math.floor(hours / 24);
if (days > 0) {
return `${days}d ${hours % 24}h`;
}
return `${hours}h`;
}
module.exports = {
deduplicateGuests,
fetchPveDiscoveryDataFixed,
improvedBackupCounting,
validateBackupAgesImproved,
formatAge
};