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
15 KiB
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:
- A pure helper function (
computeMetricForecastBurndown) for testable forecast computation - Two API endpoints (forecast-burndown data + metrics-list) added to the existing VCL multi-vertical router
- A React chart component using recharts
ComposedChartwithBar+Line+ReferenceLine
Key design decisions:
- No caching: The API queries
compliance_itemsdirectly 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
createVCLMultiVerticalRouterfactory pattern; the chart component follows the same inline-style + recharts patterns asAggregatedBurndownChartandTrendChart.
Architecture
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:
- On page load,
MetricSelectorfetches/api/compliance/vcl-multi/metrics-listto populate the dropdown. - When a metric is selected,
ForecastBurndownChartfetches/api/compliance/vcl-multi/metric/:metricId/forecast-burndown. - The route handler queries
compliance_itemsfor active devices andcompliance_snapshotsfor historical data, then passes both tocomputeMetricForecastBurndown. - The helper returns structured data that the frontend renders as a
ComposedChart.
Components and Interfaces
Backend: Helper Function
File: backend/helpers/vclHelpers.js
/**
* 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:
{
"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:
[
{ "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)
-
MetricSelector— Dropdown component that fetches and displays available metrics. Triggers forecast data fetch on selection change. -
ForecastBurndownChart— Renders aComposedChartwith:- Blue
Barfortotal_assets(left Y-axis) - Orange
Barfornon_compliant(left Y-axis, stacked appearance) - Green
Lineforcompliance_pct(right Y-axis, 0–100%) ReferenceLineas vertical divider between historical and forecast sections- Forecast data points rendered at 50% opacity
- Blue
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):
{
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:
{
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:
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:
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):
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:
- Forecast structure invariant
- Blocker/with_dates partition invariant
- Compliance percentage formula correctness
- Forecast non_compliant monotonicity
- Per-month non_compliant computation correctness
- Forecast horizon bound
- 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)