feat(dashboard): auto-compute risk scores + refresh button on Critical Gaps
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled
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:
@@ -42,3 +42,9 @@ export async function getRiskProfile(techniqueId: string): Promise<RiskProfile>
|
||||
const { data } = await client.get<RiskProfile>(`/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;
|
||||
}
|
||||
|
||||
@@ -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 */}
|
||||
<div className="rounded-xl border border-gray-800 bg-gray-900 p-4">
|
||||
<h2 className="mb-3 text-sm font-semibold text-gray-300">
|
||||
Critical Gaps — Top 10 by Risk Priority
|
||||
</h2>
|
||||
<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
|
||||
</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">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
|
||||
Reference in New Issue
Block a user