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:
@@ -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,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user