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
329 lines
15 KiB
Markdown
329 lines
15 KiB
Markdown
# 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, 0–100%)
|
||
- `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 0–4 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)
|