Files
cve-dashboard/.kiro/specs/compliance-metric-grouping/tasks.md

12 KiB

Implementation Plan: Compliance Metric Grouping

Overview

Consolidate the AEO Compliance page's metric health cards from one-per-summary-entry to one-per-metric-family. Add variant pills inside each grouped card, a hover tooltip with metric context (300ms delay), an info panel for full metric definitions, and a static metricDefinitions.json data file. All work is frontend-only — no backend changes needed. The metricFilter state changes from string|null to string[]|null to support filtering by all metric IDs in a family.

Tasks

  • 1. Create metric definitions data file and install test dependencies

    • 1.1 Create frontend/src/data/metricDefinitions.json

      • Create the frontend/src/data/ directory
      • Build the JSON array from the metric definitions table provided by the user (130+ rows, 14 fields each)
      • Each entry must have all 14 keys: metric_id, metric_title, asset_types, asset_types_in_scope, application_types_in_scope, environment_in_scope, status_in_scope, instance_types_in_scope, criticality_levels_in_scope, exclusions, special_conditions, data_sources_required, business_justification, notes
      • Use empty strings for optional fields with no value — never null or omitted keys
      • Verify the file imports without error via a quick JSON.parse check
      • Requirements: 6.1, 6.2, 6.3, 6.5, 8.3, 8.4
    • 1.2 Install fast-check as a dev dependency

      • Run npm install --save-dev fast-check in frontend/
      • Verify it appears in package.json devDependencies
      • Requirements: (testing infrastructure)
  • 2. Implement pure helper functions and their tests

    • 2.1 Add computeWorstStatus and groupByMetricFamily helpers to CompliancePage.js

      • Add STATUS_SEVERITY map: { 'Below 15% of Target': 0, 'Within 15% of Target': 1, 'Meets/Exceeds Target': 2 }
      • Implement computeWorstStatus(statuses) — returns the status with the lowest severity rank from a non-empty array
      • Implement groupByMetricFamily(allEntries, team) — filters entries by team, groups by metric_id, returns array of { metricId, entries, category, target, worstStatus } objects
      • Export both functions for testing (named exports alongside the default CompliancePage export)
      • Remove the existing teamMetrics() helper (replaced by groupByMetricFamily)
      • Requirements: 1.1, 1.2, 1.6, 3.1
    • 2.2 Write property test: Grouping invariant — no entries lost or misplaced

      • Property 1: Grouping invariant — no entries lost or misplaced
      • Create test file frontend/src/components/pages/__tests__/complianceGrouping.property.test.js
      • Generate random arrays of summary entry objects with varying metric_id and team values
      • Verify: (a) every entry appears in exactly one group, (b) all entries within a group share the same metric_id, (c) total entries across groups equals team-filtered input count
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 1.1, 1.2
    • 2.3 Write property test: Worst-status computation follows severity ordering

      • Property 2: Worst-status computation follows severity ordering
      • Generate random non-empty arrays of status strings from {"Below 15% of Target", "Within 15% of Target", "Meets/Exceeds Target"}
      • Verify the result is the status with the lowest severity rank present in the array
      • If the array contains "Below 15% of Target", the result must be "Below 15% of Target"
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 1.6, 3.1
    • * 2.4 Write property test: Device filtering with metric family includes all matching devices

      • Property 3: Device filtering with metric family includes all matching devices
      • Generate random device arrays (each with a failing_metrics array of { metric_id } objects) and random filter ID arrays
      • Verify the filtered result contains exactly those devices with at least one matching metric_id in the filter array
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 1.8, 7.1, 7.2
  • 3. Checkpoint — Verify helpers and property tests

    • Ensure all tests pass, ask the user if questions arise.
  • 4. Redesign MetricHealthCard with variant pills and worst-status coloring

    • 4.1 Redesign MetricHealthCard to accept a family group

      • Change props from { entry, active, onClick } to { family, active, onClick, onInfoClick, definitionLookup }
      • family is { metricId, entries, category, target, worstStatus }
      • Display base metricId as card title and category from the family
      • Display shared target percentage
      • Use worstStatus color for card border, status pill text, and status dot
      • When all variants meet/exceed target, show "OK" status indicator with success color
      • Add Info icon (lucide-react) in the top-right corner with stopPropagation on click to call onInfoClick(family.metricId)
      • Requirements: 1.2, 1.3, 1.6, 1.7, 3.1, 3.2, 3.3, 5.1, 5.5
    • 4.2 Implement VariantPill inline sub-component

      • Render one pill per entry in family.entries
      • Each pill shows the entry's description or team label and compliance percentage in monospace
      • Background tint from the entry's individual status color at ~12% opacity
      • Show a glow dot when the variant's status is not "Meets/Exceeds Target"
      • Layout: inline-flex with flexWrap: 'wrap' on the parent container
      • Requirements: 1.4, 1.5, 2.1, 2.2, 2.3, 2.4
  • 5. Update CompliancePage state and rendering to use grouped families

    • 5.1 Add new imports and build the definitions lookup map

      • Import { Info } from lucide-react
      • Import MetricInfoPanel from ./MetricInfoPanel (created in task 6)
      • Import metricDefinitionsRaw from ../../data/metricDefinitions.json
      • Build METRIC_DEFINITIONS lookup object at module level keyed by metric_id
      • Requirements: 6.3, 6.4
    • 5.2 Update state management and filter logic

      • Change metricFilter from string|null to string[]|null
      • Add new state: infoMetric (string|null) — which metric's info panel is open
      • Add new state: hoveredMetric (string|null) — which metric is being hovered
      • Add new ref: hoverTimeoutRef — for 300ms delay management
      • Update filteredDevices filter from m.metric_id === metricFilter to metricFilter.includes(m.metric_id)
      • Requirements: 7.1, 7.2
    • 5.3 Update card rendering to use groupByMetricFamily

      • Replace const metrics = teamMetrics(summary.entries, activeTeam) with const families = groupByMetricFamily(summary.entries, activeTeam)
      • Replace metrics.map(entry => <MetricHealthCard entry={entry} ... />) with families.map(family => <MetricHealthCard family={family} ... />)
      • On card click: set metricFilter to family.entries.map(e => e.metric_id) (array of all IDs in the family), or clear if already active
      • Active state check: compare metricFilter array contents against the family's metric IDs
      • Pass onInfoClick handler that sets infoMetric state
      • Pass definitionLookup as METRIC_DEFINITIONS
      • Requirements: 1.2, 1.8, 7.1, 7.3, 7.4, 7.5
  • 6. Implement MetricInfoPanel component

    • 6.1 Create frontend/src/components/pages/MetricInfoPanel.js
      • Props: metricId, definition (from lookup or null), summaryEntries (family entries for fallback), onClose
      • Render overlay/slide-out panel with dark theme matching DESIGN_SYSTEM.md
      • Close button (X icon, top-right)
      • Metric title in h3 monospace
      • Sections with monospace uppercase labels: Asset Types, Asset Types In Scope, Application Types In Scope, Environment In Scope, Status In Scope, Instance Types In Scope, Criticality Levels In Scope, Exclusions, Special Conditions, Data Sources Required, Business Justification, Notes
      • Show "—" placeholder for empty string fields
      • If definition is null: show "No detailed definition available" with summary description fallback
      • Click outside or close button calls onClose()
      • Requirements: 5.2, 5.3, 5.4, 5.6, 5.7
  • 7. Implement HoverTooltip inline in CompliancePage

    • 7.1 Add hover tooltip logic and rendering
      • On mouseEnter on MetricHealthCard: set 300ms timeout, then set hoveredMetric to family.metricId
      • On mouseLeave: clear timeout, set hoveredMetric to null
      • Render tooltip when hoveredMetric matches a family — positioned near the card using getBoundingClientRect()
      • Tooltip content: metric title, business justification, data sources required (from METRIC_DEFINITIONS lookup)
      • Fall back to summary entry description when no definition exists
      • Dark card background with subtle border and shadow per DESIGN_SYSTEM.md
      • Tooltip must not interfere with card click behavior
      • Requirements: 4.1, 4.2, 4.3, 4.4, 4.5, 4.6
  • 8. Checkpoint — Verify full UI integration

    • Ensure all tests pass, ask the user if questions arise.
  • 9. Property tests for definitions data and lookup

    • 9.1 Write property test: Definition lookup returns correct entry or null

      • Property 4: Definition lookup returns correct entry or null
      • Create test file frontend/src/components/pages/__tests__/metricDefinitions.property.test.js
      • Generate random arrays of metric definition objects with unique metric_id values
      • Build lookup map, query with IDs from the array (expect hit) and IDs not in the array (expect miss)
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 4.2, 4.6
    • 9.2 Write property test: Detail panel renders all required definition fields

      • Property 5: Detail panel renders all required definition fields
      • Generate random metric definition objects with all 14 fields
      • Extract the set of field keys that the MetricInfoPanel renders and verify all required keys are present
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 5.3
    • 9.3 Write property test: Definitions schema validation — all entries have required fields

      • Property 6: Definitions schema validation — all entries have required fields
      • Generate random arrays of metric definition objects
      • Verify every entry has all 14 keys present, metric_id is a non-empty string, and optional fields (exclusions, special_conditions, notes) are strings (not null/undefined)
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 6.2, 8.3, 8.4
    • 9.4 Write property test: Lookup map construction preserves all definitions

      • Property 7: Lookup map construction preserves all definitions
      • Generate random definition arrays with unique metric_id values
      • Build lookup map and verify map size equals array length, and every definition is retrievable by its metric_id
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 6.4
    • 9.5 Write property test: JSON round-trip preserves metric definition data

      • Property 8: JSON round-trip preserves metric definition data
      • Generate random metric definition objects with string values for all fields
      • Round-trip through JSON.stringify then JSON.parse and verify deep equality
      • Use fc.assert(property, { numRuns: 100 })
      • Validates: Requirements 8.1, 8.2
  • 10. 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
  • Checkpoints ensure incremental validation
  • Property tests validate universal correctness properties from the design document using fast-check
  • Unit tests validate specific examples and edge cases
  • All styling follows the project convention of inline styles (no CSS modules or Tailwind)
  • The fast-check library must be installed as a dev dependency before running property tests
  • The metricDefinitions.json file contains 130+ rows — the user will provide the metric definitions table data for conversion
  • computeWorstStatus and groupByMetricFamily are exported as named exports from CompliancePage.js for testability