Sync .kiro/ from master — v2.2.0 release batch

New specs: archer-template-library, ccp-metrics-view-restructure,
compliance-list-stale-after-sidebar-edit, compliance-metric-estimated-resolution-date,
compliance-remediation-display-fix, flexible-jira-ticket-creation,
forecast-burndown-chart, granite-loader-export, ivanti-queue-clear-completed-fix,
multi-item-jira-ticket, queue-collapsible-sections, vendor-issue-type-dropdown

New steering: archer-template-gen.md

Updated: migration-registration-check hook, remediation-plan-history spec,
gitlab-workflow, tech, versioning steering files
This commit is contained in:
Jordan Ramos
2026-06-04 11:27:31 -06:00
parent 8ebd7e4d5e
commit a61d254ff9
54 changed files with 6992 additions and 59 deletions

View File

@@ -0,0 +1 @@
{"specId": "54d073e2-9532-42f1-9315-f2129a2158af", "workflowType": "fast-task", "specType": "feature"}

View File

@@ -0,0 +1,413 @@
# Design Document
## Overview
This design restructures the CCP Metrics page from a vertical-first drill-down model to a metric-first model. The overview table changes from one row per vertical to one row per metric (aggregated across all verticals). Two new backend endpoints support the metric-centric view. The "By Vertical" table in the AggregatedBurndownChart is removed. Existing vertical-first endpoints are preserved for backward compatibility.
## Architecture
The restructure touches two layers:
1. **Backend** — Two new Express route handlers added to `backend/routes/vclMultiVertical.js`, querying `vcl_multi_vertical_summary` with aggregation across verticals.
2. **Frontend** — Replace `VerticalTable` and `VerticalDetailView` components with `MetricTable` and `MetricDetailView` in `frontend/src/components/pages/CCPMetricsPage.js`. Remove the "By Vertical" table JSX from `AggregatedBurndownChart`. Adjust drill-down state from `(selectedVertical, selectedMetric, selectedTeam)` to `(selectedMetric, selectedVertical, selectedTeam)`.
No database schema changes are required. No new tables or columns are needed — the existing `vcl_multi_vertical_summary` table already contains all data needed for metric-centric aggregation.
## Components and Interfaces
### Backend: New Endpoints
#### GET /api/compliance/vcl-multi/metrics
Aggregates all metrics across verticals using only `ALL:` rollup rows from the latest upload per vertical.
```javascript
// Query: 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
`);
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,
compliance_pct: m.total > 0 ? m.compliant / m.total : 0,
}));
```
**Response shape:**
```json
{
"metrics": [
{
"metric_id": "VM-001",
"metric_desc": "Vulnerability Management - Patching",
"category": "Vulnerability Management",
"non_compliant": 450,
"compliant": 3200,
"total": 3650,
"compliance_pct": 0.8767,
"target": 0.95
}
]
}
```
#### GET /api/compliance/vcl-multi/metric/:id/verticals
Returns per-vertical breakdown for a specific metric, including sub-team data within each vertical.
```javascript
const metricId = req.params.id;
if (!metricId || metricId.length > 50) {
return res.status(400).json({ error: 'Invalid metric ID' });
}
// 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
`);
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]);
// Separate rollup rows (ALL:) from sub-team rows
// Build per-vertical entries with nested sub_teams
```
**Response shape:**
```json
{
"metric_id": "VM-001",
"metric_desc": "Vulnerability Management - Patching",
"category": "Vulnerability Management",
"verticals": [
{
"vertical": "NTS_AEO",
"non_compliant": 200,
"compliant": 1800,
"total": 2000,
"compliance_pct": 0.90,
"target": 0.95,
"sub_teams": [
{
"team": "STEAM",
"non_compliant": 120,
"compliant": 1080,
"total": 1200,
"compliance_pct": 0.90
},
{
"team": "ACCESS-ENG",
"non_compliant": 80,
"compliant": 720,
"total": 800,
"compliance_pct": 0.90
}
]
}
]
}
```
### Frontend: Component Changes
#### AggregatedBurndownChart (Modified)
Remove the "By Vertical" contribution table JSX block. The component continues to receive `data.by_vertical` in its props (for backward compat with the `/burndown` API response) but no longer renders it.
**Before:** Summary header + bar chart + "By Vertical" table
**After:** Summary header + bar chart only
#### MetricTable (New — replaces VerticalTable)
Renders one row per metric from the `/metrics` endpoint response. Columns: Metric ID, Description, Category, Compliant, Non-Compliant, Total, Compliance %, Target %. Rows are clickable — clicking triggers `onSelectMetric(metricId)`.
```javascript
function MetricTable({ metrics, onSelectMetric }) {
if (!metrics || metrics.length === 0) return null;
// Render table with columns: metric_id, metric_desc, category,
// compliant, non_compliant, total, compliance_pct, target
// onClick row → onSelectMetric(metric.metric_id)
}
```
#### MetricDetailView (New — replaces VerticalDetailView)
Fetches data from `GET /metric/:id/verticals` and displays:
- Header with metric ID, description, category
- Aggregated stats cards (total, compliant, non-compliant, compliance %)
- Table of verticals with per-vertical compliance numbers
- Clicking a vertical row navigates to the sub-team view
```javascript
function MetricDetailView({ metricId, onBack, onSelectVertical }) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch(`${API_BASE}/compliance/vcl-multi/metric/${encodeURIComponent(metricId)}/verticals`,
{ credentials: 'include' })
.then(r => r.json())
.then(d => { setData(d); setLoading(false); })
.catch(() => setLoading(false));
}, [metricId]);
// Render: back button, header, stats, verticals table
}
```
#### MetricSubTeamView (Reused with minor adjustments)
The existing `MetricSubTeamView` component is reused. The only change is that it now receives its `metricData` (including `sub_teams`) from the `MetricDetailView`'s vertical entry rather than from the `VerticalDetailView`'s metric entry. The props interface remains the same: `{ vertical, metricId, metricData, onBack, onSelectTeam }`.
#### MetricDeviceList (Reused unchanged)
The existing `MetricDeviceList` component is reused without modification. It already accepts `{ vertical, metricId, team, onBack }` and calls the existing devices endpoint.
### Frontend: Drill-Down State Changes
**Current state model:**
```
selectedVertical → selectedMetric → selectedTeam
```
**New state model:**
```
selectedMetric → selectedVertical → selectedTeam
```
The main component's render logic changes from:
```javascript
// OLD
if (selectedTeam && selectedMetric && selectedVertical) MetricDeviceList
if (selectedMetric && selectedVertical) MetricSubTeamView
if (selectedVertical) VerticalDetailView
else Overview (VerticalTable)
```
To:
```javascript
// NEW
if (selectedTeam !== null && selectedMetric && selectedVertical) MetricDeviceList
if (selectedVertical && selectedMetric) MetricSubTeamView
if (selectedMetric) MetricDetailView
else Overview (MetricTable)
```
**State variables:**
- `selectedMetric` — string (metric_id) or null
- `selectedMetricData` — object with metric context (metric_desc, category) or null
- `selectedVertical` — string (vertical code) or null
- `selectedVerticalData` — object with vertical's sub_teams for the selected metric or null
- `selectedTeam` — string (team name) or null (null with selectedVertical set = "View All Devices")
## Data Flow
```
┌─────────────────────────────────────────────────────────────────┐
│ Overview │
│ GET /stats → StatsBar, DonutChart, TrendChart │
│ GET /burndown → AggregatedBurndownChart (no By Vertical table) │
│ GET /metrics → MetricTable │
└──────────────────────────┬──────────────────────────────────────┘
│ click metric row
┌─────────────────────────────────────────────────────────────────┐
│ MetricDetailView │
│ GET /metric/:id/verticals → header + verticals table │
└──────────────────────────┬──────────────────────────────────────┘
│ click vertical row
┌─────────────────────────────────────────────────────────────────┐
│ MetricSubTeamView │
│ (data passed from parent — no additional fetch) │
└──────────────────────────┬──────────────────────────────────────┘
│ click team row
┌─────────────────────────────────────────────────────────────────┐
│ MetricDeviceList │
│ GET /vertical/:code/metric/:metricId/devices?team=X │
└─────────────────────────────────────────────────────────────────┘
```
## Interfaces
### New API Endpoints
| Method | Path | Auth | Description |
|---|---|---|---|
| GET | `/api/compliance/vcl-multi/metrics` | requireAuth() | Metrics aggregated across all verticals |
| GET | `/api/compliance/vcl-multi/metric/:id/verticals` | requireAuth() | Per-vertical breakdown for a metric |
### Preserved API Endpoints (Backward Compatibility)
| Method | Path | Notes |
|---|---|---|
| GET | `/api/compliance/vcl-multi/stats` | Unchanged — still returns `vertical_breakdown` and `metric_breakdown` |
| GET | `/api/compliance/vcl-multi/vertical/:code/metrics` | Unchanged |
| GET | `/api/compliance/vcl-multi/vertical/:code/metric/:metricId/devices` | Unchanged |
| GET | `/api/compliance/vcl-multi/vertical/:code/burndown` | Unchanged |
| GET | `/api/compliance/vcl-multi/burndown` | Unchanged — still returns `by_vertical` in response |
### Component Props Interfaces
```javascript
// MetricTable
MetricTable.propTypes = {
metrics: Array, // from GET /metrics response
onSelectMetric: Function, // (metricId: string) => void
};
// MetricDetailView
MetricDetailView.propTypes = {
metricId: String,
onBack: Function, // () => void — returns to overview
onSelectVertical: Function, // (vertical: string, verticalData: object) => void
};
// MetricSubTeamView (existing — props unchanged)
MetricSubTeamView.propTypes = {
vertical: String,
metricId: String,
metricData: Object, // { metric_desc, sub_teams, target, ... }
onBack: Function, // () => void — returns to metric-vertical view
onSelectTeam: Function, // (team: string|null) => void
};
// MetricDeviceList (existing — props unchanged)
MetricDeviceList.propTypes = {
vertical: String,
metricId: String,
team: String, // null = all teams
onBack: Function, // () => void — returns to sub-team view
};
```
## Data Models
No new database tables or columns. The feature uses existing data:
### vcl_multi_vertical_summary (existing)
| Column | Type | Notes |
|---|---|---|
| id | SERIAL | PK |
| upload_id | INTEGER | FK → compliance_uploads |
| vertical | TEXT | Vertical code (e.g., "NTS_AEO") |
| metric_id | TEXT | Metric identifier (e.g., "VM-001") |
| metric_desc | TEXT | Human-readable description |
| category | TEXT | Metric category |
| team | TEXT | "ALL: NTS-AEO" for rollup, "STEAM" for sub-team |
| non_compliant | INTEGER | Count |
| compliant | INTEGER | Count |
| total | INTEGER | Count |
| compliance_pct | NUMERIC(5,2) | Stored as decimal (0.95 = 95%) |
| target | NUMERIC(5,2) | Stored as decimal |
**Key convention:** Rows where `team LIKE 'ALL:%'` are rollup rows that include all sub-team totals. Sub-team rows are individual team breakdowns. Only rollup rows should be used for aggregation to avoid double-counting.
## Error Handling
### Backend
| Scenario | Response |
|---|---|
| No auth cookie / expired session | 401 `{ "error": "Authentication required" }` |
| Metric ID > 50 chars | 400 `{ "error": "Invalid metric ID" }` |
| Database query failure | 500 `{ "error": "Database error" }` |
| No data for metric | 200 with empty `verticals` array |
| No summary data at all | 200 with empty `metrics` array |
### Frontend
| Scenario | Behavior |
|---|---|
| Fetch failure on overview | Existing error state renders error message |
| Fetch failure on MetricDetailView | Show error message in component |
| Empty metrics array | Show "No metrics data" empty state |
| Empty verticals array for a metric | Show "No verticals found for this metric" message |
## Correctness Properties
*A property is a characteristic or behavior that should hold true across all valid executions of a system — essentially, a formal statement about what the system should do. Properties serve as the bridge between human-readable specifications and machine-verifiable correctness guarantees.*
### Property 1: Metrics aggregation uses only rollup rows from latest uploads
*For any* set of `vcl_multi_vertical_summary` rows across multiple verticals and uploads, the `GET /metrics` endpoint SHALL return exactly one entry per distinct `metric_id`, with `non_compliant`, `compliant`, and `total` equal to the sum of only `ALL:`-prefixed team rows from the single latest upload per vertical.
**Validates: Requirements 3.1, 3.3**
### Property 2: Metrics computed fields are mathematically correct
*For any* metric entry returned by the `GET /metrics` endpoint, `compliance_pct` SHALL equal `compliant / total` (or 0 when total is 0), and `target` SHALL equal the arithmetic mean of the `target` values across all verticals that have that metric in their latest upload.
**Validates: Requirements 3.4, 3.5**
### Property 3: Metrics response is sorted by non-compliant descending
*For any* response from `GET /metrics` containing two or more metric entries, each entry's `non_compliant` value SHALL be greater than or equal to the next entry's `non_compliant` value.
**Validates: Requirements 3.6**
### Property 4: Metric-verticals breakdown is complete and correct
*For any* metric ID that exists in the database, the `GET /metric/:id/verticals` endpoint SHALL return one vertical entry for each vertical that has that metric in its latest upload, with each entry's `sub_teams` array containing exactly the non-rollup, non-"(Other)" team rows for that metric in that vertical.
**Validates: Requirements 4.1, 4.3**
### Property 5: Metric-verticals response is sorted by non-compliant descending
*For any* response from `GET /metric/:id/verticals` containing two or more vertical entries, each entry's `non_compliant` value SHALL be greater than or equal to the next entry's `non_compliant` value.
**Validates: Requirements 4.4**
### Property 6: Drill-down state determines rendered view
*For any* combination of `(selectedMetric, selectedVertical, selectedTeam)` state values, the CCP_Metrics_Page SHALL render exactly one view: Overview when no metric is selected, MetricDetailView when only metric is selected, MetricSubTeamView when metric and vertical are selected (team is null or absent), and MetricDeviceList when all three are selected.
**Validates: Requirements 6.1, 6.5**
## Testing Strategy
### Property-Based Tests (Backend)
Properties 15 are testable via property-based tests against the backend endpoints. The test setup generates random `vcl_multi_vertical_summary` rows with varying numbers of verticals, metrics, uploads, and team configurations, then asserts the invariants hold.
### Example-Based Tests (Frontend)
Requirements 1.x, 2.x, 5.x, and 6.26.4 are UI rendering and interaction tests best covered by example-based component tests using React Testing Library.
### Integration Tests (Backward Compatibility)
Requirements 7.x are integration tests that verify existing endpoints continue to return the same response shape after the new endpoints are added.

