18 KiB
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_jsonto 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
polarToCartesiananddonutArcPathSVG 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 inbackend/routes/atlas.js. The router is already mounted at/api/atlasin 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
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
- ReportingPage mounts → calls
GET /api/atlas/metrics - Backend queries all rows from
atlas_action_plans_cache, includingplans_json - Backend iterates rows, parses each
plans_json, counts plans by type and status - Backend returns aggregated JSON:
{ totalHosts, hostsWithPlans, hostsWithoutPlans, plansByType, plansByStatus, totalPlans } - Frontend stores result in
atlasMetricsstate - When "Atlas Coverage" tab is active, three donut components render from this state
Data Flow: Refresh After Sync
- User clicks Atlas sync button →
POST /api/atlas/sync(existing) - On success, frontend calls
GET /api/atlas/metricsagain - 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:
{
"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:
- Query all rows:
SELECT has_action_plan, plans_json FROM atlas_action_plans_cache - Initialize counters:
totalHosts = rows.length,hostsWithPlans = 0,hostsWithoutPlans = 0,plansByType = {},plansByStatus = {},totalPlans = 0 - For each row:
- If
has_action_plan === 1, incrementhostsWithPlans; else incrementhostsWithoutPlans - 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]andplansByStatus[plan.status]counters, and incrementtotalPlans
- If
- 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"andaria-selected; the content area usesrole="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:
const [metricsTab, setMetricsTab] = useState('ivanti');
const [atlasMetrics, setAtlasMetrics] = useState(null);
const [atlasMetricsLoading, setAtlasMetricsLoading] = useState(false);
const [atlasMetricsError, setAtlasMetricsError] = useState(null);
New fetch function:
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:
totalHostscount, "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:
totalPlanscount, "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:
totalPlanscount, "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:
{
"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:
totalHostsequal to the number of rowshostsWithPlans + hostsWithoutPlansequal tototalHostshostsWithPlansequal to the count of rows wherehas_action_plan === 1totalPlansequal to the sum of plan array lengths across all rows with valid JSON- Each key in
plansByTypemaps to the count of plans with thatplan_typeacross all valid rows - Each key in
plansByStatusmaps to the count of plans with thatstatusacross all valid rows - Rows with invalid
plans_jsonare counted intotalHostsandhostsWithPlans/hostsWithoutPlansbut their plans are excluded fromplansByType,plansByStatus, andtotalPlans
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 — 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:
// 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_cachewith varied data → callGET /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