Add Atlas metrics reporting, security audit tracker, and spec documents

This commit is contained in:
root
2026-04-24 17:30:06 +00:00
parent 8bf8dc55dd
commit 5ffedad02f
11 changed files with 2101 additions and 22 deletions

View File

@@ -0,0 +1 @@
{"specId": "a3e7c1d2-8f4b-4a91-b6e3-9d2f5c8a1b74", "workflowType": "requirements-first", "specType": "feature"}

View File

@@ -0,0 +1,362 @@
# Design Document: Atlas Metrics Report
## Overview
This feature adds a tab system to the existing Metric Graphs panel on the ReportingPage, separating Ivanti donut charts from new Atlas coverage charts. A new `GET /api/atlas/metrics` endpoint aggregates cached Atlas action plan data into chart-ready metrics. The frontend renders three new donut charts — coverage, plan type distribution, and plan status distribution — under an "Atlas Coverage" tab, while the existing four Ivanti donuts move under an "Ivanti Findings" tab.
### Key Design Decisions
- **Server-side aggregation**: The metrics endpoint computes counts and distributions on the backend rather than shipping raw `plans_json` to the client. This keeps the frontend simple and avoids parsing potentially large JSON arrays in the browser.
- **Reuse existing donut helpers**: The new Atlas donut charts reuse the `polarToCartesian` and `donutArcPath` SVG helper functions already defined in ReportingPage.js, along with the same dimensions (180px size, 72px outer radius, 48px inner radius). This keeps the visual language consistent.
- **Fetch once, refresh on sync**: Atlas metrics are fetched once on page mount and re-fetched only after a successful Atlas sync. Tab switches do not trigger new API calls.
- **No server.js changes**: The new endpoint is added inside the existing `createAtlasRouter(db, requireAuth)` factory function in `backend/routes/atlas.js`. The router is already mounted at `/api/atlas` in server.js.
- **Tab state is local**: The active tab is stored in React component state — no URL params, no localStorage. The default is "Ivanti Findings" to preserve the existing experience.
- **IvantiCountsChart conditional visibility**: The trend chart below the Metric Graphs panel is only shown when the Ivanti tab is active, since it has no relevance to Atlas data.
## Architecture
```mermaid
graph TD
subgraph Frontend - ReportingPage
TS[Tab System<br/>Ivanti Findings | Atlas Coverage]
ID[Ivanti Donuts<br/>StatusDonut, ActionCoverageDonut,<br/>FPWorkflowDonut x2]
AD[Atlas Donuts<br/>CoverageDonut, PlanTypeDonut,<br/>PlanStatusDonut]
IC[IvantiCountsChart]
end
subgraph Backend - Atlas Router
ME[GET /api/atlas/metrics]
AC[(atlas_action_plans_cache<br/>SQLite)]
end
TS -->|tab = ivanti| ID
TS -->|tab = ivanti| IC
TS -->|tab = atlas| AD
AD -->|fetch on mount + after sync| ME
ME -->|SELECT + aggregate| AC
```
### Data Flow: Metrics Fetch
1. ReportingPage mounts → calls `GET /api/atlas/metrics`
2. Backend queries all rows from `atlas_action_plans_cache`, including `plans_json`
3. Backend iterates rows, parses each `plans_json`, counts plans by type and status
4. Backend returns aggregated JSON: `{ totalHosts, hostsWithPlans, hostsWithoutPlans, plansByType, plansByStatus, totalPlans }`
5. Frontend stores result in `atlasMetrics` state
6. When "Atlas Coverage" tab is active, three donut components render from this state
### Data Flow: Refresh After Sync
1. User clicks Atlas sync button → `POST /api/atlas/sync` (existing)
2. On success, frontend calls `GET /api/atlas/metrics` again
3. Atlas donut charts re-render with updated data
## Components and Interfaces
### Backend: GET /api/atlas/metrics Endpoint
Added inside the existing `createAtlasRouter(db, requireAuth)` factory function in `backend/routes/atlas.js`.
| Method | Path | Auth | Group | Description |
|--------|------|------|-------|-------------|
| `GET` | `/metrics` | requireAuth | any | Return aggregated Atlas metrics for chart rendering |
**Response shape**:
```json
{
"totalHosts": 42,
"hostsWithPlans": 28,
"hostsWithoutPlans": 14,
"plansByType": {
"decommission": 5,
"remediation": 18,
"false_positive": 3,
"risk_acceptance": 8,
"scan_exclusion": 2
},
"plansByStatus": {
"active": 25,
"expired": 7,
"completed": 4
},
"totalPlans": 36
}
```
**Implementation approach**:
1. Query all rows: `SELECT has_action_plan, plans_json FROM atlas_action_plans_cache`
2. Initialize counters: `totalHosts = rows.length`, `hostsWithPlans = 0`, `hostsWithoutPlans = 0`, `plansByType = {}`, `plansByStatus = {}`, `totalPlans = 0`
3. For each row:
- If `has_action_plan === 1`, increment `hostsWithPlans`; else increment `hostsWithoutPlans`
- Try to parse `plans_json`; on failure, skip plan details for that row
- For each plan in the parsed array, increment the corresponding `plansByType[plan.plan_type]` and `plansByStatus[plan.status]` counters, and increment `totalPlans`
4. Return the aggregated object
Uses the existing `dbAll` promise wrapper already defined in `atlas.js`.
### Frontend: Tab System
A horizontal tab bar rendered inside the Metric Graphs panel header, to the right of the "Metric Graphs" title and `PieChart` icon.
**State**: `metricsTab``'ivanti'` (default) or `'atlas'`
**Tab bar structure**:
```
┌──────────────────────────────────────────────────────────────┐
│ [PieChart icon] METRIC GRAPHS [Ivanti Findings] [Atlas Coverage] │
└──────────────────────────────────────────────────────────────┘
```
**Styling**:
- Tabs use `role="tab"` and `aria-selected`; the content area uses `role="tabpanel"`
- Active tab: `color: #F59E0B`, `borderBottom: 2px solid #F59E0B`
- Inactive tab: `color: #64748B`, no bottom border
- Hover on inactive: `background: rgba(245, 158, 11, 0.06)`
- Font: `'JetBrains Mono', monospace`, `0.7rem`, `uppercase`, `letterSpacing: 0.08em`
- Tabs are keyboard navigable via Tab and Enter keys
### Frontend: Atlas Metrics State
New state variables in the ReportingPage component:
```javascript
const [metricsTab, setMetricsTab] = useState('ivanti');
const [atlasMetrics, setAtlasMetrics] = useState(null);
const [atlasMetricsLoading, setAtlasMetricsLoading] = useState(false);
const [atlasMetricsError, setAtlasMetricsError] = useState(null);
```
New fetch function:
```javascript
const fetchAtlasMetrics = useCallback(async () => {
setAtlasMetricsLoading(true);
setAtlasMetricsError(null);
try {
const res = await fetch(`${API_BASE}/atlas/metrics`, { credentials: 'include' });
if (res.ok) {
const data = await res.json();
setAtlasMetrics(data);
} else {
const err = await res.json().catch(() => ({}));
setAtlasMetricsError(err.error || 'Failed to fetch Atlas metrics');
}
} catch (err) {
setAtlasMetricsError(err.message);
} finally {
setAtlasMetricsLoading(false);
}
}, []);
```
Called on mount (in the existing `useEffect`) and after a successful Atlas sync.
### Frontend: Atlas Donut Charts
Three new inline components defined in ReportingPage.js, following the same pattern as `StatusDonut`, `ActionCoverageDonut`, and `FPWorkflowDonut`. All reuse the existing `polarToCartesian` and `donutArcPath` helper functions.
**AtlasCoverageDonut**
- Props: `{ hostsWithPlans, hostsWithoutPlans, totalHosts }`
- Segments: emerald (`#10B981`) for with plans, amber (`#F59E0B`) for without plans
- Center text: `totalHosts` count, "HOSTS" label
- Empty state: "No data — run Atlas Sync"
**AtlasPlanTypeDonut**
- Props: `{ plansByType, totalPlans }`
- Color map: `decommission: #EF4444`, `remediation: #0EA5E9`, `false_positive: #A855F7`, `risk_acceptance: #F59E0B`, `scan_exclusion: #64748B`
- Center text: `totalPlans` count, "PLANS" label
- Legend: only shows types with count > 0
- Empty state: "No plans — run Atlas Sync"
**AtlasPlanStatusDonut**
- Props: `{ plansByStatus, totalPlans }`
- Color map: `active: #10B981`, `expired: #EF4444`, `completed: #0EA5E9`, fallback: `#64748B`
- Center text: `totalPlans` count, "STATUS" label
- Legend: only shows statuses with count > 0
- Empty state: "No plans — run Atlas Sync"
All three follow the same SVG dimensions: 180px size, 72px outer radius, 48px inner radius.
### Frontend: Metric Graphs Panel Layout
When "Ivanti Findings" tab is active:
- Existing four donuts in horizontal flex row with dividers (unchanged)
- IvantiCountsChart rendered below the panel (unchanged)
When "Atlas Coverage" tab is active:
- Three Atlas donuts in horizontal flex row with dividers (same layout pattern)
- IvantiCountsChart hidden
## Data Models
### Metrics Endpoint Response
| Field | Type | Description |
|-------|------|-------------|
| `totalHosts` | integer | Count of all rows in `atlas_action_plans_cache` |
| `hostsWithPlans` | integer | Count of rows where `has_action_plan = 1` |
| `hostsWithoutPlans` | integer | Count of rows where `has_action_plan = 0` |
| `plansByType` | object | Map of plan type string → integer count |
| `plansByStatus` | object | Map of plan status string → integer count |
| `totalPlans` | integer | Sum of all plans across all hosts |
### Existing Atlas Cache Table (no changes)
The `atlas_action_plans_cache` table is unchanged. The metrics endpoint reads from it:
| Column | Type | Used by metrics endpoint |
|--------|------|--------------------------|
| `has_action_plan` | INTEGER | Counting hosts with/without plans |
| `plans_json` | TEXT | Parsed to count plans by type and status |
### Plan Object Shape (within `plans_json`)
The metrics endpoint reads `plan_type` and `status` from each plan object in the JSON array. The Atlas API returns these fields as strings. Example:
```json
{
"action_plan_id": "ap-123",
"plan_type": "remediation",
"status": "active",
"commit_date": "2026-07-01"
}
```
Known `plan_type` values: `decommission`, `remediation`, `false_positive`, `risk_acceptance`, `scan_exclusion`
Known `status` values: `active`, `expired`, `completed` (the frontend handles unknown values with a neutral color)
## 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: Metrics aggregation correctness
*For any* array of cache rows — each with a `has_action_plan` flag (0 or 1) and a `plans_json` string that is either valid JSON containing an array of plan objects (each with `plan_type` and `status` fields) or invalid JSON — the metrics aggregation function SHALL produce:
- `totalHosts` equal to the number of rows
- `hostsWithPlans + hostsWithoutPlans` equal to `totalHosts`
- `hostsWithPlans` equal to the count of rows where `has_action_plan === 1`
- `totalPlans` equal to the sum of plan array lengths across all rows with valid JSON
- Each key in `plansByType` maps to the count of plans with that `plan_type` across all valid rows
- Each key in `plansByStatus` maps to the count of plans with that `status` across all valid rows
- Rows with invalid `plans_json` are counted in `totalHosts` and `hostsWithPlans`/`hostsWithoutPlans` but their plans are excluded from `plansByType`, `plansByStatus`, and `totalPlans`
**Validates: Requirements 1.3, 1.4, 1.5**
### Property 2: Coverage donut data correctness
*For any* non-negative integer pair `(hostsWithPlans, hostsWithoutPlans)` where `totalHosts = hostsWithPlans + hostsWithoutPlans > 0`, the Coverage Donut SHALL display `totalHosts` as center text, and the legend SHALL show counts matching the input values with percentages that equal `(count / totalHosts) * 100` for each segment.
**Validates: Requirements 3.3, 3.4**
### Property 3: Plan type donut data correctness
*For any* `plansByType` object mapping plan type strings to positive integer counts, and `totalPlans` equal to the sum of those counts, the Plan Type Donut SHALL display `totalPlans` as center text, the legend SHALL include only types with count greater than zero, and each legend entry's percentage SHALL equal `(count / totalPlans) * 100`.
**Validates: Requirements 4.3, 4.4**
### Property 4: Plan status donut data correctness
*For any* `plansByStatus` object mapping status strings to positive integer counts, and `totalPlans` equal to the sum of those counts, the Plan Status Donut SHALL display `totalPlans` as center text, the legend SHALL include only statuses with count greater than zero, and each legend entry's percentage SHALL equal `(count / totalPlans) * 100`.
**Validates: Requirements 5.3, 5.4**
### Property 5: Plan status color assignment
*For any* status string, the Plan Status Donut color assignment function SHALL return `#10B981` if the status is `"active"`, `#EF4444` if `"expired"`, `#0EA5E9` if `"completed"`, and `#64748B` for any other string value.
**Validates: Requirements 5.2**
## Error Handling
### Backend Errors
| Error Scenario | Handling | HTTP Status | Response |
|----------------|----------|-------------|----------|
| Atlas not configured (missing env vars) | Return 503 with descriptive message | 503 | `{ error: 'Atlas API is not configured...' }` |
| Database query failure | Catch error, log, return 500 | 500 | `{ error: 'Failed to fetch Atlas metrics.' }` |
| Invalid JSON in `plans_json` column | Skip that row's plan details, continue processing | N/A (handled internally) | Metrics still returned with correct counts for valid rows |
| Empty cache table | Return metrics object with all zeros | 200 | `{ totalHosts: 0, hostsWithPlans: 0, hostsWithoutPlans: 0, plansByType: {}, plansByStatus: {}, totalPlans: 0 }` |
| Unauthenticated request | Auth middleware rejects | 401 | `{ error: 'Authentication required' }` |
### Frontend Errors
| Error Scenario | Handling | User Impact |
|----------------|----------|-------------|
| Metrics fetch returns non-200 | Store error message in `atlasMetricsError` state | Error message displayed in Atlas Coverage tab content area |
| Metrics fetch network failure | Catch error, store message | Error message displayed with failure reason |
| Metrics fetch in progress | `atlasMetricsLoading = true` | Loading spinner shown in Atlas Coverage tab content area |
| Atlas metrics data is null (not yet fetched) | Donut components check for null/undefined | Loading state or empty state shown |
## Testing Strategy
### Unit Tests
Unit tests cover specific examples, edge cases, and integration points:
**Backend — Metrics Aggregation (`GET /api/atlas/metrics`)**:
- Returns all-zero metrics when cache table is empty
- Correctly counts hosts with and without plans from seeded data
- Correctly aggregates plansByType from multiple hosts
- Correctly aggregates plansByStatus from multiple hosts
- Skips plan details for rows with invalid JSON but still counts the host
- Returns 503 when Atlas is not configured
- Requires authentication (returns 401 without session)
**Frontend — Tab System**:
- Renders two tabs with correct labels
- Defaults to "Ivanti Findings" tab on mount
- Switches content when tab is clicked
- Active tab has correct ARIA attributes (`aria-selected="true"`)
- Tab panel has `role="tabpanel"` attribute
- Keyboard navigation works (Tab + Enter)
**Frontend — Atlas Donut Charts**:
- Coverage donut shows "No data — run Atlas Sync" when totalHosts is 0
- Plan type donut shows "No plans — run Atlas Sync" when totalPlans is 0
- Plan status donut shows "No plans — run Atlas Sync" when totalPlans is 0
- SVG dimensions are 180px with 72px outer and 48px inner radius
- Color assignments match specification for each plan type
- Color assignments match specification for known statuses
**Frontend — Data Fetching**:
- Fetches metrics on mount
- Re-fetches metrics after successful Atlas sync
- Does not re-fetch on tab switch
- Shows loading indicator while fetch is in progress
- Shows error message when fetch fails
**Frontend — IvantiCountsChart Visibility**:
- Rendered when Ivanti tab is active
- Not rendered when Atlas tab is active
### Property-Based Tests
Property-based tests verify universal properties across generated inputs. Each test runs a minimum of 100 iterations.
**Library**: [fast-check](https://github.com/dubzzz/fast-check) — the standard PBT library for JavaScript/Node.js.
**Configuration**: Each property test runs with `{ numRuns: 100 }` minimum.
**Tag format**: Each test includes a comment referencing its design property:
```javascript
// Feature: atlas-metrics-report, Property 1: Metrics aggregation correctness
```
| Property | Test Description | Generator Strategy |
|----------|-----------------|-------------------|
| Property 1 | Metrics aggregation correctness | Generate arrays of objects with `has_action_plan` (0 or 1) and `plans_json` (either valid JSON array of `{plan_type, status}` objects or random invalid strings). Extract the aggregation logic into a pure function, call it with generated input, verify all invariants. |
| Property 2 | Coverage donut data correctness | Generate random `(hostsWithPlans, hostsWithoutPlans)` pairs of non-negative integers (at least one > 0). Render `AtlasCoverageDonut`, verify center text equals sum and legend percentages are mathematically correct. |
| Property 3 | Plan type donut data correctness | Generate random `plansByType` objects with 15 plan type keys mapped to positive integers. Render `AtlasPlanTypeDonut`, verify center text equals sum and legend entries match input. |
| Property 4 | Plan status donut data correctness | Generate random `plansByStatus` objects with 14 status keys mapped to positive integers. Render `AtlasPlanStatusDonut`, verify center text equals sum and legend entries match input. |
| Property 5 | Plan status color assignment | Generate random strings (mix of known statuses and arbitrary strings). Verify the color function returns the correct color for known statuses and the fallback for unknowns. |
### Integration Tests
- Full metrics flow: seed `atlas_action_plans_cache` with varied data → call `GET /api/atlas/metrics` → verify response matches expected aggregation
- Empty cache flow: ensure empty cache returns all-zero metrics
- Corrupt data flow: seed cache with mix of valid and invalid `plans_json` → verify metrics are correct for valid rows

View File

@@ -0,0 +1,124 @@
# Requirements Document
## Introduction
Add a tab system to the existing Metric Graphs panel on the ReportingPage so that Atlas-specific coverage metrics live alongside the existing Ivanti donut charts without cluttering the current layout. The existing four donut charts (Open vs Closed, Action Coverage, FP Finding Status, FP Workflow Status) move under an "Ivanti Findings" tab. A new "Atlas Coverage" tab displays donut charts derived from the cached Atlas action plan data — plan coverage, plan type breakdown, and plan status distribution. A new backend endpoint on the existing Atlas router aggregates the cached data into chart-ready metrics, keeping server.js untouched.
## Glossary
- **Dashboard**: The STEAM Security Dashboard frontend React application
- **ReportingPage**: The existing frontend page (`frontend/src/components/pages/ReportingPage.js`) displaying Ivanti host findings, metric charts, and the findings table
- **Metric_Graphs_Panel**: The existing panel on the ReportingPage containing four SVG donut charts and the IvantiCountsChart trend line
- **Tab_System**: A horizontal tab bar added to the Metric_Graphs_Panel header that switches between Ivanti and Atlas chart views
- **Atlas_Cache**: The existing `atlas_action_plans_cache` SQLite table storing per-host action plan status, including `plans_json`
- **Atlas_Router**: The existing Express route module (`backend/routes/atlas.js`) mounted at `/api/atlas`
- **Atlas_Metrics_Endpoint**: A new `GET /api/atlas/metrics` endpoint on the Atlas_Router that returns aggregated chart data
- **Coverage_Donut**: A donut chart showing hosts with action plans vs hosts without action plans
- **Plan_Type_Donut**: A donut chart showing the distribution of action plans across the five plan types (decommission, remediation, false_positive, risk_acceptance, scan_exclusion)
- **Plan_Status_Donut**: A donut chart showing the distribution of action plans across their status values (e.g. active, expired, completed)
- **Action_Plan**: A compliance plan in Atlas with a type, commit date, and status — stored in the `plans_json` column of the Atlas_Cache
- **Host_ID**: The shared numeric identifier linking an Ivanti host finding to an Atlas host
## Requirements
### Requirement 1: Atlas Metrics Aggregation Endpoint
**User Story:** As a frontend developer, I want a single endpoint that returns pre-aggregated Atlas metrics, so that the frontend can render donut charts without parsing raw plan JSON on the client.
#### Acceptance Criteria
1. THE Atlas_Router SHALL expose a `GET /api/atlas/metrics` endpoint that requires authentication
2. WHEN the metrics endpoint is called, THE Atlas_Router SHALL query all rows from the Atlas_Cache table including the `plans_json` column
3. WHEN rows are retrieved, THE Atlas_Router SHALL compute and return a JSON object containing: `totalHosts` (integer count of all cached hosts), `hostsWithPlans` (integer count of hosts where `has_action_plan` equals 1), `hostsWithoutPlans` (integer count of hosts where `has_action_plan` equals 0), `plansByType` (object mapping each plan type string to its integer count across all hosts), `plansByStatus` (object mapping each plan status string to its integer count across all hosts), and `totalPlans` (integer sum of all plans across all hosts)
4. WHEN the Atlas_Cache table is empty, THE Atlas_Router SHALL return the metrics object with all counts set to zero and `plansByType` and `plansByStatus` as empty objects
5. IF a row's `plans_json` column contains invalid JSON, THEN THE Atlas_Router SHALL skip that row's plan details and continue processing remaining rows
6. THE Atlas_Metrics_Endpoint SHALL NOT modify server.js — the endpoint SHALL be added to the existing Atlas_Router module only
### Requirement 2: Tab System in Metric Graphs Panel
**User Story:** As a dashboard user, I want the Metric Graphs panel to have tabs, so that I can switch between Ivanti findings metrics and Atlas coverage metrics without the panel becoming overcrowded.
#### Acceptance Criteria
1. THE Dashboard SHALL display a horizontal tab bar in the Metric_Graphs_Panel header area, to the right of the "Metric Graphs" title
2. THE Tab_System SHALL contain exactly two tabs labeled "Ivanti Findings" and "Atlas Coverage"
3. WHEN the ReportingPage loads, THE Tab_System SHALL default to the "Ivanti Findings" tab as the active tab
4. WHEN the "Ivanti Findings" tab is active, THE Metric_Graphs_Panel SHALL display the existing four donut charts (Open vs Closed, Action Coverage, FP Finding Status, FP Workflow Status) in their current layout
5. WHEN the "Atlas Coverage" tab is active, THE Metric_Graphs_Panel SHALL display the Atlas-specific donut charts (Coverage_Donut, Plan_Type_Donut, Plan_Status_Donut) in a horizontal flex row matching the existing chart layout pattern
6. THE Tab_System SHALL visually indicate the active tab using the design system accent color and a bottom border highlight
7. THE Tab_System SHALL use monospace font, uppercase text, and letter spacing consistent with the existing Metric_Graphs_Panel header style
8. WHEN the user switches tabs, THE Metric_Graphs_Panel content SHALL update immediately without a full page reload
### Requirement 3: Atlas Coverage Donut Chart
**User Story:** As a dashboard user, I want to see what percentage of cached hosts have Atlas action plans, so that I can gauge overall compliance coverage at a glance.
#### Acceptance Criteria
1. WHEN the "Atlas Coverage" tab is active, THE Dashboard SHALL display a Coverage_Donut chart showing hosts with plans vs hosts without plans
2. THE Coverage_Donut SHALL use emerald (`#10B981`) for hosts with plans and amber (`#F59E0B`) for hosts without plans
3. THE Coverage_Donut SHALL display the total host count as center text with a "HOSTS" label below it
4. THE Coverage_Donut SHALL include a legend showing the count and percentage for each segment
5. WHEN the Atlas_Cache contains no data, THE Coverage_Donut SHALL display a "No data — run Atlas Sync" message instead of an empty chart
6. THE Coverage_Donut SHALL follow the same SVG donut dimensions and styling as the existing StatusDonut component (180px size, 72px outer radius, 48px inner radius)
### Requirement 4: Plan Type Distribution Donut Chart
**User Story:** As a dashboard user, I want to see how action plans are distributed across plan types, so that I can understand the remediation strategy mix.
#### Acceptance Criteria
1. WHEN the "Atlas Coverage" tab is active, THE Dashboard SHALL display a Plan_Type_Donut chart showing the count of plans per type
2. THE Plan_Type_Donut SHALL assign a distinct color to each plan type: decommission (`#EF4444`), remediation (`#0EA5E9`), false_positive (`#A855F7`), risk_acceptance (`#F59E0B`), scan_exclusion (`#64748B`)
3. THE Plan_Type_Donut SHALL display the total plan count as center text with a "PLANS" label below it
4. THE Plan_Type_Donut SHALL include a legend showing the label, count, and percentage for each plan type that has a count greater than zero
5. WHEN no plans exist in the Atlas_Cache, THE Plan_Type_Donut SHALL display a "No plans — run Atlas Sync" message instead of an empty chart
6. THE Plan_Type_Donut SHALL follow the same SVG donut dimensions and styling as the existing donut chart components
### Requirement 5: Plan Status Distribution Donut Chart
**User Story:** As a dashboard user, I want to see how action plans are distributed across statuses, so that I can identify how many plans are active vs expired or completed.
#### Acceptance Criteria
1. WHEN the "Atlas Coverage" tab is active, THE Dashboard SHALL display a Plan_Status_Donut chart showing the count of plans per status value
2. THE Plan_Status_Donut SHALL assign colors to known statuses: active (`#10B981`), expired (`#EF4444`), completed (`#0EA5E9`), and use a neutral color (`#64748B`) for any unrecognized status values
3. THE Plan_Status_Donut SHALL display the total plan count as center text with a "STATUS" label below it
4. THE Plan_Status_Donut SHALL include a legend showing the label, count, and percentage for each status that has a count greater than zero
5. WHEN no plans exist in the Atlas_Cache, THE Plan_Status_Donut SHALL display a "No plans — run Atlas Sync" message instead of an empty chart
6. THE Plan_Status_Donut SHALL follow the same SVG donut dimensions and styling as the existing donut chart components
### Requirement 6: Atlas Metrics Data Fetching
**User Story:** As a frontend developer, I want the Atlas metrics fetched efficiently and kept in sync with the Atlas cache, so that the charts reflect the latest synced data without unnecessary API calls.
#### Acceptance Criteria
1. WHEN the ReportingPage mounts, THE Dashboard SHALL fetch Atlas metrics from `GET /api/atlas/metrics` and store the result in component state
2. WHEN an Atlas sync completes successfully (via the existing Atlas sync button), THE Dashboard SHALL re-fetch Atlas metrics from `GET /api/atlas/metrics` to update the charts
3. WHILE the Atlas metrics fetch is in progress, THE Dashboard SHALL display a loading indicator in the Atlas Coverage tab content area
4. IF the Atlas metrics fetch fails, THEN THE Dashboard SHALL display an error message in the Atlas Coverage tab content area with the failure reason
5. THE Dashboard SHALL NOT fetch Atlas metrics on every tab switch — the data SHALL be fetched once on mount and refreshed only after a sync operation
### Requirement 7: IvantiCountsChart Visibility with Tabs
**User Story:** As a dashboard user, I want the IvantiCountsChart trend line to remain visible when the Ivanti Findings tab is active, so that the existing reporting experience is preserved.
#### Acceptance Criteria
1. WHEN the "Ivanti Findings" tab is active, THE Dashboard SHALL display the IvantiCountsChart trend line below the Metric_Graphs_Panel, matching its current position
2. WHEN the "Atlas Coverage" tab is active, THE Dashboard SHALL hide the IvantiCountsChart trend line since it is not relevant to Atlas metrics
3. THE IvantiCountsChart component SHALL continue to function identically to its current behavior when visible
### Requirement 8: Tab System Styling and Accessibility
**User Story:** As a dashboard user, I want the tab system to be visually consistent with the existing design system and keyboard accessible, so that the interface feels cohesive and usable.
#### Acceptance Criteria
1. THE Tab_System tabs SHALL use inline styles consistent with the design system: dark background, monospace font at 0.7rem, uppercase text, 0.08em letter spacing
2. THE active tab SHALL have a bottom border of 2px solid using the panel accent color (`#F59E0B`) and brighter text color (`#F59E0B`)
3. THE inactive tab SHALL have muted text color (`#64748B`) and no bottom border highlight
4. WHEN the user hovers over an inactive tab, THE tab SHALL display a subtle background color change to indicate interactivity
5. THE Tab_System tabs SHALL use `role="tab"`, `aria-selected`, and `role="tabpanel"` attributes for screen reader accessibility
6. THE Tab_System tabs SHALL be keyboard navigable using Tab and Enter keys

View File

@@ -0,0 +1,163 @@
# Implementation Plan: Atlas Metrics Report
## Overview
Add a tab system to the Metric Graphs panel on the ReportingPage, with an "Ivanti Findings" tab (existing donuts) and an "Atlas Coverage" tab (three new donut charts). A new `GET /api/atlas/metrics` endpoint aggregates cached Atlas action plan data into chart-ready metrics. All backend changes stay within `backend/routes/atlas.js`. Frontend changes are in `frontend/src/components/pages/ReportingPage.js`.
## Tasks
- [x] 1. Implement the Atlas metrics aggregation endpoint
- [x] 1.1 Add `GET /metrics` route inside the existing `createAtlasRouter` factory function in `backend/routes/atlas.js`
- Query all rows from `atlas_action_plans_cache` using the existing `dbAll` helper: `SELECT has_action_plan, plans_json FROM atlas_action_plans_cache`
- Extract the aggregation logic into a pure function `aggregateAtlasMetrics(rows)` that takes an array of `{ has_action_plan, plans_json }` objects and returns `{ totalHosts, hostsWithPlans, hostsWithoutPlans, plansByType, plansByStatus, totalPlans }`
- For each row: count hosts with/without plans based on `has_action_plan`; parse `plans_json` and count plans by `plan_type` and `status`; skip plan details for rows with invalid JSON
- Return 503 if Atlas is not configured; return 500 on DB errors; require authentication via `requireAuth(db)`
- Return all-zero metrics with empty objects when the cache table is empty
- Do NOT modify `server.js` — the route is added inside the existing router factory
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5, 1.6_
- [x] 1.2 Write property test for metrics aggregation (Property 1)
- **Property 1: Metrics aggregation correctness**
- Extract `aggregateAtlasMetrics` as a pure exported function for testability
- Use fast-check to generate arrays of objects with `has_action_plan` (0 or 1) and `plans_json` (valid JSON arrays of `{ plan_type, status }` objects or invalid strings)
- Verify: `totalHosts === rows.length`, `hostsWithPlans + hostsWithoutPlans === totalHosts`, `hostsWithPlans` equals count of rows where `has_action_plan === 1`, `totalPlans` equals sum of valid plan array lengths, `plansByType` and `plansByStatus` counts match individual plan fields, rows with invalid JSON are counted in host totals but excluded from plan counts
- **Validates: Requirements 1.3, 1.4, 1.5**
- [ ]* 1.3 Write unit tests for the metrics endpoint
- Test empty cache returns all-zero metrics
- Test correct host counting with seeded data
- Test correct plansByType and plansByStatus aggregation
- Test rows with invalid `plans_json` are handled gracefully
- Test 503 response when Atlas is not configured
- Test 401 response for unauthenticated requests
- _Requirements: 1.1, 1.2, 1.3, 1.4, 1.5_
- [x] 2. Checkpoint — Verify backend endpoint
- Ensure all tests pass, ask the user if questions arise.
- [x] 3. Add tab system to the Metric Graphs panel
- [x] 3.1 Add tab state and tab bar UI to the Metric Graphs panel header in `ReportingPage.js`
- Add `metricsTab` state initialized to `'ivanti'`
- Render a horizontal tab bar to the right of the "Metric Graphs" title with two tabs: "Ivanti Findings" and "Atlas Coverage"
- Active tab styling: `color: #F59E0B`, `borderBottom: 2px solid #F59E0B`
- Inactive tab styling: `color: #64748B`, no bottom border
- Hover on inactive: `background: rgba(245, 158, 11, 0.06)`
- Font: `'JetBrains Mono', monospace`, `0.7rem`, `uppercase`, `letterSpacing: 0.08em`
- Add `role="tab"`, `aria-selected` attributes on tabs; `role="tabpanel"` on content area
- Tabs navigable via Tab and Enter keys
- _Requirements: 2.1, 2.2, 2.3, 2.6, 2.7, 2.8, 8.1, 8.2, 8.3, 8.4, 8.5, 8.6_
- [x] 3.2 Conditionally render Ivanti donuts vs Atlas content based on active tab
- When `metricsTab === 'ivanti'`: show existing four donut charts in their current layout (unchanged)
- When `metricsTab === 'atlas'`: show placeholder for Atlas donut charts (to be implemented in task 5)
- _Requirements: 2.4, 2.5_
- [x] 3.3 Conditionally render IvantiCountsChart based on active tab
- Show `IvantiCountsChart` only when `metricsTab === 'ivanti'`
- Hide it when `metricsTab === 'atlas'`
- _Requirements: 7.1, 7.2, 7.3_
- [x] 4. Add Atlas metrics data fetching
- [x] 4.1 Add Atlas metrics state and fetch function to `ReportingPage.js`
- Add state: `atlasMetrics` (null), `atlasMetricsLoading` (false), `atlasMetricsError` (null)
- Add `fetchAtlasMetrics` callback that calls `GET /api/atlas/metrics` with `credentials: 'include'`
- On success: store data in `atlasMetrics`; on error: store message in `atlasMetricsError`
- Set loading state during fetch
- _Requirements: 6.1, 6.3, 6.4_
- [x] 4.2 Call `fetchAtlasMetrics` on mount and after successful Atlas sync
- Add `fetchAtlasMetrics()` call in the existing mount `useEffect`
- After a successful Atlas sync (existing sync handler), call `fetchAtlasMetrics()` to refresh
- Do NOT re-fetch on tab switch
- _Requirements: 6.1, 6.2, 6.5_
- [x] 5. Implement Atlas donut chart components
- [x] 5.1 Implement `AtlasCoverageDonut` component in `ReportingPage.js`
- Props: `{ hostsWithPlans, hostsWithoutPlans, totalHosts }`
- Segments: emerald (`#10B981`) for with plans, amber (`#F59E0B`) for without plans
- Center text: `totalHosts` count, "HOSTS" label
- Legend: count and percentage for each segment
- Empty state: "No data — run Atlas Sync" when `totalHosts === 0`
- Reuse existing `polarToCartesian` and `donutArcPath` helpers; same dimensions (180px, 72px outer, 48px inner)
- Follow the same SVG and styling pattern as `StatusDonut`
- _Requirements: 3.1, 3.2, 3.3, 3.4, 3.5, 3.6_
- [x] 5.2 Implement `AtlasPlanTypeDonut` component in `ReportingPage.js`
- Props: `{ plansByType, totalPlans }`
- Color map: `decommission: #EF4444`, `remediation: #0EA5E9`, `false_positive: #A855F7`, `risk_acceptance: #F59E0B`, `scan_exclusion: #64748B`
- Center text: `totalPlans` count, "PLANS" label
- Legend: only show types with count > 0, with label, count, and percentage
- Empty state: "No plans — run Atlas Sync" when `totalPlans === 0`
- Same SVG dimensions and styling pattern
- _Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6_
- [x] 5.3 Implement `AtlasPlanStatusDonut` component in `ReportingPage.js`
- Props: `{ plansByStatus, totalPlans }`
- Color map: `active: #10B981`, `expired: #EF4444`, `completed: #0EA5E9`, fallback: `#64748B`
- Extract a `getStatusColor(status)` helper function for color assignment
- Center text: `totalPlans` count, "STATUS" label
- Legend: only show statuses with count > 0, with label, count, and percentage
- Empty state: "No plans — run Atlas Sync" when `totalPlans === 0`
- Same SVG dimensions and styling pattern
- _Requirements: 5.1, 5.2, 5.3, 5.4, 5.5, 5.6_
- [x] 5.4 Write property test for Coverage donut data correctness (Property 2)
- **Property 2: Coverage donut data correctness**
- Use fast-check to generate random `(hostsWithPlans, hostsWithoutPlans)` pairs of non-negative integers where at least one > 0
- Render `AtlasCoverageDonut`, verify center text equals `totalHosts`, legend percentages equal `(count / totalHosts) * 100`
- **Validates: Requirements 3.3, 3.4**
- [x] 5.5 Write property test for Plan type donut data correctness (Property 3)
- **Property 3: Plan type donut data correctness**
- Use fast-check to generate random `plansByType` objects with 15 plan type keys mapped to positive integers
- Render `AtlasPlanTypeDonut`, verify center text equals sum of counts, legend entries match input, percentages are correct
- **Validates: Requirements 4.3, 4.4**
- [x] 5.6 Write property test for Plan status donut data correctness (Property 4)
- **Property 4: Plan status donut data correctness**
- Use fast-check to generate random `plansByStatus` objects with 14 status keys mapped to positive integers
- Render `AtlasPlanStatusDonut`, verify center text equals sum of counts, legend entries match input, percentages are correct
- **Validates: Requirements 5.3, 5.4**
- [x] 5.7 Write property test for Plan status color assignment (Property 5)
- **Property 5: Plan status color assignment**
- Use fast-check to generate random strings (mix of known statuses and arbitrary strings)
- Verify `getStatusColor` returns `#10B981` for "active", `#EF4444` for "expired", `#0EA5E9` for "completed", `#64748B` for any other string
- **Validates: Requirements 5.2**
- [~]* 5.8 Write unit tests for Atlas donut components
- Test Coverage donut empty state message when totalHosts is 0
- Test Plan type donut empty state message when totalPlans is 0
- Test Plan status donut empty state message when totalPlans is 0
- Test SVG dimensions are 180px with correct outer/inner radius
- Test color assignments for each plan type and status
- _Requirements: 3.5, 4.5, 5.5, 3.6, 4.6, 5.6_
- [x] 6. Wire Atlas donuts into the Atlas Coverage tab
- [x] 6.1 Render Atlas donut charts in the Atlas Coverage tab content area
- When `metricsTab === 'atlas'`: render `AtlasCoverageDonut`, `AtlasPlanTypeDonut`, `AtlasPlanStatusDonut` in a horizontal flex row with dividers (same layout pattern as Ivanti donuts)
- Pass data from `atlasMetrics` state to each donut component
- Show loading indicator while `atlasMetricsLoading` is true
- Show error message when `atlasMetricsError` is set
- Add chart labels above each donut: "Host Coverage", "Plan Types", "Plan Status" — matching existing label style
- _Requirements: 2.5, 3.1, 4.1, 5.1, 6.3, 6.4_
- [ ]* 6.2 Write integration tests for the full metrics flow
- Test: fetch metrics on mount populates Atlas donut charts
- Test: tab switch does not trigger re-fetch
- Test: loading state shown during fetch
- Test: error state shown on fetch failure
- _Requirements: 6.1, 6.2, 6.3, 6.4, 6.5_
- [x] 7. 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
- All backend changes are confined to `backend/routes/atlas.js` — server.js is NOT modified
- The `aggregateAtlasMetrics` function is extracted as a pure function for testability and property-based testing
- Property tests use fast-check with `{ numRuns: 100 }` minimum
- Checkpoints ensure incremental validation after backend and full integration
- The existing donut chart pattern (StatusDonut, ActionCoverageDonut, FPWorkflowDonut) serves as the template for all three Atlas donut components