Auto-sync .kiro/ from master (post-checkout hook)

This commit is contained in:
Jordan Ramos
2026-05-19 15:01:25 -06:00
parent ada9df26a8
commit 8ebd7e4d5e
23 changed files with 3485 additions and 19 deletions

View File

@@ -0,0 +1 @@
{"specId": "c717b04e-9452-4390-99bb-2c6871c1b9bd", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,319 @@
# Design Document: VCL Aggregated Burndown
## Overview
This feature adds an aggregated (cross-vertical) burndown forecast to the CCP Metrics overview page. Currently, burndown data is only available per-vertical via `GET /api/compliance/vcl-multi/vertical/:code/burndown`. This feature introduces a new endpoint `GET /api/compliance/vcl-multi/burndown` that rolls up burndown data across all verticals, a new pure helper function `computeAggregatedBurndown` for testable computation logic, and a new `AggregatedBurndownChart` React component displayed on the overview page.
The design reuses the existing `computeVerticalBurndown` pattern from `vclHelpers.js` and extends it with hostname deduplication (a device appearing in multiple metrics counts once) and per-vertical contribution breakdown. The frontend component follows the same Recharts `BarChart` pattern used in the per-vertical burndown chart within `VerticalDetailView`.
## Architecture
```mermaid
sequenceDiagram
participant FE as CCPMetricsPage
participant BE as Express Backend
participant DB as PostgreSQL
Note over FE,DB: Overview Page Load (existing + new)
FE->>BE: GET /api/compliance/vcl-multi/stats
BE->>DB: Aggregate stats across verticals
BE-->>FE: { stats, donut, vertical_breakdown }
FE->>BE: GET /api/compliance/vcl-multi/trend
BE->>DB: Monthly snapshots
BE-->>FE: { months: [...] }
FE->>BE: GET /api/compliance/vcl-multi/burndown
BE->>DB: SELECT hostname, resolution_date, vertical FROM compliance_items WHERE vertical IS NOT NULL AND status = 'active'
BE->>BE: Deduplicate by hostname (earliest resolution_date)
BE->>BE: computeAggregatedBurndown(devices)
BE-->>FE: { total_non_compliant, blockers, with_dates, monthly_forecast, projected_clear_date, by_vertical }
FE->>FE: Render AggregatedBurndownChart
```
### Data Flow
1. **Query** — Fetch all active non-compliant devices across all verticals from `compliance_items`.
2. **Deduplicate** — Group by hostname, keeping the earliest non-null `resolution_date` across metric entries. A device appearing in 3 metrics counts as 1 device.
3. **Compute** — Pass deduplicated device list to `computeAggregatedBurndown` which produces totals, monthly buckets, cumulative projection, and per-vertical breakdown.
4. **Respond** — Return the computed burndown data to the frontend.
5. **Render**`AggregatedBurndownChart` displays a bar chart of monthly remediations, summary stats header, and per-vertical contribution table.
## Components and Interfaces
### Backend
#### New Endpoint
**`GET /api/compliance/vcl-multi/burndown`**
Returns aggregated burndown forecast across all verticals.
- Auth: `requireAuth()`
- Route file: `backend/routes/vclMultiVertical.js`
- Response:
```json
{
"total_non_compliant": 400,
"blockers": 120,
"with_dates": 280,
"monthly_forecast": {
"2026-06": 85,
"2026-07": 110,
"2026-08": 55,
"2026-09": 30
},
"projected_clear_date": "2026-09",
"by_vertical": [
{ "vertical": "NTS_AEO", "total": 180, "blockers": 50, "with_dates": 130 },
{ "vertical": "SDIT_CISO", "total": 120, "blockers": 40, "with_dates": 80 },
{ "vertical": "TSI", "total": 100, "blockers": 30, "with_dates": 70 }
]
}
```
When no active non-compliant devices exist:
```json
{
"total_non_compliant": 0,
"blockers": 0,
"with_dates": 0,
"monthly_forecast": {},
"projected_clear_date": null,
"by_vertical": []
}
```
#### New Pure Helper Function
Added to `backend/helpers/vclHelpers.js`:
```javascript
/**
* Deduplicates devices by hostname, keeping the earliest non-null resolution_date.
* A device appearing in multiple metrics counts once.
*
* @param {Array<{ hostname: string, resolution_date: string|null, vertical: string }>} items
* @returns {Array<{ hostname: string, resolution_date: string|null, vertical: string }>}
*/
function deduplicateByHostname(items) { ... }
/**
* Computes aggregated burndown from a deduplicated array of device objects.
* Each device has { hostname, resolution_date, vertical }.
*
* @param {Array<{ hostname: string, resolution_date: string|null, vertical: string }>} devices
* @returns {{
* total: number,
* blockers: number,
* with_dates: number,
* monthly: Object<string, number>,
* projection: Object<string, { remediated: number, remaining: number }>,
* projected_clear_date: string|null,
* by_vertical: Array<{ vertical: string, total: number, blockers: number, with_dates: number }>
* }}
*/
function computeAggregatedBurndown(devices) { ... }
```
**`deduplicateByHostname` logic:**
- Groups items by hostname
- For each hostname, selects the earliest non-null `resolution_date` across all entries
- If all entries for a hostname have null dates, the device is a blocker
- Preserves the `vertical` from the first entry (for per-vertical breakdown, the endpoint groups separately)
**`computeAggregatedBurndown` logic:**
- Counts total devices, blockers (null date), and with_dates (non-null date)
- Buckets with_dates devices by YYYY-MM of resolution_date into `monthly`
- Sorts monthly keys chronologically
- Computes `projection` as cumulative remaining: starts at `total`, subtracts each month's count
- Sets `projected_clear_date` to the last month key if blockers = 0, otherwise null
- Groups devices by vertical for `by_vertical`, sorted descending by total, omitting verticals with zero devices
#### Endpoint Implementation Pattern
The endpoint follows the same pattern as the existing per-vertical burndown:
```javascript
router.get('/burndown', async (req, res) => {
try {
const { rows } = await pool.query(
`SELECT hostname, resolution_date, vertical
FROM compliance_items
WHERE vertical IS NOT NULL AND status = 'active'`
);
// Deduplicate by hostname (earliest non-null resolution_date)
const devices = deduplicateByHostname(rows);
const burndown = computeAggregatedBurndown(devices);
res.json({
total_non_compliant: burndown.total,
blockers: burndown.blockers,
with_dates: burndown.with_dates,
monthly_forecast: burndown.monthly,
projected_clear_date: burndown.projected_clear_date,
by_vertical: burndown.by_vertical,
});
} catch (err) {
console.error('[VCL Multi] GET /burndown error:', err.message);
res.status(500).json({ error: 'Database error' });
}
});
```
### Frontend
#### New Component: `AggregatedBurndownChart`
Inline component within `CCPMetricsPage.js` (following the existing pattern where `StatsBar`, `DonutChart`, `TrendChart`, and `VerticalTable` are all defined in the same file).
**Placement:** Below the existing charts row (TrendChart + DonutChart), above the VerticalTable.
**Behavior:**
- Fetches `GET /api/compliance/vcl-multi/burndown` on page load alongside existing stats/trend calls
- Displays a summary header with total non-compliant, blockers, in-progress, and projected clear date
- Renders a Recharts `BarChart` with one bar per monthly bucket (purple fill, matching existing burndown chart style)
- Below the chart, renders a compact per-vertical contribution table sorted by total descending
- Shows "No non-compliant devices" message when total = 0
- Shows "All X non-compliant devices lack remediation dates" when monthly_forecast is empty but blockers > 0
- Shows loading spinner while fetching
- Shows inline error message on API failure
**Chart specification:**
```javascript
<ResponsiveContainer width="100%" height={200}>
<BarChart data={monthlyData}>
<CartesianGrid stroke="rgba(255,255,255,0.05)" strokeDasharray="3 3" />
<XAxis dataKey="month" tick={{ fontSize: 10, fill: '#64748B' }} />
<YAxis tick={{ fontSize: 10, fill: '#64748B' }} />
<Tooltip contentStyle={{ background: '#1E293B', border: '1px solid #334155', borderRadius: '0.5rem', fontSize: '0.75rem' }} />
<Bar dataKey="count" fill="#A78BFA" fillOpacity={0.7} name="Projected Remediations" />
</BarChart>
</ResponsiveContainer>
```
## Data Models
No schema changes required. The feature reads from the existing `compliance_items` table:
```sql
-- Existing columns used:
-- hostname TEXT
-- vertical TEXT (nullable)
-- status TEXT ('active'|'resolved')
-- resolution_date DATE (nullable)
```
The query for the aggregated burndown:
```sql
SELECT hostname, resolution_date, vertical
FROM compliance_items
WHERE vertical IS NOT NULL AND status = 'active'
```
This is the same data source used by the per-vertical burndown endpoint, just without the `vertical = $1` filter.
## 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: Partition Invariant
*For any* array of device objects passed to `computeAggregatedBurndown`, the result must satisfy `blockers + with_dates = total`. Every device is either a blocker (null resolution_date) or in-progress (non-null resolution_date), with no device uncounted or double-counted.
**Validates: Requirements 2.2**
### Property 2: Monthly Bucket Conservation
*For any* array of device objects passed to `computeAggregatedBurndown`, the sum of all values in the `monthly` object must equal `with_dates`. Every in-progress device appears in exactly one monthly bucket, and no device is lost or duplicated during bucketing.
**Validates: Requirements 2.3, 1.5**
### Property 3: Chronological Monthly Ordering
*For any* array of device objects passed to `computeAggregatedBurndown`, the keys of the `monthly` object must be in ascending chronological order (lexicographic sort of YYYY-MM strings).
**Validates: Requirements 2.4**
### Property 4: Cumulative Projection Consistency
*For any* array of device objects passed to `computeAggregatedBurndown`, the `projection` object must satisfy: for each month in chronological order, `projection[month].remaining = total - (cumulative sum of monthly[m] for all m <= month)`. The first month's remaining equals `total - monthly[first_month]`.
**Validates: Requirements 2.5**
### Property 5: Projected Clear Date Logic
*For any* array of device objects passed to `computeAggregatedBurndown`: if `blockers > 0`, then `projected_clear_date` must be `null`; if `blockers = 0` and `with_dates > 0`, then `projected_clear_date` must equal the last (chronologically greatest) key in `monthly`.
**Validates: Requirements 1.7**
### Property 6: Hostname Deduplication with Earliest Date
*For any* array of items where the same hostname appears multiple times with different resolution_dates, `deduplicateByHostname` must produce exactly one entry per unique hostname, and that entry's `resolution_date` must be the earliest non-null date among all entries for that hostname (or null if all entries have null dates).
**Validates: Requirements 1.6**
### Property 7: Aggregation Consistency with Per-Vertical Computation
*For any* array of device objects spanning multiple verticals, the aggregated `total` must equal the sum of per-vertical totals, the aggregated `blockers` must equal the sum of per-vertical blockers, the aggregated `with_dates` must equal the sum of per-vertical with_dates, and for each month key, the aggregated monthly count must equal the sum of that month's count across all per-vertical computations.
**Validates: Requirements 4.1, 4.2, 4.3, 4.4**
### Property 8: By-Vertical Sorting and Filtering
*For any* array of device objects spanning multiple verticals, the `by_vertical` array must be sorted in descending order by `total`, must not contain any entry where `total = 0`, and the sum of all `by_vertical[i].total` must equal the overall `total`.
**Validates: Requirements 5.1, 5.2, 5.4**
## Error Handling
| Condition | HTTP Status | Response | Behavior |
|---|---|---|---|
| Database error | 500 | `{ "error": "Database error" }` | Log error, return 500 |
| Unauthenticated request | 401 | `{ "error": "Authentication required" }` | Middleware rejects |
| No active non-compliant devices | 200 | Zero/empty response (see above) | Graceful empty state |
Frontend error handling:
- API failure: inline error message in red monospace text, consistent with existing error patterns on the page
- Loading state: `<Loader />` spinner with "Loading..." text
- Empty state (total = 0): informational message instead of empty chart
- All blockers (monthly empty, blockers > 0): message indicating all devices lack dates
## Testing Strategy
### Property-Based Testing
Use `fast-check` (already used in this project). Each correctness property maps to a single property-based test with minimum 100 iterations.
Property tests target the pure helper functions exported from `backend/helpers/vclHelpers.js`:
- `deduplicateByHostname` — Property 6
- `computeAggregatedBurndown` — Properties 1, 2, 3, 4, 5, 7, 8
Tag format: **Feature: vcl-aggregated-burndown, Property {number}: {title}**
Test file: `backend/__tests__/vcl-aggregated-burndown.property.test.js`
### Unit Testing
Unit tests cover specific examples and edge cases:
- **Empty input** — verify all-zero response (Requirement 2.6)
- **All blockers** — verify with_dates = 0, monthly = {}, projected_clear_date = null (Requirement 2.7)
- **Single device, single metric** — verify basic computation
- **Duplicate hostnames across metrics** — verify deduplication picks earliest date
- **Duplicate hostnames where all dates are null** — verify device is a blocker
- **API endpoint integration** — verify response shape with mocked DB data
- **Auth middleware** — verify 401 without session
Test file: `backend/__tests__/vcl-aggregated-burndown.test.js`
### Frontend Testing
- Component renders loading state
- Component renders empty state message when total = 0
- Component renders blocker-only message when monthly is empty
- Component renders bar chart with correct data
- Component renders per-vertical table sorted correctly
- Component renders error message on API failure

View File

@@ -0,0 +1,84 @@
# Requirements Document
## Introduction
This feature adds an aggregated (cross-vertical) burndown forecast chart to the main CCP Metrics overview page. Currently, burndown forecasts only exist at the per-vertical drill-down level — when a user clicks into a specific vertical (e.g., NTS_AEO), they see that vertical's burndown showing blockers, devices with dates, and monthly projected remediation. This feature rolls up the same burndown concept across ALL verticals and displays it on the overview page alongside the existing Stats Bar, Compliance Trend chart, and Non-Compliant Status donut chart.
## Glossary
- **Aggregated_Burndown_API**: The backend endpoint that computes burndown forecast data across all verticals by querying active non-compliant devices from `compliance_items` where `vertical IS NOT NULL`.
- **Burndown_Chart**: A Recharts-based bar chart component that visualizes monthly projected remediation counts and cumulative remaining non-compliant devices across all verticals.
- **Blocker**: A non-compliant device in `compliance_items` with `status = 'active'` and `resolution_date IS NULL` — no committed remediation timeline exists.
- **In_Progress_Device**: A non-compliant device in `compliance_items` with `status = 'active'` and a non-null `resolution_date` — a target remediation date has been set.
- **Monthly_Bucket**: A grouping of in-progress devices by the month portion (YYYY-MM) of their `resolution_date`, representing how many devices are expected to be remediated in that month.
- **Projected_Clear_Date**: The earliest month by which all in-progress devices (those with resolution dates) are projected to be remediated, assuming blockers remain unresolved.
- **Overview_Page**: The main CCP Metrics page (`CCPMetricsPage.js`) that displays aggregated cross-vertical statistics before any vertical drill-down is selected.
## Requirements
### Requirement 1: Aggregated Burndown API Endpoint
**User Story:** As a compliance analyst, I want a single API endpoint that returns burndown forecast data aggregated across all verticals, so that the overview page can display a cross-organizational remediation projection without requiring multiple per-vertical API calls.
#### Acceptance Criteria
1. WHEN a GET request is made to `/api/compliance/vcl-multi/burndown`, THE Aggregated_Burndown_API SHALL return a JSON response containing `total_non_compliant`, `blockers`, `with_dates`, `monthly_forecast`, and `projected_clear_date` fields.
2. THE Aggregated_Burndown_API SHALL compute `total_non_compliant` as the count of distinct hostnames in `compliance_items` where `vertical IS NOT NULL` and `status = 'active'`.
3. THE Aggregated_Burndown_API SHALL compute `blockers` as the count of distinct hostnames where `resolution_date IS NULL` among active non-compliant devices across all verticals.
4. THE Aggregated_Burndown_API SHALL compute `with_dates` as the count of distinct hostnames where `resolution_date IS NOT NULL` among active non-compliant devices across all verticals.
5. THE Aggregated_Burndown_API SHALL compute `monthly_forecast` by bucketing in-progress devices into Monthly_Buckets based on the YYYY-MM portion of their `resolution_date`, with each bucket containing the count of devices projected to be remediated in that month.
6. THE Aggregated_Burndown_API SHALL deduplicate devices by hostname before computing burndown totals — a device appearing in multiple metrics counts once, using the earliest non-null `resolution_date` across its metric entries.
7. THE Aggregated_Burndown_API SHALL return `projected_clear_date` as the last month in the sorted monthly forecast when all in-progress devices have been accounted for, or `null` when blockers exist.
8. WHEN no active non-compliant devices exist across any vertical, THE Aggregated_Burndown_API SHALL return `total_non_compliant: 0`, `blockers: 0`, `with_dates: 0`, `monthly_forecast: {}`, and `projected_clear_date: null`.
9. THE Aggregated_Burndown_API SHALL require authentication via `requireAuth()` middleware.
### Requirement 2: Aggregated Burndown Computation Logic
**User Story:** As a developer, I want a pure helper function that computes aggregated burndown from a set of device objects, so that the computation is testable in isolation and reusable.
#### Acceptance Criteria
1. THE `computeAggregatedBurndown` helper function SHALL accept an array of device objects (each with `hostname` and `resolution_date` fields) and return an object with `total`, `blockers`, `with_dates`, `monthly`, `projection`, and `projected_clear_date` fields.
2. FOR ALL valid input arrays, THE `computeAggregatedBurndown` function SHALL satisfy the invariant: `blockers + with_dates = total`.
3. FOR ALL valid input arrays, THE `computeAggregatedBurndown` function SHALL satisfy the invariant: the sum of all values in `monthly` equals `with_dates`.
4. THE `computeAggregatedBurndown` function SHALL produce `monthly` buckets sorted chronologically by month key (YYYY-MM format).
5. THE `computeAggregatedBurndown` function SHALL compute `projection` as a cumulative remaining count per month — starting from `total` and subtracting each month's remediated count in chronological order.
6. WHEN the input array is empty, THE `computeAggregatedBurndown` function SHALL return `total: 0`, `blockers: 0`, `with_dates: 0`, `monthly: {}`, `projection: {}`, and `projected_clear_date: null`.
7. WHEN all devices have `resolution_date = null`, THE `computeAggregatedBurndown` function SHALL return `blockers` equal to `total`, `with_dates: 0`, `monthly: {}`, and `projected_clear_date: null`.
### Requirement 3: Burndown Chart Frontend Component
**User Story:** As a senior leader viewing the CCP Metrics overview page, I want to see an aggregated burndown forecast chart showing when non-compliant devices across all verticals are projected to be remediated, so that I can assess organizational remediation progress at a glance.
#### Acceptance Criteria
1. THE Burndown_Chart SHALL be displayed on the Overview_Page below the Compliance Trend chart and Non-Compliant Status donut chart section.
2. THE Burndown_Chart SHALL display a bar chart with one bar per Monthly_Bucket, where the bar height represents the count of devices projected to be remediated in that month.
3. THE Burndown_Chart SHALL display a summary header showing the total non-compliant count, blocker count, in-progress count, and projected clear date.
4. THE Burndown_Chart SHALL use the Recharts `BarChart` component with styling consistent with the existing Compliance Trend chart (dark background, teal/purple color palette, monospace axis labels).
5. WHEN the Aggregated_Burndown_API returns `total_non_compliant: 0`, THE Burndown_Chart SHALL display a message indicating no non-compliant devices exist rather than rendering an empty chart.
6. WHEN the Aggregated_Burndown_API returns `monthly_forecast` with zero entries but `blockers > 0`, THE Burndown_Chart SHALL display the blocker count with a message indicating all non-compliant devices lack remediation dates.
7. THE Burndown_Chart SHALL fetch data from the Aggregated_Burndown_API on page load and display a loading indicator while the request is in flight.
8. IF the Aggregated_Burndown_API request fails, THEN THE Burndown_Chart SHALL display an inline error message consistent with the existing error display pattern on the Overview_Page.
### Requirement 4: Burndown Data Consistency
**User Story:** As a compliance analyst, I want the aggregated burndown numbers to be consistent with the per-vertical burndown data, so that I can trust the overview numbers match the sum of individual verticals.
#### Acceptance Criteria
1. FOR ALL sets of active non-compliant devices, THE Aggregated_Burndown_API `total_non_compliant` SHALL equal the sum of `total_non_compliant` values returned by each individual per-vertical burndown endpoint (`GET /api/compliance/vcl-multi/vertical/:code/burndown`).
2. FOR ALL sets of active non-compliant devices, THE Aggregated_Burndown_API `blockers` SHALL equal the sum of `blockers` values returned by each individual per-vertical burndown endpoint.
3. FOR ALL sets of active non-compliant devices, THE Aggregated_Burndown_API `with_dates` SHALL equal the sum of `with_dates` values returned by each individual per-vertical burndown endpoint.
4. FOR ALL monthly buckets, THE Aggregated_Burndown_API monthly forecast count for a given month SHALL equal the sum of that month's forecast count across all per-vertical burndown responses.
### Requirement 5: Per-Vertical Contribution Breakdown
**User Story:** As a compliance analyst, I want to see which verticals contribute the most to the aggregated burndown, so that I can identify which organizations need the most attention for remediation planning.
#### Acceptance Criteria
1. THE Aggregated_Burndown_API response SHALL include a `by_vertical` array containing per-vertical breakdowns with fields: `vertical`, `total`, `blockers`, `with_dates`.
2. THE `by_vertical` array SHALL be sorted in descending order by `total` (most non-compliant devices first).
3. THE Burndown_Chart component SHALL display the per-vertical breakdown below the bar chart as a compact summary table showing each vertical's contribution to the overall burndown.
4. WHEN a vertical has zero active non-compliant devices, THE Aggregated_Burndown_API SHALL omit that vertical from the `by_vertical` array.

View File

@@ -0,0 +1,113 @@
# Implementation Plan: VCL Aggregated Burndown
## Overview
Implement an aggregated cross-vertical burndown forecast feature consisting of: two new pure helper functions (`deduplicateByHostname` and `computeAggregatedBurndown`) in `vclHelpers.js`, a new `GET /api/compliance/vcl-multi/burndown` endpoint in the existing `vclMultiVertical.js` route file, property-based tests validating 8 correctness properties, unit tests covering edge cases and API integration, and an `AggregatedBurndownChart` inline component in `CCPMetricsPage.js`.
## Tasks
- [x] 1. Implement backend helper functions
- [x] 1.1 Add `deduplicateByHostname` function to `backend/helpers/vclHelpers.js`
- Groups items by hostname
- For each hostname, selects the earliest non-null `resolution_date` across all entries
- If all entries for a hostname have null dates, the device is a blocker (null date preserved)
- Preserves the `vertical` from the first entry for that hostname
- Export the function from the module
- _Requirements: 1.6_
- [x] 1.2 Add `computeAggregatedBurndown` function to `backend/helpers/vclHelpers.js`
- Accepts an array of device objects with `hostname`, `resolution_date`, and `vertical` fields
- Counts total devices, blockers (null date), and with_dates (non-null date)
- Buckets with_dates devices by YYYY-MM of resolution_date into `monthly` object
- Sorts monthly keys chronologically
- Computes `projection` as cumulative remaining: starts at `total`, subtracts each month's count
- Sets `projected_clear_date` to the last month key if blockers = 0, otherwise null
- Groups devices by vertical for `by_vertical`, sorted descending by total, omitting verticals with zero devices
- Each `by_vertical` entry has `{ vertical, total, blockers, with_dates }`
- Returns `{ total, blockers, with_dates, monthly, projection, projected_clear_date, by_vertical }`
- Export the function from the module
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 5.1, 5.2, 5.4_
- [x]* 1.3 Write property tests for `deduplicateByHostname` and `computeAggregatedBurndown`
- **Property 1: Partition Invariant** — `blockers + with_dates = total` for any input
- **Validates: Requirements 2.2**
- **Property 2: Monthly Bucket Conservation** — sum of monthly values = with_dates
- **Validates: Requirements 2.3, 1.5**
- **Property 3: Chronological Monthly Ordering** — monthly keys in ascending YYYY-MM order
- **Validates: Requirements 2.4**
- **Property 4: Cumulative Projection Consistency** — projection[month].remaining = total - cumulative sum
- **Validates: Requirements 2.5**
- **Property 5: Projected Clear Date Logic** — null when blockers > 0, last month key when blockers = 0
- **Validates: Requirements 1.7**
- **Property 6: Hostname Deduplication with Earliest Date** — one entry per hostname, earliest non-null date
- **Validates: Requirements 1.6**
- **Property 7: Aggregation Consistency with Per-Vertical Computation** — aggregated totals = sum of per-vertical totals
- **Validates: Requirements 4.1, 4.2, 4.3, 4.4**
- **Property 8: By-Vertical Sorting and Filtering** — sorted descending by total, no zero-total entries, sum = overall total
- **Validates: Requirements 5.1, 5.2, 5.4**
- Test file: `backend/__tests__/vcl-aggregated-burndown.property.test.js`
- [x] 2. Implement backend API endpoint
- [x] 2.1 Add `GET /burndown` route to `backend/routes/vclMultiVertical.js`
- Query `compliance_items` for all active non-compliant devices across verticals
- Call `deduplicateByHostname` on the query results
- Call `computeAggregatedBurndown` on the deduplicated devices
- Map the result to the API response shape: `{ total_non_compliant, blockers, with_dates, monthly_forecast, projected_clear_date, by_vertical }`
- Handle database errors with 500 status and `{ error: "Database error" }`
- Route is protected by `requireAuth()` (already applied via `router.use`)
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9_
- [x]* 2.2 Write unit tests for the burndown endpoint
- Test empty DB returns zero/empty response (Requirement 1.8)
- Test all-blocker scenario returns with_dates=0, monthly={}, projected_clear_date=null (Requirement 2.7)
- Test single device single metric basic computation
- Test duplicate hostnames across metrics — verify deduplication picks earliest date
- Test duplicate hostnames where all dates are null — verify device is a blocker
- Test response shape matches API contract
- Test 401 without auth session (mock requireAuth to reject)
- Test file: `backend/__tests__/vcl-aggregated-burndown.test.js`
- _Requirements: 1.1, 1.8, 1.9, 2.6, 2.7_
- [x] 3. Checkpoint - Ensure all backend tests pass
- Ensure all tests pass, ask the user if questions arise.
- [x] 4. Implement frontend component
- [x] 4.1 Add `AggregatedBurndownChart` component to `frontend/src/components/pages/CCPMetricsPage.js`
- Add as an inline component following the existing pattern (StatsBar, DonutChart, TrendChart are all in the same file)
- Fetch `GET /api/compliance/vcl-multi/burndown` on page load alongside existing stats/trend calls
- Display summary header with total non-compliant, blockers, in-progress, and projected clear date
- Render a Recharts `BarChart` with one bar per monthly bucket (purple fill `#A78BFA`, fillOpacity 0.7)
- Below the chart, render a compact per-vertical contribution table sorted by total descending
- Show "No non-compliant devices" message when total = 0
- Show "All X non-compliant devices lack remediation dates" when monthly_forecast is empty but blockers > 0
- Show `<Loader />` spinner while fetching
- Show inline error message on API failure
- Place the component below the charts row (TrendChart + DonutChart), above the VerticalTable
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 5.3_
- [x] 5. 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
- Unit tests validate specific examples and edge cases
- The design uses JavaScript — all implementations use Node.js/Express (backend) and React 19 (frontend)
- The `vclMultiVertical.js` route file already exists with `router.use(requireAuth())` applied globally
## Task Dependency Graph
```json
{
"waves": [
{ "id": 0, "tasks": ["1.1"] },
{ "id": 1, "tasks": ["1.2"] },
{ "id": 2, "tasks": ["1.3", "2.1"] },
{ "id": 3, "tasks": ["2.2"] },
{ "id": 4, "tasks": ["4.1"] }
]
}
```