feat(snapshots): evolution API, tactic breakdown and dashboard trend chart [FASE-5.2]
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
This commit is contained in:
@@ -12,10 +12,28 @@ export interface SnapshotSummary {
|
|||||||
not_covered_count: number;
|
not_covered_count: number;
|
||||||
in_progress_count: number;
|
in_progress_count: number;
|
||||||
not_evaluated_count: number;
|
not_evaluated_count: number;
|
||||||
|
coverage_percentage?: number;
|
||||||
|
by_tactic?: Record<string, unknown>;
|
||||||
|
by_status?: Record<string, number>;
|
||||||
|
stale_count?: number;
|
||||||
|
never_tested_count?: number;
|
||||||
created_by: string | null;
|
created_by: string | null;
|
||||||
created_at: string | null;
|
created_at: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface CoverageEvolutionPoint {
|
||||||
|
date: string | null;
|
||||||
|
name: string | null;
|
||||||
|
org_score: number;
|
||||||
|
coverage_pct: number;
|
||||||
|
by_tactic: Record<string, unknown>;
|
||||||
|
by_status: Record<string, number>;
|
||||||
|
stale_count: number;
|
||||||
|
never_tested_count: number;
|
||||||
|
validated_count: number;
|
||||||
|
total_techniques: number;
|
||||||
|
}
|
||||||
|
|
||||||
export interface TechniqueState {
|
export interface TechniqueState {
|
||||||
mitre_id: string;
|
mitre_id: string;
|
||||||
technique_id: string;
|
technique_id: string;
|
||||||
@@ -76,6 +94,17 @@ export async function getSnapshot(snapshotId: string): Promise<SnapshotDetail> {
|
|||||||
return data;
|
return data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Coverage trend points for dashboard charts. */
|
||||||
|
export async function getCoverageEvolution(
|
||||||
|
months = 12,
|
||||||
|
): Promise<CoverageEvolutionPoint[]> {
|
||||||
|
const { data } = await client.get<CoverageEvolutionPoint[]>(
|
||||||
|
"/snapshots/evolution",
|
||||||
|
{ params: { months } },
|
||||||
|
);
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
/** Compare two snapshots. */
|
/** Compare two snapshots. */
|
||||||
export async function compareSnapshots(
|
export async function compareSnapshots(
|
||||||
aId: string,
|
aId: string,
|
||||||
|
|||||||
@@ -16,6 +16,16 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
ArrowRight,
|
ArrowRight,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import {
|
||||||
|
LineChart,
|
||||||
|
Line,
|
||||||
|
XAxis,
|
||||||
|
YAxis,
|
||||||
|
CartesianGrid,
|
||||||
|
Tooltip,
|
||||||
|
ResponsiveContainer,
|
||||||
|
Legend,
|
||||||
|
} from "recharts";
|
||||||
import {
|
import {
|
||||||
getCoverageSummary,
|
getCoverageSummary,
|
||||||
getCoverageByTactic,
|
getCoverageByTactic,
|
||||||
@@ -28,6 +38,7 @@ import {
|
|||||||
type ValidationRateItem,
|
type ValidationRateItem,
|
||||||
type RecentTestItem,
|
type RecentTestItem,
|
||||||
} from "../api/metrics";
|
} from "../api/metrics";
|
||||||
|
import { getCoverageEvolution } from "../api/snapshots";
|
||||||
import CoverageSummaryCard from "../components/CoverageSummaryCard";
|
import CoverageSummaryCard from "../components/CoverageSummaryCard";
|
||||||
import TacticCoverageChart from "../components/TacticCoverageChart";
|
import TacticCoverageChart from "../components/TacticCoverageChart";
|
||||||
import type { TestState } from "../types/models";
|
import type { TestState } from "../types/models";
|
||||||
@@ -97,6 +108,11 @@ export default function DashboardPage() {
|
|||||||
queryFn: getRecentTests,
|
queryFn: getRecentTests,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { data: coverageEvolution, isLoading: evolutionLoading } = useQuery({
|
||||||
|
queryKey: ["snapshots", "evolution", 6],
|
||||||
|
queryFn: () => getCoverageEvolution(6),
|
||||||
|
});
|
||||||
|
|
||||||
if (summaryLoading || tacticsLoading) {
|
if (summaryLoading || tacticsLoading) {
|
||||||
return (
|
return (
|
||||||
<div className="flex h-64 items-center justify-center">
|
<div className="flex h-64 items-center justify-center">
|
||||||
@@ -191,6 +207,67 @@ export default function DashboardPage() {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Coverage evolution (snapshots) */}
|
||||||
|
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||||
|
<h2 className="mb-4 text-lg font-semibold text-white flex items-center gap-2">
|
||||||
|
<TrendingUp className="h-5 w-5 text-cyan-400" />
|
||||||
|
Coverage Evolution (6 months)
|
||||||
|
</h2>
|
||||||
|
{evolutionLoading ? (
|
||||||
|
<div className="flex h-48 items-center justify-center">
|
||||||
|
<Loader2 className="h-6 w-6 animate-spin text-cyan-400" />
|
||||||
|
</div>
|
||||||
|
) : coverageEvolution && coverageEvolution.length > 0 ? (
|
||||||
|
<ResponsiveContainer width="100%" height={240}>
|
||||||
|
<LineChart data={coverageEvolution}>
|
||||||
|
<CartesianGrid strokeDasharray="3 3" stroke="#1f2937" />
|
||||||
|
<XAxis
|
||||||
|
dataKey="date"
|
||||||
|
tick={{ fill: "#6b7280", fontSize: 10 }}
|
||||||
|
tickFormatter={(val) => {
|
||||||
|
const d = new Date(val);
|
||||||
|
return `${d.getMonth() + 1}/${d.getDate()}`;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<YAxis
|
||||||
|
domain={[0, 100]}
|
||||||
|
tick={{ fill: "#6b7280", fontSize: 10 }}
|
||||||
|
/>
|
||||||
|
<Tooltip
|
||||||
|
contentStyle={{
|
||||||
|
backgroundColor: "#111827",
|
||||||
|
border: "1px solid #374151",
|
||||||
|
borderRadius: "8px",
|
||||||
|
fontSize: "12px",
|
||||||
|
}}
|
||||||
|
labelStyle={{ color: "#9ca3af" }}
|
||||||
|
/>
|
||||||
|
<Legend />
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="org_score"
|
||||||
|
stroke="#06b6d4"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3 }}
|
||||||
|
name="Org score"
|
||||||
|
/>
|
||||||
|
<Line
|
||||||
|
type="monotone"
|
||||||
|
dataKey="coverage_pct"
|
||||||
|
stroke="#22c55e"
|
||||||
|
strokeWidth={2}
|
||||||
|
dot={{ r: 3 }}
|
||||||
|
name="Coverage %"
|
||||||
|
/>
|
||||||
|
</LineChart>
|
||||||
|
</ResponsiveContainer>
|
||||||
|
) : (
|
||||||
|
<p className="text-sm text-gray-500 py-8 text-center">
|
||||||
|
No snapshots yet. Weekly snapshots populate this chart automatically.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* ── V2 Section: Test Pipeline ────────────────────────────────── */}
|
{/* ── V2 Section: Test Pipeline ────────────────────────────────── */}
|
||||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
<div className="rounded-xl border border-gray-800 bg-gray-900 p-6">
|
||||||
<div className="mb-4 flex items-center justify-between">
|
<div className="mb-4 flex items-center justify-between">
|
||||||
|
|||||||
Reference in New Issue
Block a user