From 646ac7146e790db41f21fa661eae134715bd261c Mon Sep 17 00:00:00 2001
From: kitos
Date: Tue, 2 Jun 2026 09:48:59 +0200
Subject: [PATCH] fix(dashboard): force refetch on mount + refresh button for
metric widgets
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
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.
---
frontend/src/pages/DashboardPage.tsx | 50 +++++++++++++++++++++++-----
1 file changed, 41 insertions(+), 9 deletions(-)
diff --git a/frontend/src/pages/DashboardPage.tsx b/frontend/src/pages/DashboardPage.tsx
index a90bc08..b982bc9 100644
--- a/frontend/src/pages/DashboardPage.tsx
+++ b/frontend/src/pages/DashboardPage.tsx
@@ -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
- {summary && (
+
+ {/* Manual refresh for metric widgets */}
+
+ {summary && (
@@ -155,7 +186,8 @@ export default function DashboardPage() {
Coverage
- )}
+ )}
+
{/* Summary Cards */}