1
+ /**
2
+ * Fixes for dataFetcher.js to address ground truth discrepancies
3
+ */
4
+
5
+ /**
6
+ * Deduplicates guests across multiple endpoints
7
+ * @param {Array } guests - Array of VM or container objects
8
+ * @returns {Array } Deduplicated array
9
+ */
10
+ function deduplicateGuests ( guests ) {
11
+ const seen = new Map ( ) ;
12
+
13
+ return guests . filter ( guest => {
14
+ // Create a unique key based on VMID and node
15
+ // This prevents counting the same guest multiple times
16
+ const key = `${ guest . vmid } -${ guest . node } ` ;
17
+
18
+ if ( seen . has ( key ) ) {
19
+ console . log ( `[DataFetcher] Duplicate guest found: VMID ${ guest . vmid } on node ${ guest . node } ` ) ;
20
+ return false ;
21
+ }
22
+
23
+ seen . set ( key , true ) ;
24
+ return true ;
25
+ } ) ;
26
+ }
27
+
28
+ /**
29
+ * Fixed version of fetchPveDiscoveryData with deduplication
30
+ */
31
+ async function fetchPveDiscoveryDataFixed ( currentApiClients ) {
32
+ const pveEndpointIds = Object . keys ( currentApiClients ) ;
33
+ let allNodes = [ ] , allVms = [ ] , allContainers = [ ] ;
34
+
35
+ if ( pveEndpointIds . length === 0 ) {
36
+ return { nodes : [ ] , vms : [ ] , containers : [ ] } ;
37
+ }
38
+
39
+ const pvePromises = pveEndpointIds . map ( endpointId => {
40
+ if ( ! currentApiClients [ endpointId ] ) {
41
+ console . error ( `[DataFetcher] No client found for endpoint: ${ endpointId } ` ) ;
42
+ return Promise . resolve ( { nodes : [ ] , vms : [ ] , containers : [ ] } ) ;
43
+ }
44
+ const { client : apiClientInstance , config } = currentApiClients [ endpointId ] ;
45
+ return fetchDataForPveEndpoint ( endpointId , apiClientInstance , config ) ;
46
+ } ) ;
47
+
48
+ const pveResults = await Promise . all ( pvePromises ) ;
49
+
50
+ // Aggregate results from all endpoints
51
+ pveResults . forEach ( result => {
52
+ if ( result ) {
53
+ allNodes . push ( ...( result . nodes || [ ] ) ) ;
54
+ allVms . push ( ...( result . vms || [ ] ) ) ;
55
+ allContainers . push ( ...( result . containers || [ ] ) ) ;
56
+ }
57
+ } ) ;
58
+
59
+ // Deduplicate guests before returning
60
+ const deduplicatedVms = deduplicateGuests ( allVms ) ;
61
+ const deduplicatedContainers = deduplicateGuests ( allContainers ) ;
62
+
63
+ console . log ( `[DataFetcher] Guest deduplication: VMs ${ allVms . length } -> ${ deduplicatedVms . length } , Containers ${ allContainers . length } -> ${ deduplicatedContainers . length } ` ) ;
64
+
65
+ return {
66
+ nodes : allNodes ,
67
+ vms : deduplicatedVms ,
68
+ containers : deduplicatedContainers
69
+ } ;
70
+ }
71
+
72
+ /**
73
+ * Improved PBS backup counting that correctly identifies backup runs
74
+ */
75
+ function improvedBackupCounting ( pbsData ) {
76
+ const backupsByGuest = new Map ( ) ;
77
+ const backupsByDate = new Map ( ) ;
78
+
79
+ if ( ! pbsData || ! pbsData [ 0 ] ?. datastores ) {
80
+ return { totalBackups : 0 , uniqueBackupRuns : 0 , backupsByGuest } ;
81
+ }
82
+
83
+ pbsData [ 0 ] . datastores . forEach ( ds => {
84
+ ( ds . snapshots || [ ] ) . forEach ( snap => {
85
+ const guestKey = `${ snap [ 'backup-type' ] } /${ snap [ 'backup-id' ] } ` ;
86
+ const dateKey = new Date ( snap [ 'backup-time' ] * 1000 ) . toISOString ( ) . split ( 'T' ) [ 0 ] ;
87
+ const runKey = `${ guestKey } :${ dateKey } ` ;
88
+
89
+ // Count by guest
90
+ if ( ! backupsByGuest . has ( guestKey ) ) {
91
+ backupsByGuest . set ( guestKey , [ ] ) ;
92
+ }
93
+ backupsByGuest . get ( guestKey ) . push ( snap ) ;
94
+
95
+ // Count unique backup runs (one per guest per day)
96
+ backupsByDate . set ( runKey , snap ) ;
97
+ } ) ;
98
+ } ) ;
99
+
100
+ return {
101
+ totalBackups : Array . from ( backupsByGuest . values ( ) ) . reduce ( ( sum , backups ) => sum + backups . length , 0 ) ,
102
+ uniqueBackupRuns : backupsByDate . size ,
103
+ backupsByGuest
104
+ } ;
105
+ }
106
+
107
+ /**
108
+ * Validates backup ages and identifies missing backups
109
+ */
110
+ function validateBackupAgesImproved ( pbsData , expectedGuests ) {
111
+ const now = Date . now ( ) ;
112
+ const twentyFourHours = 24 * 60 * 60 * 1000 ;
113
+ const results = {
114
+ guestsWithRecentBackups : new Set ( ) ,
115
+ guestsWithoutRecentBackups : new Set ( ) ,
116
+ backupAgesByGuest : new Map ( ) ,
117
+ issues : [ ]
118
+ } ;
119
+
120
+ // Initialize all expected guests as missing
121
+ expectedGuests . forEach ( guestId => {
122
+ results . guestsWithoutRecentBackups . add ( guestId ) ;
123
+ } ) ;
124
+
125
+ if ( pbsData && pbsData [ 0 ] ?. datastores ) {
126
+ pbsData [ 0 ] . datastores . forEach ( ds => {
127
+ ( ds . snapshots || [ ] ) . forEach ( snap => {
128
+ const guestId = snap [ 'backup-id' ] ;
129
+ const backupTime = snap [ 'backup-time' ] * 1000 ;
130
+ const ageMs = now - backupTime ;
131
+
132
+ // Track most recent backup for each guest
133
+ if ( ! results . backupAgesByGuest . has ( guestId ) ||
134
+ backupTime > results . backupAgesByGuest . get ( guestId ) . time ) {
135
+ results . backupAgesByGuest . set ( guestId , {
136
+ time : backupTime ,
137
+ ageHours : ageMs / ( 1000 * 60 * 60 ) ,
138
+ ageReadable : formatAge ( ageMs )
139
+ } ) ;
140
+ }
141
+
142
+ // Check if backup is recent (within 24 hours)
143
+ if ( ageMs < twentyFourHours ) {
144
+ results . guestsWithRecentBackups . add ( guestId ) ;
145
+ results . guestsWithoutRecentBackups . delete ( guestId ) ;
146
+ }
147
+ } ) ;
148
+ } ) ;
149
+ }
150
+
151
+ // Identify specific issues
152
+ if ( results . guestsWithoutRecentBackups . has ( '102' ) ) {
153
+ results . issues . push ( 'VM 102 has no recent backup despite being in backup job' ) ;
154
+ }
155
+
156
+ return results ;
157
+ }
158
+
159
+ /**
160
+ * Format age in human-readable format
161
+ */
162
+ function formatAge ( ageMs ) {
163
+ const hours = Math . floor ( ageMs / ( 1000 * 60 * 60 ) ) ;
164
+ const days = Math . floor ( hours / 24 ) ;
165
+
166
+ if ( days > 0 ) {
167
+ return `${ days } d ${ hours % 24 } h` ;
168
+ }
169
+ return `${ hours } h` ;
170
+ }
171
+
172
+ module . exports = {
173
+ deduplicateGuests,
174
+ fetchPveDiscoveryDataFixed,
175
+ improvedBackupCounting,
176
+ validateBackupAgesImproved,
177
+ formatAge
178
+ } ;
0 commit comments