View File

@@ -0,0 +1,112 @@
# Requirements Document
## Introduction
This feature restructures the CCP Metrics page drill-down hierarchy from a vertical-first model (Vertical → Metric → Subteam → Devices) to a metric-first model (Metric → Vertical → Subteam → Devices). The overview table changes from showing one row per vertical to one row per metric (aggregated across all verticals), with each metric appearing once with totals summed across verticals. Clicking a metric drills into which verticals have that metric. Additionally, the "By Vertical" contribution table currently rendered below the AggregatedBurndownChart is removed — the burndown chart and summary stats remain.
Two new backend endpoints support the metric-centric view alongside existing vertical-first endpoints, which are preserved for backward compatibility.
## Glossary
- **CCP_Metrics_Page**: The React page component (`CCPMetricsPage.js`) that displays cross-organizational compliance posture and provides drill-down navigation.
- **Overview_Table**: The primary data table on the CCP_Metrics_Page overview that users interact with to begin drill-down navigation.
- **Metrics_API**: The new backend endpoint `GET /api/compliance/vcl-multi/metrics` that returns all metrics aggregated across verticals.
- **Metric_Verticals_API**: The new backend endpoint `GET /api/compliance/vcl-multi/metric/:id/verticals` that returns per-vertical breakdown for a specific metric.
- **Devices_API**: The existing backend endpoint `GET /api/compliance/vcl-multi/vertical/:code/metric/:metricId/devices` that returns non-compliant devices filtered by vertical, metric, and optionally team.
- **Drill_Down_State**: The set of React state variables that track the user's current navigation depth within the metric-centric hierarchy.
- **AggregatedBurndownChart**: The existing React component that displays the cross-vertical burndown forecast bar chart and summary statistics on the overview page.
- **Metric_Row**: A single row in the Overview_Table representing one metric with compliance totals summed across all verticals.
## Requirements
### Requirement 1: Remove "By Vertical" Table from AggregatedBurndownChart
**User Story:** As a compliance analyst, I want the burndown chart section to show only the forecast chart and summary stats without the per-vertical contribution table, so that the overview page is less cluttered and the vertical breakdown is accessible through the drill-down instead.
#### Acceptance Criteria
1. THE AggregatedBurndownChart component SHALL render the summary header (total non-compliant, blockers, in-progress, projected clear date) and the bar chart.
2. THE AggregatedBurndownChart component SHALL omit the "By Vertical" contribution table that previously appeared below the bar chart.
3. THE AggregatedBurndownChart component SHALL preserve all existing loading, error, empty-state, and all-blockers display behaviors.
### Requirement 2: Metric-Aggregated Overview Table
**User Story:** As a senior leader, I want the overview table to show one row per metric with totals summed across all verticals, so that I can immediately see which metrics have the most non-compliance across the organization.
#### Acceptance Criteria
1. THE Overview_Table SHALL display one Metric_Row per distinct metric, with compliance totals (compliant, non_compliant, total, compliance_pct, target) aggregated across all verticals.
2. THE Overview_Table SHALL display columns for metric ID, description, category, compliant count, non-compliant count, total count, compliance percentage, and target percentage.
3. THE Overview_Table SHALL sort Metric_Rows by non-compliant count in descending order by default.
4. WHEN a user clicks a Metric_Row, THE CCP_Metrics_Page SHALL navigate to the metric-vertical drill-down view for that metric.
5. THE Overview_Table SHALL replace the existing VerticalTable component on the overview page.
### Requirement 3: Metrics Aggregated API Endpoint
**User Story:** As a frontend developer, I want a single API endpoint that returns all metrics aggregated across verticals, so that the overview table can display metric-centric data without client-side aggregation.
#### Acceptance Criteria
1. WHEN a GET request is made to `/api/compliance/vcl-multi/metrics`, THE Metrics_API SHALL return a JSON response containing a `metrics` array where each entry represents one distinct metric aggregated across all verticals.
2. THE Metrics_API SHALL compute each metric entry with fields: `metric_id`, `metric_desc`, `category`, `non_compliant`, `compliant`, `total`, `compliance_pct`, and `target`.
3. THE Metrics_API SHALL aggregate totals by summing `non_compliant`, `compliant`, and `total` across all verticals for each metric, using only rollup rows (team starting with "ALL:") from the latest upload per vertical.
4. THE Metrics_API SHALL compute `compliance_pct` as `compliant / total` for each aggregated metric.
5. THE Metrics_API SHALL compute `target` as the average target across verticals for each metric.
6. THE Metrics_API SHALL sort the returned metrics array by `non_compliant` in descending order.
7. WHEN no summary data exists, THE Metrics_API SHALL return an empty `metrics` array.
8. THE Metrics_API SHALL require authentication via `requireAuth()` middleware.
9. IF a database error occurs, THEN THE Metrics_API SHALL return HTTP 500 with `{ "error": "Database error" }`.
### Requirement 4: Metric-Verticals Drill-Down API Endpoint
**User Story:** As a frontend developer, I want an API endpoint that returns which verticals have a specific metric and their per-vertical compliance numbers, so that the drill-down view can show vertical breakdown for a selected metric.
#### Acceptance Criteria
1. WHEN a GET request is made to `/api/compliance/vcl-multi/metric/:id/verticals`, THE Metric_Verticals_API SHALL return a JSON response containing a `verticals` array with per-vertical breakdown for the specified metric.
2. THE Metric_Verticals_API SHALL return each vertical entry with fields: `vertical`, `non_compliant`, `compliant`, `total`, `compliance_pct`, `target`, and `sub_teams`.
3. THE Metric_Verticals_API SHALL include a `sub_teams` array within each vertical entry containing per-team breakdown with fields: `team`, `non_compliant`, `compliant`, `total`, `compliance_pct`.
4. THE Metric_Verticals_API SHALL sort the `verticals` array by `non_compliant` in descending order.
5. THE Metric_Verticals_API SHALL also return `metric_id`, `metric_desc`, and `category` fields at the top level for display context.
6. WHEN the specified metric ID does not exist in any vertical, THE Metric_Verticals_API SHALL return an empty `verticals` array with the metric_id echoed back.
7. IF the metric ID parameter exceeds 50 characters, THEN THE Metric_Verticals_API SHALL return HTTP 400 with `{ "error": "Invalid metric ID" }`.
8. THE Metric_Verticals_API SHALL require authentication via `requireAuth()` middleware.
9. IF a database error occurs, THEN THE Metric_Verticals_API SHALL return HTTP 500 with `{ "error": "Database error" }`.
### Requirement 5: Metric-Centric Drill-Down Hierarchy
**User Story:** As a compliance analyst, I want to drill down from a metric to see which verticals are affected, then into a vertical's sub-teams, and finally to the device list, so that I can trace non-compliance from the metric level down to individual devices.
#### Acceptance Criteria
1. WHEN a user clicks a Metric_Row in the Overview_Table, THE CCP_Metrics_Page SHALL display a metric-vertical view showing per-vertical breakdown for that metric.
2. THE metric-vertical view SHALL display the metric ID, description, and category as a header, along with aggregated compliance stats for that metric.
3. THE metric-vertical view SHALL display a table of verticals with columns: vertical name, compliant, non-compliant, total, and compliance percentage.
4. WHEN a user clicks a vertical row in the metric-vertical view, THE CCP_Metrics_Page SHALL display a sub-team breakdown view for that vertical and metric combination.
5. THE sub-team breakdown view SHALL display per-team compliance numbers with columns: team name, compliant, non-compliant, total, and compliance percentage.
6. WHEN a user clicks a sub-team row in the sub-team view, THE CCP_Metrics_Page SHALL display the device list filtered by the selected vertical, metric, and team.
7. THE device list view SHALL reuse the existing Devices_API endpoint (`GET /api/compliance/vcl-multi/vertical/:code/metric/:metricId/devices`) with the vertical and metric parameters derived from the drill-down context.
8. WHEN a user clicks a "View All Devices" option in the sub-team view, THE CCP_Metrics_Page SHALL display the device list for all teams within that vertical and metric.
### Requirement 6: Drill-Down Navigation State
**User Story:** As a user navigating the drill-down hierarchy, I want clear back-navigation at each level so that I can return to the previous view without losing context.
#### Acceptance Criteria
1. THE Drill_Down_State SHALL track the selected metric, selected vertical, and selected team to determine which view to render.
2. WHEN the metric-vertical view is displayed, THE CCP_Metrics_Page SHALL provide a "Back to Overview" navigation element that clears the selected metric and returns to the overview.
3. WHEN the sub-team view is displayed, THE CCP_Metrics_Page SHALL provide a "Back to Verticals" navigation element that clears the selected vertical and returns to the metric-vertical view.
4. WHEN the device list view is displayed, THE CCP_Metrics_Page SHALL provide a "Back to Sub-Teams" navigation element that clears the selected team and returns to the sub-team view.
5. THE CCP_Metrics_Page SHALL render the overview (with Overview_Table) when no metric is selected in the Drill_Down_State.
### Requirement 7: Backward Compatibility
**User Story:** As a developer maintaining the system, I want existing vertical-first endpoints to remain functional so that any external consumers or future features relying on them continue to work.
#### Acceptance Criteria
1. THE backend SHALL preserve the existing `GET /api/compliance/vcl-multi/vertical/:code/metrics` endpoint with unchanged behavior and response shape.
2. THE backend SHALL preserve the existing `GET /api/compliance/vcl-multi/vertical/:code/metric/:metricId/devices` endpoint with unchanged behavior and response shape.
3. THE backend SHALL preserve the existing `GET /api/compliance/vcl-multi/vertical/:code/burndown` endpoint with unchanged behavior and response shape.
4. THE backend SHALL preserve the existing `GET /api/compliance/vcl-multi/stats` endpoint with unchanged behavior and response shape, including the `vertical_breakdown` and `metric_breakdown` fields.

