# 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
Ivanti Findings | Atlas Coverage] ID[Ivanti Donuts
StatusDonut, ActionCoverageDonut,
FPWorkflowDonut x2] AD[Atlas Donuts
CoverageDonut, PlanTypeDonut,
PlanStatusDonut] IC[IvantiCountsChart] end subgraph Backend - Atlas Router ME[GET /api/atlas/metrics] AC[(atlas_action_plans_cache
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 1–5 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 1–4 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