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": "13977c27-d996-4859-a09b-f0162bb13be0", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,328 @@
# Design Document: Forecast Burndown Chart
## Overview
This feature adds a per-metric forecast burndown chart to the CCP Metrics page. It combines historical compliance data (from `compliance_snapshots`) with forward-looking remediation projections (derived from `compliance_items` resolution dates) into a single stacked bar + line chart. A metric selector allows users to switch between individual compliance metrics (e.g., 2.3.5, 2.3.6, 5.2.5).
The design separates concerns into:
1. A **pure helper function** (`computeMetricForecastBurndown`) for testable forecast computation
2. Two **API endpoints** (forecast-burndown data + metrics-list) added to the existing VCL multi-vertical router
3. A **React chart component** using recharts `ComposedChart` with `Bar` + `Line` + `ReferenceLine`
Key design decisions:
- **No caching**: The API queries `compliance_items` directly on each request so edits on the AEO Compliance page are immediately reflected.
- **Pure computation**: The forecast logic is a stateless helper function that accepts pre-fetched data, making it testable in isolation without database mocks.
- **Existing patterns**: Routes use the `createVCLMultiVerticalRouter` factory pattern; the chart component follows the same inline-style + recharts patterns as `AggregatedBurndownChart` and `TrendChart`.
## Architecture
```mermaid
flowchart TD
subgraph Frontend
MS[MetricSelector] -->|selected metric_id| FBC[ForecastBurndownChart]
FBC -->|fetch| API1[GET /metric/:metricId/forecast-burndown]
MS -->|fetch on mount| API2[GET /metrics-list]
end
subgraph Backend
API1 --> RH1[Route Handler]
API2 --> RH2[Route Handler]
RH1 -->|query| DB[(PostgreSQL)]
RH1 -->|compute| HF[computeMetricForecastBurndown]
RH2 -->|query| DB
end
subgraph Database
DB --> CI[compliance_items]
DB --> CS[compliance_snapshots]
end
```
The data flow:
1. On page load, `MetricSelector` fetches `/api/compliance/vcl-multi/metrics-list` to populate the dropdown.
2. When a metric is selected, `ForecastBurndownChart` fetches `/api/compliance/vcl-multi/metric/:metricId/forecast-burndown`.
3. The route handler queries `compliance_items` for active devices and `compliance_snapshots` for historical data, then passes both to `computeMetricForecastBurndown`.
4. The helper returns structured data that the frontend renders as a `ComposedChart`.
## Components and Interfaces
### Backend: Helper Function
**File:** `backend/helpers/vclHelpers.js`
```javascript
/**
* Computes per-metric forecast burndown from device records and historical snapshots.
*
* @param {Array<{hostname: string, resolution_date: string|null}>} currentDevices
* Active non-compliant devices for the metric
* @param {number} totalAssets
* Total device count in scope for this metric (from snapshot or summary)
* @param {Array<{month: string, total_assets: number, non_compliant: number, compliance_pct: number}>} historicalSnapshots
* Pre-computed historical data points (up to 4 months)
* @returns {{
* historical: Array<{month: string, total_assets: number, non_compliant: number, compliance_pct: number}>,
* forecast: Array<{month: string, total_assets: number, non_compliant: number, compliance_pct: number}>,
* current_snapshot: {total_assets: number, non_compliant: number, compliant: number, compliance_pct: number, blockers: number, with_dates: number}
* }}
*/
function computeMetricForecastBurndown(currentDevices, totalAssets, historicalSnapshots) { ... }
```
### Backend: API Endpoints
**File:** `backend/routes/vclMultiVertical.js` (added to existing router)
| Method | Path | Auth | Description |
|--------|------|------|-------------|
| GET | `/api/compliance/vcl-multi/metric/:metricId/forecast-burndown` | `requireAuth()` | Returns combined historical + forecast data for a metric |
| GET | `/api/compliance/vcl-multi/metrics-list` | `requireAuth()` | Returns list of metrics with active non-compliant device counts |
**Forecast Burndown Response Shape:**
```json
{
"metric_id": "2.3.5",
"historical": [
{ "month": "2025-01", "total_assets": 500, "non_compliant": 45, "compliance_pct": 91.0 },
{ "month": "2025-02", "total_assets": 510, "non_compliant": 38, "compliance_pct": 92.5 }
],
"forecast": [
{ "month": "2025-04", "total_assets": 520, "non_compliant": 25, "compliance_pct": 95.2 },
{ "month": "2025-05", "total_assets": 520, "non_compliant": 15, "compliance_pct": 97.1 }
],
"current_snapshot": {
"total_assets": 520,
"non_compliant": 30,
"compliant": 490,
"compliance_pct": 94.2,
"blockers": 8,
"with_dates": 22
}
}
```
**Metrics List Response Shape:**
```json
[
{ "metric_id": "2.3.5", "device_count": 30 },
{ "metric_id": "2.3.6", "device_count": 12 },
{ "metric_id": "5.2.5", "device_count": 45 }
]
```
### Frontend: Components
**File:** `frontend/src/components/pages/CCPMetricsPage.js` (added to existing file)
1. **`MetricSelector`** — Dropdown component that fetches and displays available metrics. Triggers forecast data fetch on selection change.
2. **`ForecastBurndownChart`** — Renders a `ComposedChart` with:
- Blue `Bar` for `total_assets` (left Y-axis)
- Orange `Bar` for `non_compliant` (left Y-axis, stacked appearance)
- Green `Line` for `compliance_pct` (right Y-axis, 0100%)
- `ReferenceLine` as vertical divider between historical and forecast sections
- Forecast data points rendered at 50% opacity
## Data Models
### Database Tables (existing, no schema changes required)
**`compliance_items`** (relevant columns):
| Column | Type | Description |
|--------|------|-------------|
| hostname | TEXT | Device hostname |
| metric_id | TEXT | Compliance metric identifier (e.g., "2.3.5") |
| status | TEXT | `'active'` or `'resolved'` |
| resolution_date | DATE | Planned remediation date (nullable) |
| vertical | TEXT | Organizational vertical (e.g., "NTS_AEO") |
| team | TEXT | Team assignment |
**`compliance_snapshots`** (relevant columns):
| Column | Type | Description |
|--------|------|-------------|
| snapshot_month | TEXT | Month in YYYY-MM format |
| vertical | TEXT | Organizational vertical |
| total_devices | INTEGER | Total devices in scope |
| compliant | INTEGER | Compliant device count |
| non_compliant | INTEGER | Non-compliant device count |
| compliance_pct | NUMERIC(5,2) | Compliance percentage |
### Internal Data Structures
**Chart data point (shared between historical and forecast):**
```javascript
{
month: "2025-04", // YYYY-MM
total_assets: 520, // Total devices in scope
non_compliant: 25, // Non-compliant count
compliance_pct: 95.2, // Percentage (0-100, 1 decimal)
isForecast: true // Frontend flag for opacity styling
}
```
**Current snapshot:**
```javascript
{
total_assets: 520,
non_compliant: 30,
compliant: 490,
compliance_pct: 94.2,
blockers: 8, // Devices with no resolution_date
with_dates: 22 // Devices with a resolution_date
}
```
### Query Patterns
**Metrics list query:**
```sql
SELECT metric_id, COUNT(DISTINCT hostname) AS device_count
FROM compliance_items
WHERE status = 'active' AND vertical IS NOT NULL
GROUP BY metric_id
ORDER BY metric_id ASC
```
**Active devices for a metric:**
```sql
SELECT hostname, resolution_date, vertical
FROM compliance_items
WHERE metric_id = $1 AND status = 'active' AND vertical IS NOT NULL
```
**Historical snapshots for a vertical (3 months prior):**
```sql
SELECT snapshot_month AS month, total_devices AS total_assets,
non_compliant, compliance_pct::numeric AS compliance_pct
FROM compliance_snapshots
WHERE vertical = $1 AND snapshot_month >= $2 AND snapshot_month < $3
ORDER BY snapshot_month ASC
```
## 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.*
The `computeMetricForecastBurndown` helper function is a pure function with clear input/output behavior and universal properties that hold across a wide input space. This makes it an ideal candidate for property-based testing.
### Property 1: Forecast structure invariant
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `computeMetricForecastBurndown` function SHALL return an object with `historical`, `forecast`, and `current_snapshot` fields, where every element in the `forecast` array contains `month` (YYYY-MM string), `total_assets` (equal to the input `totalAssets` or the current snapshot's total), `non_compliant` (non-negative integer), and `compliance_pct` (number between 0 and 100), and all `total_assets` values in the forecast array are identical.
**Validates: Requirements 1.4, 3.1, 3.6**
### Property 2: Blocker and with_dates partition invariant
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `computeMetricForecastBurndown` function SHALL produce a `current_snapshot` where `blockers + with_dates = non_compliant`.
**Validates: Requirements 3.2**
### Property 3: Compliance percentage formula correctness
*For any* valid inputs where `totalAssets > 0`, the `computeMetricForecastBurndown` function SHALL compute `compliance_pct` for each forecast and current_snapshot data point as `ROUND((total_assets - non_compliant) / total_assets * 100, 1)`. When `totalAssets` is 0, all `compliance_pct` values SHALL be 0.
**Validates: Requirements 3.3, 3.10**
### Property 4: Forecast non_compliant monotonicity
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `forecast` array produced by `computeMetricForecastBurndown` SHALL have monotonically non-increasing `non_compliant` values (each month's `non_compliant` is less than or equal to the previous month's `non_compliant`).
**Validates: Requirements 3.4**
### Property 5: Per-month non_compliant computation correctness
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, for each month in the `forecast` array, the `non_compliant` value SHALL equal the count of devices whose `resolution_date` is after that month (not yet remediated) plus the count of devices with no `resolution_date` (blockers).
**Validates: Requirements 3.8**
### Property 6: Forecast horizon bound
*For any* valid `currentDevices` array and non-negative `totalAssets` integer, the `forecast` array produced by `computeMetricForecastBurndown` SHALL contain at most 12 elements, and SHALL terminate either when all devices with resolution dates are projected to be remediated or at the 12-month maximum, whichever comes first.
**Validates: Requirements 1.9, 3.9**
### Property 7: Past-due resolution dates treated as current month
*For any* device in `currentDevices` whose `resolution_date` is in a month that has already passed relative to the current date, the `computeMetricForecastBurndown` function SHALL treat that device as projected to be remediated in the current month (i.e., it is excluded from `non_compliant` in the first forecast month if that month is after the current month, but remains in `non_compliant` for the current month's snapshot).
**Validates: Requirements 6.5**
## Error Handling
### Backend API Errors
| Scenario | HTTP Status | Response Body | Behavior |
|----------|-------------|---------------|----------|
| Unauthenticated request | 401 | `{ "error": "Unauthorized" }` | `requireAuth()` middleware rejects |
| Invalid/unknown metricId | 200 | `{ metric_id, historical: [], forecast: [], current_snapshot: {zeros} }` | Graceful empty response |
| Database query failure | 500 | `{ "error": "Failed to compute forecast burndown" }` | Catch block logs error, returns 500 |
| Database failure on metrics-list | 500 | `{ "error": "Failed to fetch metrics list" }` | Catch block logs error, returns 500 |
### Frontend Error States
| Scenario | UI Behavior |
|----------|-------------|
| Metrics list fetch fails | Inline error with AlertCircle icon, red border, error message text |
| Forecast data fetch fails | Inline error in chart area with AlertCircle icon and error description |
| Empty metrics list | "No metrics with active non-compliant devices" message |
| Empty forecast + empty historical | "No data available for this metric" message in chart area |
| Race condition (rapid metric switching) | Discard stale responses; only render data from the most recent selection |
### Helper Function Edge Cases
| Input Condition | Behavior |
|-----------------|----------|
| `currentDevices` is empty | Return empty `forecast`, zeroed `current_snapshot` (except `total_assets`) |
| `totalAssets` is 0 | All `compliance_pct` values are 0 (avoid division by zero) |
| `totalAssets` < `currentDevices.length` | Use `currentDevices.length` as `non_compliant` without clamping |
| All devices are blockers (no resolution dates) | Return empty `forecast` array (no month-by-month projection possible) |
| All resolution dates are in the past | Treat as remediated in current month; forecast shows only blockers remaining |
## Testing Strategy
### Property-Based Tests (via fast-check)
The `computeMetricForecastBurndown` helper function is a pure function with well-defined invariants, making it ideal for property-based testing. Each property test runs a minimum of 100 iterations with randomly generated inputs.
**Library:** `fast-check` (already available in the project's test dependencies based on existing `.property.test.js` files)
**Test file:** `backend/__tests__/forecast-burndown-chart.property.test.js`
**Generator strategy:**
- `currentDevices`: Array of `{ hostname: arbitraryString, resolution_date: oneOf(null, arbitraryFutureDate, arbitraryPastDate) }`
- `totalAssets`: Non-negative integer (including 0 and values less than device count)
- `historicalSnapshots`: Array of 04 elements with valid month strings and non-negative counts
Each test is tagged with: `Feature: forecast-burndown-chart, Property {N}: {title}`
Properties to implement:
1. Forecast structure invariant
2. Blocker/with_dates partition invariant
3. Compliance percentage formula correctness
4. Forecast non_compliant monotonicity
5. Per-month non_compliant computation correctness
6. Forecast horizon bound
7. Past-due resolution dates treated as current month
### Unit Tests (example-based)
**Test file:** `backend/__tests__/forecast-burndown-chart.test.js`
- Specific examples with known inputs and expected outputs
- Edge cases: empty devices, zero totalAssets, all blockers, all past-due dates
- Integration with route handlers (mocked database)
### Integration Tests
- Verify API endpoints return correct response shapes with seeded database data
- Verify authentication middleware is applied
- Verify correct SQL filtering (vertical scoping, active-only devices)
- Verify historical data derivation from `compliance_snapshots`
### Frontend Tests
- Component rendering with mock data (snapshot tests)
- Loading and error states
- Metric selector interaction (selection triggers fetch)
- Race condition handling (rapid metric switching discards stale responses)

View File

@@ -0,0 +1,132 @@
# Requirements Document
## Introduction
This feature adds a per-metric forecast burndown chart to the CCP Metrics page. Unlike the existing aggregated burndown (which rolls up across all verticals by hostname), this chart focuses on individual compliance metrics (e.g., 2.3.5, 2.3.6, 5.2.5) and combines historical compliance data with forward-looking remediation projections in a single stacked bar + line chart. The chart displays actual historical data on the left side and forecasted future data on the right side, separated by a bold vertical divider. A metric selector allows users to switch between metrics. The chart reads from the same `compliance_items` table that the AEO Compliance page writes to, so device-level edits (resolution_date changes) dynamically feed into the forecast.
## Glossary
- **Forecast_Burndown_API**: The backend endpoint that returns combined historical and forecast burndown data for a specific compliance metric, identified by `metric_id`.
- **Metric_Selector**: A dropdown or picker UI component that allows the user to choose which compliance metric to display in the chart.
- **Historical_Data**: Monthly compliance snapshots from the past 3 months plus the current month, showing actual total asset counts, non-compliant device counts, and compliance percentages as recorded at each snapshot point.
- **Forecast_Data**: Projected future monthly data points computed from active non-compliant devices with scheduled `resolution_date` values, assuming total asset count remains constant and all remediation plans complete on schedule.
- **Compliance_Percentage**: The ratio of compliant devices to total devices for a given metric, expressed as a percentage (0%100%).
- **Total_Assets**: The total number of devices in scope for a given metric at a point in time.
- **Non_Compliant_Count**: The number of devices that are non-compliant for a given metric at a point in time.
- **Divider_Line**: A bold vertical line on the chart separating actual historical data (left) from forecasted future data (right).
- **CCP_Metrics_Page**: The page component (`CCPMetricsPage.js`) where the forecast burndown chart is displayed.
- **Compliance_Items_Table**: The `compliance_items` PostgreSQL table that tracks non-compliant devices with fields including `hostname`, `metric_id`, `status`, `resolution_date`, `vertical`, and `team`.
## Requirements
### Requirement 1: Per-Metric Forecast Burndown API Endpoint
**User Story:** As a compliance analyst, I want an API endpoint that returns combined historical and forecast burndown data for a specific metric, so that the frontend can render a chart showing past compliance trends and projected future remediation.
#### Acceptance Criteria
1. WHEN a GET request is made to `/api/compliance/vcl-multi/metric/:metricId/forecast-burndown`, THE Forecast_Burndown_API SHALL return a JSON response containing `metric_id`, `historical`, `forecast`, and `current_snapshot` fields within 3 seconds.
2. THE Forecast_Burndown_API SHALL compute `historical` as an array of exactly 4 monthly data points (the current month plus the 3 preceding months in chronological order), where each data point contains `month` (YYYY-MM), `total_assets`, `non_compliant`, and `compliance_pct`.
3. THE Forecast_Burndown_API SHALL derive historical `total_assets` and `non_compliant` counts from the `compliance_snapshots` table filtered by the vertical associated with the requested `metricId` in `compliance_items` (determined by querying the `vertical` column of active devices matching that `metric_id`), combined with per-metric device counts from `compliance_items` for that snapshot period.
4. THE Forecast_Burndown_API SHALL compute `forecast` as an array of projected future monthly data points, where each data point contains `month` (YYYY-MM), `total_assets`, `non_compliant`, and `compliance_pct`.
5. THE Forecast_Burndown_API SHALL project forecast data by assuming the current `total_assets` count remains constant and that each non-compliant device with a `resolution_date` in a future month will become compliant in that month.
6. THE Forecast_Burndown_API SHALL compute `current_snapshot` containing `total_assets`, `non_compliant`, `compliant`, `compliance_pct`, `blockers` (count of devices with no resolution_date), and `with_dates` (count of devices with a resolution_date).
7. THE Forecast_Burndown_API SHALL require authentication via `requireAuth()` middleware.
8. IF the `metricId` parameter does not match any active devices in `compliance_items` (where `status = 'active'` and `vertical IS NOT NULL`), THEN THE Forecast_Burndown_API SHALL return a 200 response with empty `historical` and `forecast` arrays and `current_snapshot` with all numeric fields set to 0.
9. THE Forecast_Burndown_API SHALL return forecast data extending forward until all devices with resolution dates are projected to be remediated, or for a maximum of 12 months from the current date, whichever comes first.
10. IF the database query fails or an internal error occurs while processing the request, THEN THE Forecast_Burndown_API SHALL return a 500 response with a JSON body containing an `error` field indicating the failure reason.
### Requirement 2: Available Metrics List Endpoint
**User Story:** As a frontend developer, I want an API endpoint that returns the list of distinct metrics with active non-compliant devices, so that the metric selector can be populated with valid options.
#### Acceptance Criteria
1. WHEN a GET request is made to `/api/compliance/vcl-multi/metrics-list`, THE Forecast_Burndown_API SHALL return a 200 response containing a JSON array of objects, each containing `metric_id` (string) and `device_count` (integer representing the number of distinct hostnames with status `active` for that metric) fields.
2. THE Forecast_Burndown_API SHALL include only metrics that have at least one active non-compliant device in `compliance_items` where `vertical IS NOT NULL`.
3. THE Forecast_Burndown_API SHALL sort the returned array by `metric_id` in ascending alphanumeric order.
4. THE Forecast_Burndown_API SHALL require authentication via `requireAuth()` middleware.
5. IF no metrics have active non-compliant devices with a non-null vertical, THEN THE Forecast_Burndown_API SHALL return a 200 response containing an empty JSON array.
6. IF the database query fails, THEN THE Forecast_Burndown_API SHALL return a 500 response with a JSON object containing an `error` field.
### Requirement 3: Forecast Computation Logic
**User Story:** As a developer, I want a pure helper function that computes forecast burndown data for a given metric from device records and historical snapshots, so that the computation is testable in isolation.
#### Acceptance Criteria
1. THE `computeMetricForecastBurndown` helper function SHALL accept a `currentDevices` array (each element containing at minimum `hostname` and `resolution_date` fields, where `resolution_date` is either a YYYY-MM-DD string or null), a `totalAssets` count (non-negative integer), and a `historicalSnapshots` array (each element containing `month` as YYYY-MM, `total_assets`, `non_compliant`, and `compliance_pct`), and return an object with `historical`, `forecast`, and `current_snapshot` fields.
2. FOR ALL valid inputs, THE `computeMetricForecastBurndown` function SHALL satisfy the invariant: `current_snapshot.blockers + current_snapshot.with_dates = current_snapshot.non_compliant`.
3. FOR ALL valid inputs where `total_assets > 0`, THE `computeMetricForecastBurndown` function SHALL compute `compliance_pct` as `ROUND((total_assets - non_compliant) / total_assets * 100, 1)` for each data point.
4. FOR ALL forecast data points, THE `computeMetricForecastBurndown` function SHALL produce monotonically non-increasing `non_compliant` counts (each month has equal or fewer non-compliant devices than the previous month).
5. FOR ALL forecast data points, THE `computeMetricForecastBurndown` function SHALL produce monotonically non-decreasing `compliance_pct` values.
6. THE `computeMetricForecastBurndown` function SHALL hold `total_assets` constant across all forecast data points, using the current month's total as the baseline.
7. WHEN the `currentDevices` array is empty, THE `computeMetricForecastBurndown` function SHALL return an empty `forecast` array and `current_snapshot` with all zero values except `total_assets`.
8. FOR ALL forecast months, THE `computeMetricForecastBurndown` function SHALL compute `non_compliant` as the count of devices whose `resolution_date` is after that month (devices not yet remediated) plus `blockers` (devices with no resolution_date).
9. THE `computeMetricForecastBurndown` function SHALL generate forecast data points extending forward month-by-month until all devices with resolution dates are projected to be remediated, or for a maximum of 12 months from the current date, whichever comes first.
10. IF `totalAssets` is 0, THEN THE `computeMetricForecastBurndown` function SHALL return `compliance_pct` of 0 for all data points in `historical`, `forecast`, and `current_snapshot`.
11. IF `totalAssets` is less than the number of elements in `currentDevices`, THEN THE `computeMetricForecastBurndown` function SHALL use the count of `currentDevices` as `non_compliant` without clamping to `totalAssets`.
### Requirement 4: Forecast Burndown Chart Component
**User Story:** As a senior leader viewing the CCP Metrics page, I want to see a combined historical and forecast burndown chart for each metric, so that I can visualize past compliance trends and projected future remediation timelines.
#### Acceptance Criteria
1. THE Forecast_Burndown_Chart SHALL be displayed on the CCP_Metrics_Page in a dedicated section below the Metric_Selector.
2. THE Forecast_Burndown_Chart SHALL render a stacked bar chart where blue bars represent Total_Assets (100% height baseline) and orange bars represent Non_Compliant_Count.
3. THE Forecast_Burndown_Chart SHALL render a green line overlay showing Compliance_Percentage as a trend line with percentage labels at each data point.
4. THE Forecast_Burndown_Chart SHALL display a left Y-axis scaled to the maximum Total_Assets value for device counts, and a right Y-axis scaled from 0% to 100% for Compliance_Percentage.
5. THE Forecast_Burndown_Chart SHALL display the X-axis labeled with months (YYYY-MM format), showing up to 16 data points (up to 4 historical months plus up to 12 forecast months).
6. THE Forecast_Burndown_Chart SHALL render a bold vertical Divider_Line separating Historical_Data (left side) from Forecast_Data (right side), positioned between the last historical data point and the first forecast data point.
7. THE Forecast_Burndown_Chart SHALL render forecast bars and line segments with 50% opacity to visually distinguish projections from actuals.
8. THE Forecast_Burndown_Chart SHALL display raw device count labels inside the bars (total assets count in blue bars, non-compliant count in orange bars).
9. THE Forecast_Burndown_Chart SHALL display compliance percentage values as labels on the green trend line at each data point.
10. WHEN the Forecast_Burndown_API returns both empty `historical` and empty `forecast` arrays for a selected metric, THE Forecast_Burndown_Chart SHALL display a message indicating no data is available for that metric.
11. WHEN the user selects a different metric from the Metric_Selector, THE Forecast_Burndown_Chart SHALL fetch updated data from the Forecast_Burndown_API and re-render the chart with the new metric's data.
12. WHILE the Forecast_Burndown_API request is in flight, THE Forecast_Burndown_Chart SHALL display a loading indicator in place of the chart content.
13. IF the Forecast_Burndown_API request fails, THEN THE Forecast_Burndown_Chart SHALL display an inline error message with an AlertCircle icon and the error description, styled with a red border consistent with the existing error display pattern on the CCP_Metrics_Page.
14. WHEN the Forecast_Burndown_API returns a non-empty `historical` array but an empty `forecast` array, THE Forecast_Burndown_Chart SHALL render only the historical bars and trend line without a Divider_Line or forecast section.
### Requirement 5: Metric Selector Component
**User Story:** As a compliance analyst, I want a metric picker that lets me choose which metric's forecast burndown to view, so that I can analyze remediation progress for individual compliance requirements.
#### Acceptance Criteria
1. THE Metric_Selector SHALL be displayed above or adjacent to the Forecast_Burndown_Chart on the CCP_Metrics_Page.
2. THE Metric_Selector SHALL populate its options by fetching the available metrics list from the metrics-list endpoint on page load.
3. THE Metric_Selector SHALL display each metric option showing the `metric_id` and the count of active non-compliant devices.
4. WHEN the user selects a metric, THE Metric_Selector SHALL trigger a data fetch for that metric's forecast burndown and update the chart.
5. WHEN the Metric_Selector finishes loading the metrics list and defaults to the first metric in the sorted list, THE Metric_Selector SHALL automatically trigger a data fetch for that default metric's forecast burndown and update the chart.
6. WHILE the metrics list is loading, THE Metric_Selector SHALL display a loading indicator and remain non-interactive until the fetch completes or fails.
7. IF the metrics list endpoint returns an empty array, THEN THE Metric_Selector SHALL display a message indicating no metrics with active non-compliant devices exist.
8. IF the metrics list endpoint request fails, THEN THE Metric_Selector SHALL display an inline error message consistent with the existing error display pattern on the CCP_Metrics_Page.
9. WHILE the forecast burndown data is loading after a metric selection, THE Metric_Selector SHALL remain interactive, and if the user selects a different metric before the previous fetch completes, THE Metric_Selector SHALL discard the previous in-flight response and use only the result from the most recent selection.
### Requirement 6: Dynamic Data Integration with AEO Compliance Page
**User Story:** As a compliance analyst, I want the forecast burndown chart to reflect device-level edits made on the AEO Compliance page in real time, so that when I update a device's resolution_date for a metric, the forecast projection updates accordingly on the next chart load.
#### Acceptance Criteria
1. THE Forecast_Burndown_API SHALL query the `compliance_items` table directly (not a cached copy), so that edits made via the AEO Compliance page's `PATCH /items/:hostname/metadata` endpoint are reflected in the next API call made after the PATCH response completes.
2. WHEN a user changes a device's `resolution_date` on the AEO Compliance page for a specific metric, THE Forecast_Burndown_API SHALL include that updated date in its forecast computation on the next request for that metric.
3. WHEN a device's status changes from `active` to `resolved` on the AEO Compliance page, THE Forecast_Burndown_API SHALL exclude that device from the non-compliant count on the next request.
4. IF a device's `resolution_date` is cleared (set to NULL) on the AEO Compliance page, THEN THE Forecast_Burndown_API SHALL count that device as a blocker in the `current_snapshot.blockers` count and exclude it from month-by-month forecast remediation projections on the next request.
5. IF a device's `resolution_date` is changed to a date in a month that has already passed relative to the current date, THEN THE Forecast_Burndown_API SHALL treat that device as projected to be remediated in the current month (not exclude it from non-compliant count until its status changes to `resolved`).
6. THE Forecast_Burndown_API SHALL use the current state of `compliance_items` as the source of truth for the current snapshot and forecast projections, with no application-level caching between requests.
### Requirement 7: Historical Data Derivation
**User Story:** As a compliance analyst, I want the chart to show actual historical compliance data for the past 3 months, so that I can see the real trend leading into the forecast.
#### Acceptance Criteria
1. THE Forecast_Burndown_API SHALL derive historical data from the `compliance_snapshots` table, which records monthly total_devices, compliant, and non_compliant counts per vertical, for the 3 calendar months immediately preceding the current month.
2. THE Forecast_Burndown_API SHALL compute per-metric historical non_compliant count at each snapshot point by multiplying the vertical's snapshot non_compliant count by the ratio of the metric's non-compliant devices to the vertical's total non-compliant devices (as recorded in `compliance_items` for that period), rounding to the nearest integer.
3. IF the vertical's total non_compliant count is 0 at a historical snapshot point, THEN THE Forecast_Burndown_API SHALL set the metric's non_compliant count to 0 for that data point.
4. THE Forecast_Burndown_API SHALL use the vertical's snapshot total_devices as the metric's total_assets for each historical data point.
5. WHEN no historical snapshots exist for a metric's vertical, THE Forecast_Burndown_API SHALL return an empty `historical` array.
6. THE Forecast_Burndown_API SHALL include the current month as the most recent historical data point, computed from live `compliance_items` data rather than a stored snapshot.
7. THE Forecast_Burndown_API SHALL return historical data points in chronological order (oldest first).

View File

@@ -0,0 +1,158 @@
# Implementation Plan: Forecast Burndown Chart
## Overview
Add a per-metric forecast burndown chart to the CCP Metrics page. A pure helper function (`computeMetricForecastBurndown`) computes forecast projections from device records and historical snapshots. Two new API endpoints serve the metrics list and forecast data. A React frontend renders a metric selector and a ComposedChart (Bar + Line + ReferenceLine) using recharts. No database migrations are needed — the feature reads from existing `compliance_items` and `compliance_snapshots` tables.
## Tasks
- [x] 1. Implement the computeMetricForecastBurndown helper function
- [x] 1.1 Add computeMetricForecastBurndown to backend/helpers/vclHelpers.js
- Implement the pure function accepting `currentDevices`, `totalAssets`, and `historicalSnapshots`
- Return object with `historical`, `forecast`, and `current_snapshot` fields
- Compute `current_snapshot.blockers` (devices with no resolution_date) and `current_snapshot.with_dates` (devices with a resolution_date)
- Compute `compliance_pct` as `ROUND((total_assets - non_compliant) / total_assets * 100, 1)`, returning 0 when totalAssets is 0
- Generate forecast months by iterating forward from current month, decrementing non_compliant as devices reach their resolution_date month
- Treat past-due resolution dates as remediated in the current month
- Hold total_assets constant across all forecast data points
- Terminate forecast when all dated devices are remediated or at 12-month maximum
- Return empty forecast array when all devices are blockers (no resolution dates)
- Export the function for use in route handlers and tests
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 3.10, 3.11, 6.5_
- [ ]* 1.2 Write property test: Forecast structure invariant
- **Property 1: Forecast structure invariant**
- **Validates: Requirements 1.4, 3.1, 3.6**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [ ]* 1.3 Write property test: Blocker and with_dates partition invariant
- **Property 2: Blocker and with_dates partition invariant**
- **Validates: Requirements 3.2**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [ ]* 1.4 Write property test: Compliance percentage formula correctness
- **Property 3: Compliance percentage formula correctness**
- **Validates: Requirements 3.3, 3.10**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [ ]* 1.5 Write property test: Forecast non_compliant monotonicity
- **Property 4: Forecast non_compliant monotonicity**
- **Validates: Requirements 3.4**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [ ]* 1.6 Write property test: Per-month non_compliant computation correctness
- **Property 5: Per-month non_compliant computation correctness**
- **Validates: Requirements 3.8**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [ ]* 1.7 Write property test: Forecast horizon bound
- **Property 6: Forecast horizon bound**
- **Validates: Requirements 1.9, 3.9**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [ ]* 1.8 Write property test: Past-due resolution dates treated as current month
- **Property 7: Past-due resolution dates treated as current month**
- **Validates: Requirements 6.5**
- Test file: `backend/__tests__/forecast-burndown-chart.property.test.js`
- [x] 2. Checkpoint - Helper function verified
- Ensure all tests pass, ask the user if questions arise.
- [x] 3. Implement backend API endpoints
- [x] 3.1 Add GET /metrics-list endpoint to backend/routes/vclMultiVertical.js
- Add route handler at `/metrics-list` with `requireAuth()` middleware
- Query `compliance_items` for distinct metric_ids with active non-compliant devices where vertical IS NOT NULL
- Return JSON array of `{ metric_id, device_count }` sorted by metric_id ascending
- Return empty array when no metrics have active devices
- Return HTTP 500 with `{ "error": "Failed to fetch metrics list" }` on database failure
- _Requirements: 2.1, 2.2, 2.3, 2.4, 2.5, 2.6_
- [x] 3.2 Add GET /metric/:metricId/forecast-burndown endpoint to backend/routes/vclMultiVertical.js
- Add route handler with `requireAuth()` middleware
- Query active devices for the metric from `compliance_items` (status = 'active', vertical IS NOT NULL)
- Determine the vertical from active devices and query `compliance_snapshots` for 3 months of historical data
- Compute per-metric historical non_compliant using the ratio method from Requirement 7.2
- Include current month as the most recent historical data point computed from live data
- Pass data to `computeMetricForecastBurndown` helper
- Return response with `metric_id`, `historical`, `forecast`, and `current_snapshot`
- Return 200 with empty arrays and zeroed snapshot when metricId has no active devices
- Return HTTP 500 with `{ "error": "Failed to compute forecast burndown" }` on database failure
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 1.10, 6.1, 6.2, 6.3, 6.4, 6.5, 6.6, 7.1, 7.2, 7.3, 7.4, 7.5, 7.6, 7.7_
- [ ]* 3.3 Write unit tests for API endpoints
- Test metrics-list returns correct shape with mocked database
- Test forecast-burndown returns correct shape with mocked database
- Test authentication middleware is applied to both endpoints
- Test empty/error states
- Test file: `backend/__tests__/forecast-burndown-chart.test.js`
- _Requirements: 1.7, 1.8, 1.10, 2.4, 2.5, 2.6_
- [x] 4. Checkpoint - Backend complete
- Ensure all tests pass, ask the user if questions arise.
- [x] 5. Implement frontend MetricSelector and ForecastBurndownChart components
- [x] 5.1 Add MetricSelector component to frontend/src/components/pages/CCPMetricsPage.js
- Fetch metrics list from `/api/compliance/vcl-multi/metrics-list` on mount
- Display dropdown showing each metric_id with active non-compliant device count
- Auto-select first metric and trigger forecast data fetch on load
- Handle loading state (non-interactive while fetching)
- Handle empty state ("No metrics with active non-compliant devices")
- Handle error state (inline error with AlertCircle icon, red border)
- On selection change, trigger `onMetricSelect` callback
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6, 5.7, 5.8_
- [x] 5.2 Add ForecastBurndownChart component to frontend/src/components/pages/CCPMetricsPage.js
- Fetch forecast data from `/api/compliance/vcl-multi/metric/:metricId/forecast-burndown` when metric changes
- Render recharts ComposedChart with:
- Blue Bar for total_assets (left Y-axis)
- Orange Bar for non_compliant (left Y-axis)
- Green Line for compliance_pct (right Y-axis, 0-100%)
- ReferenceLine as vertical divider between historical and forecast sections
- Render forecast data points at 50% opacity
- Display raw device count labels inside bars
- Display compliance percentage labels on the trend line
- X-axis labeled with months (YYYY-MM format)
- Left Y-axis scaled to max total_assets, right Y-axis 0-100%
- Handle loading state (loading indicator in chart area)
- Handle error state (inline error with AlertCircle icon and description)
- Handle empty data state ("No data available for this metric")
- Handle historical-only state (no divider line when forecast is empty)
- Discard stale responses on rapid metric switching (race condition handling)
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 4.10, 4.11, 4.12, 4.13, 4.14, 5.9_
- [x] 5.3 Wire MetricSelector and ForecastBurndownChart into CCPMetricsPage layout
- Add state for selected metric
- Place MetricSelector above ForecastBurndownChart in a dedicated section
- Connect selection change to chart data fetch
- Follow existing inline-style patterns from AggregatedBurndownChart and TrendChart
- _Requirements: 4.1, 5.1, 5.4_
- [x] 6. 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 helper function is pure and stateless — all 7 property tests exercise it in isolation without database mocks
- All backend changes are in `backend/helpers/vclHelpers.js` and `backend/routes/vclMultiVertical.js`
- All frontend changes are within `frontend/src/components/pages/CCPMetricsPage.js`
- Property-based tests use `fast-check` (already in project dependencies)
- No database migrations needed — uses existing `compliance_items` and `compliance_snapshots` tables
## Task Dependency Graph
```json
{
"waves": [
{ "id": 0, "tasks": ["1.1"] },
{ "id": 1, "tasks": ["1.2", "1.3", "1.4", "1.5", "1.6", "1.7", "1.8"] },
{ "id": 2, "tasks": ["3.1", "3.2"] },
{ "id": 3, "tasks": ["3.3"] },
{ "id": 4, "tasks": ["5.1", "5.2"] },
{ "id": 5, "tasks": ["5.3"] }
]
}
```