164 lines
11 KiB
Markdown
164 lines
11 KiB
Markdown
|
|
# 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 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**
|
|||
|
|
|
|||
|
|
- [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 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**
|
|||
|
|
|
|||
|
|
- [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
|