11 KiB
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
-
1. Implement the Atlas metrics aggregation endpoint
-
1.1 Add
GET /metricsroute inside the existingcreateAtlasRouterfactory function inbackend/routes/atlas.js- Query all rows from
atlas_action_plans_cacheusing the existingdbAllhelper: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; parseplans_jsonand count plans byplan_typeandstatus; 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
- Query all rows from
-
1.2 Write property test for metrics aggregation (Property 1)
- Property 1: Metrics aggregation correctness
- Extract
aggregateAtlasMetricsas a pure exported function for testability - Use fast-check to generate arrays of objects with
has_action_plan(0 or 1) andplans_json(valid JSON arrays of{ plan_type, status }objects or invalid strings) - Verify:
totalHosts === rows.length,hostsWithPlans + hostsWithoutPlans === totalHosts,hostsWithPlansequals count of rows wherehas_action_plan === 1,totalPlansequals sum of valid plan array lengths,plansByTypeandplansByStatuscounts 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_jsonare 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
-
-
2. Checkpoint — Verify backend endpoint
- Ensure all tests pass, ask the user if questions arise.
-
3. Add tab system to the Metric Graphs panel
-
3.1 Add tab state and tab bar UI to the Metric Graphs panel header in
ReportingPage.js- Add
metricsTabstate 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-selectedattributes 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
- Add
-
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
- When
-
3.3 Conditionally render IvantiCountsChart based on active tab
- Show
IvantiCountsChartonly whenmetricsTab === 'ivanti' - Hide it when
metricsTab === 'atlas' - Requirements: 7.1, 7.2, 7.3
- Show
-
-
4. Add Atlas metrics data fetching
-
4.1 Add Atlas metrics state and fetch function to
ReportingPage.js- Add state:
atlasMetrics(null),atlasMetricsLoading(false),atlasMetricsError(null) - Add
fetchAtlasMetricscallback that callsGET /api/atlas/metricswithcredentials: 'include' - On success: store data in
atlasMetrics; on error: store message inatlasMetricsError - Set loading state during fetch
- Requirements: 6.1, 6.3, 6.4
- Add state:
-
4.2 Call
fetchAtlasMetricson mount and after successful Atlas sync- Add
fetchAtlasMetrics()call in the existing mountuseEffect - 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
- Add
-
-
5. Implement Atlas donut chart components
-
5.1 Implement
AtlasCoverageDonutcomponent inReportingPage.js- Props:
{ hostsWithPlans, hostsWithoutPlans, totalHosts } - Segments: emerald (
#10B981) for with plans, amber (#F59E0B) for without plans - Center text:
totalHostscount, "HOSTS" label - Legend: count and percentage for each segment
- Empty state: "No data — run Atlas Sync" when
totalHosts === 0 - Reuse existing
polarToCartesiananddonutArcPathhelpers; 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
- Props:
-
5.2 Implement
AtlasPlanTypeDonutcomponent inReportingPage.js- 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 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
- Props:
-
5.3 Implement
AtlasPlanStatusDonutcomponent inReportingPage.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:
totalPlanscount, "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
- Props:
-
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 equalstotalHosts, legend percentages equal(count / totalHosts) * 100 - Validates: Requirements 3.3, 3.4
-
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
plansByTypeobjects with 1–5 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
-
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
plansByStatusobjects with 1–4 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
-
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
getStatusColorreturns#10B981for "active",#EF4444for "expired",#0EA5E9for "completed",#64748Bfor 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
-
-
6. Wire Atlas donuts into the Atlas Coverage tab
-
6.1 Render Atlas donut charts in the Atlas Coverage tab content area
- When
metricsTab === 'atlas': renderAtlasCoverageDonut,AtlasPlanTypeDonut,AtlasPlanStatusDonutin a horizontal flex row with dividers (same layout pattern as Ivanti donuts) - Pass data from
atlasMetricsstate to each donut component - Show loading indicator while
atlasMetricsLoadingis true - Show error message when
atlasMetricsErroris 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
- When
-
* 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
-
-
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
aggregateAtlasMetricsfunction 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