Fix compliance stats to use Summary sheet data instead of item counts

The compliance_items table only contains non-compliant devices (detail
sheet rows). Compliant devices are never inserted — they only exist in
the Summary sheet totals. This caused Compliant to show 0 and
Compliance % to show 0% for all verticals.

Fix: stats endpoint now reads from vcl_multi_vertical_summary (parsed
Summary sheet data) for total/compliant/non-compliant counts. Snapshot
creation also uses summary data for accurate trend charting.

The compliance_items table is still used for:
- Donut chart (blocked vs in-progress based on resolution_date)
- Burndown forecast (devices with/without resolution dates)
- Device drill-down (actual non-compliant device list)
This commit is contained in:
Jordan Ramos
2026-05-14 12:01:19 -06:00
parent 408aaa7012
commit 55238ec71e

View File

@@ -141,26 +141,26 @@ async function persistMultiVerticalUpload({ items, summary, reportDate, filename
} }
// 6. Create/update compliance_snapshots for this vertical // 6. Create/update compliance_snapshots for this vertical
// Use summary data for accurate totals (compliance_items only has non-compliant devices)
const currentMonth = new Date().toISOString().slice(0, 7); const currentMonth = new Date().toISOString().slice(0, 7);
const { rows: verticalStats } = await client.query( let totalDevices = 0, snapshotCompliant = 0, snapshotNonCompliant = 0;
`SELECT
COUNT(DISTINCT hostname)::int AS total_devices, if (summary && summary.entries && summary.entries.length > 0) {
COUNT(DISTINCT CASE WHEN status = 'active' THEN hostname END)::int AS non_compliant for (const entry of summary.entries) {
FROM compliance_items totalDevices += entry.total || 0;
WHERE vertical = $1`, snapshotCompliant += entry.compliant || 0;
[vertical] snapshotNonCompliant += entry.non_compliant || 0;
); }
const vs = verticalStats[0] || { total_devices: 0, non_compliant: 0 }; }
const totalDevices = vs.total_devices;
const compliant = totalDevices - vs.non_compliant; const compPct = totalDevices > 0 ? Math.round((snapshotCompliant / totalDevices) * 100 * 100) / 100 : 0;
const compPct = totalDevices > 0 ? Math.round((compliant / totalDevices) * 100 * 100) / 100 : 0;
await client.query( await client.query(
`INSERT INTO compliance_snapshots (snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct) `INSERT INTO compliance_snapshots (snapshot_month, vertical, total_devices, compliant, non_compliant, compliance_pct)
VALUES ($1, $2, $3, $4, $5, $6) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (snapshot_month, vertical) ON CONFLICT (snapshot_month, vertical)
DO UPDATE SET total_devices = $3, compliant = $4, non_compliant = $5, compliance_pct = $6`, DO UPDATE SET total_devices = $3, compliant = $4, non_compliant = $5, compliance_pct = $6`,
[currentMonth, vertical, totalDevices, compliant, vs.non_compliant, compPct] [currentMonth, vertical, totalDevices, snapshotCompliant, snapshotNonCompliant, compPct]
); );
return { uploadId, newCount, recurringCount, resolvedCount }; return { uploadId, newCount, recurringCount, resolvedCount };
@@ -437,22 +437,53 @@ function createVCLMultiVerticalRouter(upload) {
*/ */
router.get('/stats', async (req, res) => { router.get('/stats', async (req, res) => {
try { try {
// Aggregate device-level stats across all multi-vertical items // Use Summary sheet data (vcl_multi_vertical_summary) for accurate totals.
const { rows: statsRows } = await pool.query(` // The compliance_items table only contains NON-COMPLIANT devices, so counting
SELECT // hostnames there gives 0 compliant. The Summary sheet has the real numbers.
COUNT(DISTINCT hostname)::int AS total_devices,
COUNT(DISTINCT CASE WHEN status = 'active' THEN hostname END)::int AS non_compliant // Get the latest upload per vertical to pull summary data from
FROM compliance_items const { rows: latestUploads } = await pool.query(`
SELECT DISTINCT ON (vertical) id, vertical
FROM compliance_uploads
WHERE vertical IS NOT NULL WHERE vertical IS NOT NULL
ORDER BY vertical, id DESC
`); `);
const raw = statsRows[0] || { total_devices: 0, non_compliant: 0 }; if (latestUploads.length === 0) {
const total_devices = raw.total_devices; return res.json({
const non_compliant = raw.non_compliant; stats: { total_devices: 0, compliant: 0, non_compliant: 0, compliance_pct: 0, target_pct: VCL_TARGET_PCT },
const compliant = total_devices - non_compliant; donut: { blocked: { count: 0, pct: 0 }, in_progress: { count: 0, pct: 0 } },
const compliance_pct = total_devices > 0 ? Math.round((compliant / total_devices) * 100) : 0; vertical_breakdown: [],
last_upload_date: null,
});
}
// Donut: blocked vs in-progress across all verticals 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.
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)
GROUP BY vertical
ORDER BY vertical
`, [latestUploadIds]);
// Compute aggregated stats across all verticals
let aggTotal = 0, aggCompliant = 0, aggNonCompliant = 0;
for (const v of verticalSummary) {
aggTotal += v.total_devices;
aggCompliant += v.compliant;
aggNonCompliant += v.non_compliant;
}
const compliance_pct = aggTotal > 0 ? Math.round((aggCompliant / aggTotal) * 100) : 0;
// Donut: blocked vs in-progress (from compliance_items — devices with/without resolution dates)
const { rows: donutRows } = await pool.query(` const { rows: donutRows } = await pool.query(`
SELECT hostname, MAX(resolution_date) AS resolution_date SELECT hostname, MAX(resolution_date) AS resolution_date
FROM compliance_items FROM compliance_items
@@ -462,18 +493,6 @@ function createVCLMultiVerticalRouter(upload) {
const donutItems = donutRows.map(r => ({ resolution_date: r.resolution_date })); const donutItems = donutRows.map(r => ({ resolution_date: r.resolution_date }));
const donut = categorizeNonCompliant(donutItems); const donut = categorizeNonCompliant(donutItems);
// Per-vertical breakdown
const { rows: verticalRows } = await pool.query(`
SELECT
vertical,
COUNT(DISTINCT hostname)::int AS total_devices,
COUNT(DISTINCT CASE WHEN status = 'active' THEN hostname END)::int AS non_compliant
FROM compliance_items
WHERE vertical IS NOT NULL
GROUP BY vertical
ORDER BY vertical
`);
// Get last upload date per vertical // Get last upload date per vertical
const { rows: uploadDates } = await pool.query(` const { rows: uploadDates } = await pool.query(`
SELECT vertical, MAX(report_date) AS last_upload SELECT vertical, MAX(report_date) AS last_upload
@@ -484,30 +503,28 @@ function createVCLMultiVerticalRouter(upload) {
const uploadDateMap = {}; const uploadDateMap = {};
uploadDates.forEach(r => { uploadDateMap[r.vertical] = r.last_upload; }); uploadDates.forEach(r => { uploadDateMap[r.vertical] = r.last_upload; });
// Get burndown data per vertical // Get burndown data per vertical (from compliance_items — actual device records)
const { rows: burndownRows } = await pool.query(` const { rows: burndownRows } = await pool.query(`
SELECT vertical, hostname, resolution_date SELECT vertical, hostname, resolution_date
FROM compliance_items FROM compliance_items
WHERE vertical IS NOT NULL AND status = 'active' WHERE vertical IS NOT NULL AND status = 'active'
`); `);
// Group by vertical for burndown computation
const burndownByVertical = {}; const burndownByVertical = {};
for (const row of burndownRows) { for (const row of burndownRows) {
if (!burndownByVertical[row.vertical]) burndownByVertical[row.vertical] = []; if (!burndownByVertical[row.vertical]) burndownByVertical[row.vertical] = [];
burndownByVertical[row.vertical].push(row); burndownByVertical[row.vertical].push(row);
} }
const vertical_breakdown = verticalRows.map(v => { // Build per-vertical breakdown using summary data for totals
const totalDev = v.total_devices; const vertical_breakdown = verticalSummary.map(v => {
const comp = totalDev - v.non_compliant; const pct = v.total_devices > 0 ? Math.round((v.compliant / v.total_devices) * 100) : 0;
const pct = totalDev > 0 ? Math.round((comp / totalDev) * 100) : 0;
const items = burndownByVertical[v.vertical] || []; const items = burndownByVertical[v.vertical] || [];
const burndown = computeVerticalBurndown(items); const burndown = computeVerticalBurndown(items);
return { return {
vertical: v.vertical, vertical: v.vertical,
total_devices: totalDev, total_devices: v.total_devices,
compliant: comp, compliant: v.compliant,
non_compliant: v.non_compliant, non_compliant: v.non_compliant,
compliance_pct: pct, compliance_pct: pct,
blockers: burndown.blockers, blockers: burndown.blockers,
@@ -518,9 +535,9 @@ function createVCLMultiVerticalRouter(upload) {
res.json({ res.json({
stats: { stats: {
total_devices, total_devices: aggTotal,
compliant, compliant: aggCompliant,
non_compliant, non_compliant: aggNonCompliant,
compliance_pct, compliance_pct,
target_pct: VCL_TARGET_PCT, target_pct: VCL_TARGET_PCT,
}, },