From ebaf4cd18c69fd55ab1b08a9ea3e5806da71e2a9 Mon Sep 17 00:00:00 2001 From: Jordan Ramos Date: Thu, 14 May 2026 12:09:44 -0600 Subject: [PATCH] =?UTF-8?q?Fix=20double-counting=20in=20VCL=20multi-vertic?= =?UTF-8?q?al=20stats=20=E2=80=94=20use=20only=20ALL:=20rollup=20rows?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Summary sheet in each vertical spreadsheet contains both sub-team rows (ACCESS-OPS, STEAM, INTELDEV, etc.) AND a rollup row (ALL: NTS-AEO) per metric. The rollup row already includes all sub-team totals, so summing all rows was double-counting every device. Fixed in three places: - GET /stats endpoint: added AND team LIKE 'ALL:%' filter - persistMultiVerticalUpload snapshot creation: only sum ALL: entries - GET /vertical/:code/metrics category aggregation: only use ALL: rows Also ran a one-time data fix to correct existing compliance_snapshots. --- backend/routes/vclMultiVertical.js | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/backend/routes/vclMultiVertical.js b/backend/routes/vclMultiVertical.js index e7338b9..d52db95 100644 --- a/backend/routes/vclMultiVertical.js +++ b/backend/routes/vclMultiVertical.js @@ -141,15 +141,20 @@ async function persistMultiVerticalUpload({ items, summary, reportDate, filename } // 6. Create/update compliance_snapshots for this vertical - // Use summary data for accurate totals (compliance_items only has non-compliant devices) + // Use summary data for accurate totals (compliance_items only has non-compliant devices). + // IMPORTANT: Only use "ALL:" rollup rows to avoid double-counting. Each Summary sheet + // has sub-team rows AND a rollup row per metric — the rollup already includes sub-teams. const currentMonth = new Date().toISOString().slice(0, 7); let totalDevices = 0, snapshotCompliant = 0, snapshotNonCompliant = 0; if (summary && summary.entries && summary.entries.length > 0) { for (const entry of summary.entries) { - totalDevices += entry.total || 0; - snapshotCompliant += entry.compliant || 0; - snapshotNonCompliant += entry.non_compliant || 0; + // Only count rollup rows (team starts with "ALL:") to avoid double-counting + if (entry.team && entry.team.startsWith('ALL:')) { + totalDevices += entry.total || 0; + snapshotCompliant += entry.compliant || 0; + snapshotNonCompliant += entry.non_compliant || 0; + } } } @@ -460,16 +465,18 @@ function createVCLMultiVerticalRouter(upload) { const latestUploadIds = latestUploads.map(u => u.id); - // Aggregate summary data from the latest upload per vertical - // Each row in vcl_multi_vertical_summary is one metric for one vertical. - // We sum across metrics per vertical to get vertical-level totals. + // Aggregate summary data from the latest upload per vertical. + // IMPORTANT: Only use "ALL:" rollup rows to avoid double-counting. + // Each spreadsheet's Summary sheet contains both sub-team rows (ACCESS-OPS, + // STEAM, etc.) AND a rollup row (ALL: NTS-AEO) per metric. The rollup row + // already includes all sub-team totals, so summing all rows would double-count. const { rows: verticalSummary } = await pool.query(` SELECT vertical, SUM(total)::int AS total_devices, SUM(compliant)::int AS compliant, SUM(non_compliant)::int AS non_compliant FROM vcl_multi_vertical_summary - WHERE upload_id = ANY($1) + WHERE upload_id = ANY($1) AND team LIKE 'ALL:%' GROUP BY vertical ORDER BY vertical `, [latestUploadIds]); @@ -702,9 +709,10 @@ function createVCLMultiVerticalRouter(upload) { [uploadId, vertical] ); - // Aggregate by category + // Aggregate by category — only use "ALL:" rollup rows to avoid double-counting const categoryMap = {}; for (const m of metrics) { + if (!m.team || !m.team.startsWith('ALL:')) continue; const cat = m.category || 'Other'; if (!categoryMap[cat]) categoryMap[cat] = { category: cat, non_compliant: 0, compliant: 0, total: 0 }; categoryMap[cat].non_compliant += m.non_compliant;