fix(dashboard): force refetch on mount + refresh button for metric widgets
Some checks failed
Aegis CI / lint-and-test (push) Has been cancelled

Root cause: after backend restart (502 errors on startup), metric queries
(pipeline, team, recent, validation) get cached in error state. When the
user stays on the dashboard, the component never remounts so queries don't
auto-retry.

Fixes:
1. refetchOnMount:'always' — queries ALWAYS refetch when component mounts,
   even if cached with error/stale data. Prevents stuck empty state.
2. gcTime:0 — error state is not cached; next mount starts a fresh query.
3. retry:3 — more retries before giving up (covers slow startup windows).
4. Refresh button in header — manually invalidates and refetches all 4
   metric queries with a single click. Spinner icon during refetch.
This commit is contained in:
kitos
2026-06-02 09:48:59 +02:00
parent 0d4c105aa3
commit 646ac7146e

View File

@@ -1,4 +1,4 @@
import { useQuery } from "@tanstack/react-query";
import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useNavigate } from "react-router-dom";
import {
Shield,
@@ -15,6 +15,7 @@ import {
Users,
TrendingUp,
ArrowRight,
RefreshCw,
} from "lucide-react";
import {
LineChart,
@@ -87,29 +88,48 @@ export default function DashboardPage() {
queryFn: getCoverageByTactic,
});
// V2 queries — retry:2 so transient failures don't leave widgets blank
const { data: pipeline, isLoading: pipelineLoading, isError: pipelineError } = useQuery({
const queryClient = useQueryClient();
// Refresh all V2 metric widgets manually
const refreshMetrics = () => {
queryClient.invalidateQueries({ queryKey: ["metrics", "test-pipeline"] });
queryClient.invalidateQueries({ queryKey: ["metrics", "team-activity"] });
queryClient.invalidateQueries({ queryKey: ["metrics", "validation-rate"] });
queryClient.invalidateQueries({ queryKey: ["metrics", "recent-tests"] });
};
// V2 queries — retry:3 + refetchOnMount:'always' so queries re-run even
// when cached in error state (happens if backend was still starting on first load)
const { data: pipeline, isLoading: pipelineLoading, isError: pipelineError, isFetching: pipelineFetching } = useQuery({
queryKey: ["metrics", "test-pipeline"],
queryFn: getTestPipeline,
retry: 2,
retry: 3,
refetchOnMount: "always",
gcTime: 0,
});
const { data: teamActivity, isLoading: teamLoading, isError: teamError } = useQuery({
queryKey: ["metrics", "team-activity"],
queryFn: getTeamActivity,
retry: 2,
retry: 3,
refetchOnMount: "always",
gcTime: 0,
});
const { data: validationRates, isLoading: validationLoading, isError: validationError } = useQuery({
queryKey: ["metrics", "validation-rate"],
queryFn: getValidationRate,
retry: 2,
retry: 3,
refetchOnMount: "always",
gcTime: 0,
});
const { data: recentTests, isLoading: recentLoading, isError: recentError } = useQuery({
queryKey: ["metrics", "recent-tests"],
queryFn: getRecentTests,
retry: 2,
retry: 3,
refetchOnMount: "always",
gcTime: 0,
});
const { data: coverageEvolution, isLoading: evolutionLoading } = useQuery({
@@ -147,7 +167,18 @@ export default function DashboardPage() {
MITRE ATT&CK coverage overview
</p>
</div>
{summary && (
<div className="flex items-center gap-3">
{/* Manual refresh for metric widgets */}
<button
onClick={refreshMetrics}
disabled={pipelineLoading || pipelineFetching}
className="flex items-center gap-1.5 rounded-lg border border-gray-700 bg-gray-800 px-3 py-2 text-xs text-gray-400 hover:bg-gray-700 hover:text-white transition-colors disabled:opacity-50"
title="Refresh dashboard metrics"
>
<RefreshCw className={`h-3.5 w-3.5 ${pipelineFetching ? "animate-spin" : ""}`} />
Refresh
</button>
{summary && (
<div className="flex items-center gap-2 rounded-lg border border-cyan-500/30 bg-cyan-500/10 px-4 py-2">
<Percent className="h-5 w-5 text-cyan-400" />
<span className="text-lg font-bold text-cyan-400">
@@ -155,7 +186,8 @@ export default function DashboardPage() {
</span>
<span className="text-sm text-gray-400">Coverage</span>
</div>
)}
)}
</div>
</div>
{/* Summary Cards */}