From 1249391ef0d3e3651a779f6dd336362054f2d74e Mon Sep 17 00:00:00 2001 From: Kitos Date: Mon, 18 May 2026 15:07:12 +0200 Subject: [PATCH] feat(snapshots): evolution API, tactic breakdown and dashboard trend chart [FASE-5.2] --- frontend/src/api/snapshots.ts | 29 +++++++++++ frontend/src/pages/DashboardPage.tsx | 77 ++++++++++++++++++++++++++++ 2 files changed, 106 insertions(+) diff --git a/frontend/src/api/snapshots.ts b/frontend/src/api/snapshots.ts index ed4a968..618ee9e 100644 --- a/frontend/src/api/snapshots.ts +++ b/frontend/src/api/snapshots.ts @@ -12,10 +12,28 @@ export interface SnapshotSummary { not_covered_count: number; in_progress_count: number; not_evaluated_count: number; + coverage_percentage?: number; + by_tactic?: Record; + by_status?: Record; + stale_count?: number; + never_tested_count?: number; created_by: string | null; created_at: string | null; } +export interface CoverageEvolutionPoint { + date: string | null; + name: string | null; + org_score: number; + coverage_pct: number; + by_tactic: Record; + by_status: Record; + stale_count: number; + never_tested_count: number; + validated_count: number; + total_techniques: number; +} + export interface TechniqueState { mitre_id: string; technique_id: string; @@ -76,6 +94,17 @@ export async function getSnapshot(snapshotId: string): Promise { return data; } +/** Coverage trend points for dashboard charts. */ +export async function getCoverageEvolution( + months = 12, +): Promise { + const { data } = await client.get( + "/snapshots/evolution", + { params: { months } }, + ); + return data; +} + /** Compare two snapshots. */ export async function compareSnapshots( aId: string, diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx index 4bc9ec7..521fc21 100644 --- a/frontend/src/pages/DashboardPage.tsx +++ b/frontend/src/pages/DashboardPage.tsx @@ -16,6 +16,16 @@ import { TrendingUp, ArrowRight, } from "lucide-react"; +import { + LineChart, + Line, + XAxis, + YAxis, + CartesianGrid, + Tooltip, + ResponsiveContainer, + Legend, +} from "recharts"; import { getCoverageSummary, getCoverageByTactic, @@ -28,6 +38,7 @@ import { type ValidationRateItem, type RecentTestItem, } from "../api/metrics"; +import { getCoverageEvolution } from "../api/snapshots"; import CoverageSummaryCard from "../components/CoverageSummaryCard"; import TacticCoverageChart from "../components/TacticCoverageChart"; import type { TestState } from "../types/models"; @@ -97,6 +108,11 @@ export default function DashboardPage() { queryFn: getRecentTests, }); + const { data: coverageEvolution, isLoading: evolutionLoading } = useQuery({ + queryKey: ["snapshots", "evolution", 6], + queryFn: () => getCoverageEvolution(6), + }); + if (summaryLoading || tacticsLoading) { return (
@@ -191,6 +207,67 @@ export default function DashboardPage() {
)} + {/* Coverage evolution (snapshots) */} +
+

+ + Coverage Evolution (6 months) +

+ {evolutionLoading ? ( +
+ +
+ ) : coverageEvolution && coverageEvolution.length > 0 ? ( + + + + { + const d = new Date(val); + return `${d.getMonth() + 1}/${d.getDate()}`; + }} + /> + + + + + + + + ) : ( +

+ No snapshots yet. Weekly snapshots populate this chart automatically. +

+ )} +
+ {/* ── V2 Section: Test Pipeline ────────────────────────────────── */}