Restructure CCP Metrics to metric-first hierarchy, fix Jira cross-project sync

CCP Metrics View Restructure:
- Add GET /metrics endpoint (aggregated across verticals)
- Add GET /metric/:id/verticals endpoint (per-vertical breakdown)
- Replace VerticalTable with MetricTable on overview (one row per metric)
- Add MetricDetailView for metric-first drill-down
- Restructure navigation: Metric → Vertical → Subteam → Devices
- Remove By Vertical table from AggregatedBurndownChart

Jira Sync Fix:
- Remove hardcoded project filter from getIssue() and searchIssuesByKeys()
- Issue keys are globally unique; project filter broke cross-project tickets
- Fixes 502 Bad Gateway when syncing tickets from non-STEAM projects
This commit is contained in:
Jordan Ramos
2026-05-20 13:30:22 -06:00
parent 64d5e0cb40
commit 56bd5ca148
4 changed files with 469 additions and 51 deletions

View File

@@ -1240,6 +1240,129 @@ function createVCLMultiVerticalRouter(upload) {
}
});
// -----------------------------------------------------------------------
// GET /metric/:id/verticals — Per-vertical breakdown for a specific metric
// -----------------------------------------------------------------------
/**
* GET /metric/:id/verticals
* Returns per-vertical breakdown for a specific metric, including sub-team data
* within each vertical. Uses only the latest upload per vertical.
*
* @method GET
* @route /metric/:id/verticals
* @param {string} id — metric identifier (e.g., "VM-001")
*
* @response 200
* {
* metric_id: string,
* metric_desc: string|null,
* category: string|null,
* verticals: Array<{
* vertical: string,
* non_compliant: number,
* compliant: number,
* total: number,
* compliance_pct: number,
* target: number,
* sub_teams: Array<{
* team: string,
* non_compliant: number,
* compliant: number,
* total: number,
* compliance_pct: number
* }>
* }>
* }
* @response 400 { error: string } — metric ID exceeds 50 characters
* @response 500 { error: string }
*/
router.get('/metric/:id/verticals', async (req, res) => {
const metricId = req.params.id;
if (!metricId || metricId.length > 50) {
return res.status(400).json({ error: 'Invalid metric ID' });
}
try {
// Get latest upload per vertical
const { rows: latestUploads } = await pool.query(`
SELECT DISTINCT ON (vertical) id, vertical
FROM compliance_uploads
WHERE vertical IS NOT NULL
ORDER BY vertical, id DESC
`);
if (latestUploads.length === 0) {
return res.json({ metric_id: metricId, metric_desc: null, category: null, verticals: [] });
}
const latestUploadIds = latestUploads.map(u => u.id);
// Get all rows for this metric from latest uploads
const { rows: allRows } = await pool.query(`
SELECT vertical, metric_desc, category, team,
non_compliant, compliant, total, compliance_pct, target
FROM vcl_multi_vertical_summary
WHERE upload_id = ANY($1) AND metric_id = $2
ORDER BY vertical, team
`, [latestUploadIds, metricId]);
// If no rows found, return empty verticals with metric_id echoed back
if (allRows.length === 0) {
return res.json({ metric_id: metricId, metric_desc: null, category: null, verticals: [] });
}
// Extract metric_desc and category from the first row
const metric_desc = allRows[0].metric_desc || null;
const category = allRows[0].category || null;
// Separate rollup rows (ALL:) from sub-team rows and build per-vertical entries
const verticalMap = {};
for (const row of allRows) {
const isRollup = row.team && row.team.startsWith('ALL:');
if (isRollup) {
// Primary vertical entry from rollup row
verticalMap[row.vertical] = {
vertical: row.vertical,
non_compliant: row.non_compliant,
compliant: row.compliant,
total: row.total,
compliance_pct: row.compliance_pct,
target: row.target,
sub_teams: [],
};
}
}
// Second pass: attach sub-team rows to their parent vertical
for (const row of allRows) {
const isRollup = row.team && row.team.startsWith('ALL:');
const isOther = row.team === '(Other)';
if (isRollup || isOther) continue;
if (verticalMap[row.vertical]) {
verticalMap[row.vertical].sub_teams.push({
team: row.team,
non_compliant: row.non_compliant,
compliant: row.compliant,
total: row.total,
compliance_pct: row.compliance_pct,
});
}
}
// Sort verticals by non_compliant DESC
const verticals = Object.values(verticalMap).sort((a, b) => b.non_compliant - a.non_compliant);
res.json({ metric_id: metricId, metric_desc, category, verticals });
} catch (err) {
console.error('[VCL Multi] GET /metric/:id/verticals error:', err.message);
res.status(500).json({ error: 'Database error' });
}
});
// -----------------------------------------------------------------------
// GET /burndown — Aggregated cross-vertical burndown forecast
// -----------------------------------------------------------------------
@@ -1288,6 +1411,78 @@ function createVCLMultiVerticalRouter(upload) {
}
});
// -----------------------------------------------------------------------
// GET /metrics — Metrics aggregated across all verticals
// -----------------------------------------------------------------------
/**
* GET /metrics
* Returns all metrics aggregated across verticals using only ALL: rollup rows
* from the latest upload per vertical.
*
* @method GET
* @route /metrics
*
* @response 200
* {
* metrics: Array<{
* metric_id: string,
* metric_desc: string,
* category: string,
* non_compliant: number,
* compliant: number,
* total: number,
* compliance_pct: number,
* target: number
* }>
* }
* @response 500 { error: "Database error" }
*/
router.get('/metrics', async (req, res) => {
try {
// Get latest upload ID per vertical
const { rows: latestUploads } = await pool.query(`
SELECT DISTINCT ON (vertical) id, vertical
FROM compliance_uploads
WHERE vertical IS NOT NULL
ORDER BY vertical, id DESC
`);
if (latestUploads.length === 0) {
return res.json({ metrics: [] });
}
const latestUploadIds = latestUploads.map(u => u.id);
// Aggregate metrics across verticals (ALL: rows only)
const { rows: metrics } = await pool.query(`
SELECT metric_id,
MAX(metric_desc) AS metric_desc,
MAX(category) AS category,
SUM(non_compliant)::int AS non_compliant,
SUM(compliant)::int AS compliant,
SUM(total)::int AS total,
ROUND(AVG(target::numeric), 4) AS target
FROM vcl_multi_vertical_summary
WHERE upload_id = ANY($1) AND team LIKE 'ALL:%'
GROUP BY metric_id
ORDER BY non_compliant DESC
`, [latestUploadIds]);
// Compute compliance_pct for each metric
const result = metrics.map(m => ({
...m,
target: parseFloat(m.target) || 0,
compliance_pct: m.total > 0 ? m.compliant / m.total : 0,
}));
res.json({ metrics: result });
} catch (err) {
console.error('[VCL Multi] GET /metrics error:', err.message);
res.status(500).json({ error: 'Database error' });
}
});
return router;
}