feat(dashboard): auto-compute risk scores + refresh button on Critical Gaps
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

- Auto-trigger POST /risk/compute on first load if no profiles exist
- Add "Refresh scores" button next to Critical Gaps header (spins while computing)
- Add computeRiskScores() to frontend/src/api/risk.ts
- After compute, invalidate risk-profiles query so table updates immediately

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
kitos
2026-05-28 15:58:49 +02:00
parent 45b13bccde
commit 8024f32954
2 changed files with 46 additions and 6 deletions

View File

@@ -42,3 +42,9 @@ export async function getRiskProfile(techniqueId: string): Promise<RiskProfile>
const { data } = await client.get<RiskProfile>(`/risk/profiles/${techniqueId}`); const { data } = await client.get<RiskProfile>(`/risk/profiles/${techniqueId}`);
return data; return data;
} }
/** Trigger recomputation of all risk scores. */
export async function computeRiskScores(): Promise<{ computed: number; skipped: number; errors: number; duration_seconds: number }> {
const { data } = await client.post("/risk/compute");
return data;
}

View File

@@ -1,4 +1,5 @@
import { useQuery } from "@tanstack/react-query"; import { useEffect } from "react";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { import {
Loader2, Loader2,
@@ -7,6 +8,7 @@ import {
TrendingDown, TrendingDown,
Minus, Minus,
ArrowRight, ArrowRight,
RefreshCw,
} from "lucide-react"; } from "lucide-react";
import { import {
LineChart, LineChart,
@@ -28,7 +30,7 @@ import {
import { getCoverageByTactic } from "../api/metrics"; import { getCoverageByTactic } from "../api/metrics";
import { getThreatActors } from "../api/threat-actors"; import { getThreatActors } from "../api/threat-actors";
import { getTechniques, type TechniqueSummary } from "../api/techniques"; import { getTechniques, type TechniqueSummary } from "../api/techniques";
import { getRiskProfiles, type RiskProfile } from "../api/risk"; import { getRiskProfiles, computeRiskScores, type RiskProfile } from "../api/risk";
// ── Score Gauge Component ──────────────────────────────────────────── // ── Score Gauge Component ────────────────────────────────────────────
@@ -163,11 +165,28 @@ export default function ExecutiveDashboardPage() {
queryFn: () => getTechniques(), queryFn: () => getTechniques(),
}); });
const { data: riskProfiles } = useQuery({ const queryClient = useQueryClient();
const { data: riskProfiles, isLoading: loadingRisk } = useQuery({
queryKey: ["risk-profiles-exec"], queryKey: ["risk-profiles-exec"],
queryFn: () => getRiskProfiles({ limit: 500 }), queryFn: () => getRiskProfiles({ limit: 500 }),
}); });
const computeMutation = useMutation({
mutationFn: computeRiskScores,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["risk-profiles-exec"] });
},
});
// Auto-compute on first load if no profiles exist
useEffect(() => {
if (!loadingRisk && riskProfiles !== undefined && riskProfiles.length === 0) {
computeMutation.mutate();
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [loadingRisk, riskProfiles?.length]);
const isLoading = loadingScore || loadingMetrics; const isLoading = loadingScore || loadingMetrics;
if (isLoading) { if (isLoading) {
@@ -420,9 +439,24 @@ export default function ExecutiveDashboardPage() {
{/* Section 6: Critical Gaps */} {/* Section 6: Critical Gaps */}
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4"> <div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
<h2 className="mb-3 text-sm font-semibold text-gray-300"> <div className="mb-3 flex items-center justify-between">
<h2 className="text-sm font-semibold text-gray-300">
Critical Gaps Top 10 by Risk Priority Critical Gaps Top 10 by Risk Priority
</h2> </h2>
<button
onClick={() => computeMutation.mutate()}
disabled={computeMutation.isPending}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-2.5 py-1 text-xs text-gray-400 hover:text-cyan-400 hover:border-cyan-500/30 disabled:opacity-50 transition-colors"
title="Recompute risk scores"
>
{computeMutation.isPending ? (
<Loader2 className="h-3 w-3 animate-spin" />
) : (
<RefreshCw className="h-3 w-3" />
)}
{computeMutation.isPending ? "Computing…" : "Refresh scores"}
</button>
</div>
<div className="overflow-x-auto"> <div className="overflow-x-auto">
<table className="w-full text-sm"> <table className="w-full text-sm">
<thead> <thead>