From ae2b7e04330773de219b15aea8ca48bb1216ebaf Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Wed, 20 May 2026 17:53:29 -0600 Subject: [PATCH] Fix forecast deduplication for multi-vertical metrics Devices appearing in multiple verticals were counted multiple times, causing non_compliant > totalAssets and negative compliance percentages. Deduplicate by hostname before passing to the forecast helper. --- backend/routes/vclMultiVertical.js | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/backend/routes/vclMultiVertical.js b/backend/routes/vclMultiVertical.js index 1b9f035..607a09d 100644 --- a/backend/routes/vclMultiVertical.js +++ b/backend/routes/vclMultiVertical.js @@ -1673,11 +1673,19 @@ function createVCLMultiVerticalRouter(upload) { compliance_pct: currentMonthCompliancePct, }); - // 8. Prepare currentDevices for the helper (only need hostname and resolution_date) - const currentDevices = activeDevices.map(d => ({ - hostname: d.hostname, - resolution_date: d.resolution_date || null, - })); + // 8. Prepare currentDevices for the helper — deduplicate by hostname + // A device may appear in multiple verticals; count it once, keeping the + // earliest resolution_date (or null if any row has no date) + const deviceMap = {}; + for (const d of activeDevices) { + if (!deviceMap[d.hostname]) { + deviceMap[d.hostname] = { hostname: d.hostname, resolution_date: d.resolution_date || null }; + } else if (d.resolution_date && !deviceMap[d.hostname].resolution_date) { + // If any row has a resolution_date and current doesn't, use it + deviceMap[d.hostname].resolution_date = d.resolution_date; + } + } + const currentDevices = Object.values(deviceMap); // 9. Pass data to computeMetricForecastBurndown helper const result = computeMetricForecastBurndown(currentDevices, totalAssets, historicalSnapshots);