From 8024f329547d0ebeafe0032749dc070b4019b81e Mon Sep 17 00:00:00 2001 From: kitos Date: Thu, 28 May 2026 15:58:49 +0200 Subject: [PATCH] feat(dashboard): auto-compute risk scores + refresh button on Critical Gaps - 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 --- frontend/src/api/risk.ts | 6 +++ frontend/src/pages/ExecutiveDashboardPage.tsx | 46 ++++++++++++++++--- 2 files changed, 46 insertions(+), 6 deletions(-) diff --git a/frontend/src/api/risk.ts b/frontend/src/api/risk.ts index 0ea5c14..45defda 100644 --- a/frontend/src/api/risk.ts +++ b/frontend/src/api/risk.ts @@ -42,3 +42,9 @@ export async function getRiskProfile(techniqueId: string): Promise const { data } = await client.get(`/risk/profiles/${techniqueId}`); 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; +} diff --git a/frontend/src/pages/ExecutiveDashboardPage.tsx b/frontend/src/pages/ExecutiveDashboardPage.tsx index f712a55..5f67d34 100644 --- a/frontend/src/pages/ExecutiveDashboardPage.tsx +++ b/frontend/src/pages/ExecutiveDashboardPage.tsx @@ -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 { Loader2, @@ -7,6 +8,7 @@ import { TrendingDown, Minus, ArrowRight, + RefreshCw, } from "lucide-react"; import { LineChart, @@ -28,7 +30,7 @@ import { import { getCoverageByTactic } from "../api/metrics"; import { getThreatActors } from "../api/threat-actors"; 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 ──────────────────────────────────────────── @@ -163,11 +165,28 @@ export default function ExecutiveDashboardPage() { queryFn: () => getTechniques(), }); - const { data: riskProfiles } = useQuery({ + const queryClient = useQueryClient(); + + const { data: riskProfiles, isLoading: loadingRisk } = useQuery({ queryKey: ["risk-profiles-exec"], 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; if (isLoading) { @@ -420,9 +439,24 @@ export default function ExecutiveDashboardPage() { {/* Section 6: Critical Gaps */}
-

- Critical Gaps — Top 10 by Risk Priority -

+
+

+ Critical Gaps — Top 10 by Risk Priority +

+ +