mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-21 18:53:37 +00:00
cbd6964ac8
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>
178 lines
5.7 KiB
JavaScript
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
|
|
}; |