# 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)