View File

@@ -0,0 +1,151 @@
# Implementation Plan: CCP Metrics View Restructure
## Overview
Restructure the CCP Metrics page from a vertical-first drill-down model to a metric-first model. Two new backend endpoints aggregate metrics across verticals. The frontend replaces VerticalTable/VerticalDetailView with MetricTable/MetricDetailView, removes the "By Vertical" table from AggregatedBurndownChart, and inverts the drill-down state hierarchy.
## Tasks
- [x] 1. Add backend endpoints for metric-centric aggregation
- [x] 1.1 Implement GET /metrics endpoint in backend/routes/vclMultiVertical.js
- Add route handler that queries `vcl_multi_vertical_summary` using only `ALL:` rollup rows from the latest upload per vertical
- Aggregate by `metric_id`: SUM non_compliant, compliant, total; MAX metric_desc, category; AVG target
- Compute `compliance_pct` as `compliant / total` (0 when total is 0)
- Sort by `non_compliant` DESC
- Return `{ metrics: [...] }` with empty array when no data exists
- Require `requireAuth()` middleware
- Return HTTP 500 with `{ "error": "Database error" }` on query failure
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9_
- [x] 1.2 Implement GET /metric/:id/verticals endpoint in backend/routes/vclMultiVertical.js
- Validate metric ID parameter (reject if > 50 chars with HTTP 400)
- Query all rows for the metric from latest uploads
- Separate `ALL:` rollup rows from sub-team rows
- Build per-vertical entries with nested `sub_teams` arrays
- Sort verticals by `non_compliant` DESC
- Return `{ metric_id, metric_desc, category, verticals: [...] }` with empty verticals array when metric not found
- Require `requireAuth()` middleware
- Return HTTP 500 with `{ "error": "Database error" }` on query failure
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9_
- [ ]* 1.3 Write property test: Metrics aggregation uses only rollup rows from latest uploads
- **Property 1: Metrics aggregation uses only rollup rows from latest uploads**
- Generate random `vcl_multi_vertical_summary` rows with multiple verticals, metrics, uploads, and team configurations
- Assert the endpoint returns exactly one entry per distinct `metric_id` with totals matching only `ALL:`-prefixed rows from the latest upload per vertical
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- **Validates: Requirements 3.1, 3.3**
- [ ]* 1.4 Write property test: Metrics computed fields are mathematically correct
- **Property 2: Metrics computed fields are mathematically correct**
- Assert `compliance_pct` equals `compliant / total` (or 0 when total is 0) for each metric
- Assert `target` equals the arithmetic mean of target values across verticals for each metric
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- **Validates: Requirements 3.4, 3.5**
- [ ]* 1.5 Write property test: Metrics response is sorted by non-compliant descending
- **Property 3: Metrics response is sorted by non-compliant descending**
- Assert each entry's `non_compliant` is >= the next entry's `non_compliant`
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- **Validates: Requirements 3.6**
- [ ]* 1.6 Write property test: Metric-verticals breakdown is complete and correct
- **Property 4: Metric-verticals breakdown is complete and correct**
- Assert one vertical entry per vertical that has the metric in its latest upload
- Assert each entry's `sub_teams` contains exactly the non-rollup team rows for that metric/vertical
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- **Validates: Requirements 4.1, 4.3**
- [ ]* 1.7 Write property test: Metric-verticals response is sorted by non-compliant descending
- **Property 5: Metric-verticals response is sorted by non-compliant descending**
- Assert each vertical entry's `non_compliant` is >= the next entry's `non_compliant`
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- **Validates: Requirements 4.4**
- [x] 2. Checkpoint - Backend endpoints verified
- Ensure all tests pass, ask the user if questions arise.
- [x] 3. Remove "By Vertical" table from AggregatedBurndownChart
- [x] 3.1 Remove the "By Vertical" contribution table JSX from AggregatedBurndownChart in frontend/src/components/pages/CCPMetricsPage.js
- Remove the entire `{data.by_vertical && data.by_vertical.length > 0 && (...)}` block that renders the per-vertical table below the bar chart
- Preserve the summary header (total non-compliant, blockers, in-progress, projected clear date) and bar chart
- Preserve all loading, error, empty-state, and all-blockers display behaviors
- _Requirements: 1.1, 1.2, 1.3_
- [x] 4. Implement MetricTable component and replace VerticalTable
- [x] 4.1 Create MetricTable component in frontend/src/components/pages/CCPMetricsPage.js
- Render one row per metric from the `/metrics` endpoint response
- Columns: Metric ID, Description, Category, Compliant, Non-Compliant, Total, Compliance %, Target %
- Sort rows by non-compliant descending (server-side, already sorted)
- Rows are clickable — clicking triggers `onSelectMetric(metricId)`
- Handle empty state (no metrics data)
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5_
- [x] 4.2 Replace VerticalTable usage with MetricTable in the overview render logic
- Add fetch call to `GET /api/compliance/vcl-multi/metrics` in the overview data loading
- Pass fetched metrics data to MetricTable
- Wire `onSelectMetric` to set `selectedMetric` state
- _Requirements: 2.4, 2.5_
- [x] 5. Implement MetricDetailView and update drill-down state
- [x] 5.1 Create MetricDetailView component in frontend/src/components/pages/CCPMetricsPage.js
- Fetch data from `GET /metric/:id/verticals` on mount
- Display header with metric ID, description, category
- Display aggregated stats cards (total, compliant, non-compliant, compliance %)
- Display table of verticals with columns: vertical name, compliant, non-compliant, total, compliance %
- Clicking a vertical row calls `onSelectVertical(vertical, verticalData)`
- Include "Back to Overview" button that calls `onBack`
- Handle loading, error, and empty states
- _Requirements: 5.1, 5.2, 5.3, 5.4, 6.2_
- [x] 5.2 Restructure drill-down state in CCPMetricsPage main component
- Change state model from `selectedVertical → selectedMetric → selectedTeam` to `selectedMetric → selectedVertical → selectedTeam`
- Add state variables: `selectedMetric`, `selectedMetricData`, `selectedVertical`, `selectedVerticalData`, `selectedTeam`
- Update render logic: no metric = Overview (MetricTable), metric only = MetricDetailView, metric+vertical = MetricSubTeamView, all three = MetricDeviceList
- Wire back-navigation handlers at each level (clear appropriate state to go up one level)
- Pass `selectedVerticalData.sub_teams` to MetricSubTeamView as `metricData`
- _Requirements: 5.4, 5.5, 5.6, 5.7, 5.8, 6.1, 6.2, 6.3, 6.4, 6.5_
- [ ]* 5.3 Write property test: Drill-down state determines rendered view
- **Property 6: Drill-down state determines rendered view**
- Generate random combinations of `(selectedMetric, selectedVertical, selectedTeam)` state values
- Assert exactly one view is rendered for each combination following the state hierarchy rules
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- **Validates: Requirements 6.1, 6.5**
- [ ] 6. Verify backward compatibility of existing endpoints
- [ ]* 6.1 Write integration tests verifying existing endpoints are unchanged
- Verify `GET /vertical/:code/metrics` returns same response shape
- Verify `GET /vertical/:code/metric/:metricId/devices` returns same response shape
- Verify `GET /vertical/:code/burndown` returns same response shape
- Verify `GET /stats` still returns `vertical_breakdown` and `metric_breakdown` fields
- Test file: `backend/__tests__/ccp-metrics-view-restructure.property.test.js`
- _Requirements: 7.1, 7.2, 7.3, 7.4_
- [x] 7. Final checkpoint - Ensure all tests pass
- Ensure all tests pass, ask the user if questions arise.
## Notes
- Tasks marked with `*` are optional and can be skipped for faster MVP
- Each task references specific requirements for traceability
- Checkpoints ensure incremental validation
- Property tests validate universal correctness properties from the design document
- The existing `MetricSubTeamView` and `MetricDeviceList` components are reused with minor prop adjustments — no new component creation needed for those
- All backend endpoints are added to the existing `backend/routes/vclMultiVertical.js` file
- All frontend changes are within `frontend/src/components/pages/CCPMetricsPage.js`
- Property-based tests use `fast-check` (already in project dependencies)
## Task Dependency Graph
```json
{
"waves": [
{ "id": 0, "tasks": ["1.1", "1.2"] },
{ "id": 1, "tasks": ["1.3", "1.4", "1.5", "1.6", "1.7", "3.1"] },
{ "id": 2, "tasks": ["4.1"] },
{ "id": 3, "tasks": ["4.2", "5.1"] },
{ "id": 4, "tasks": ["5.2"] },
{ "id": 5, "tasks": ["5.3", "6.1"] }
]
}
```