|
@@ -8,6 +8,7 @@ import { get, post } from "../../lib/api";
|
|
|
export default function ApiHealth() {
|
|
export default function ApiHealth() {
|
|
|
const [responseTime, setResponseTime] = useState<number | null>(null);
|
|
const [responseTime, setResponseTime] = useState<number | null>(null);
|
|
|
const [showRestartConfirm, setShowRestartConfirm] = useState(false);
|
|
const [showRestartConfirm, setShowRestartConfirm] = useState(false);
|
|
|
|
|
+ const [isRestarting, setIsRestarting] = useState(false);
|
|
|
const queryClient = useQueryClient();
|
|
const queryClient = useQueryClient();
|
|
|
|
|
|
|
|
const { data, isLoading, error } = useQuery({
|
|
const { data, isLoading, error } = useQuery({
|
|
@@ -22,45 +23,57 @@ export default function ApiHealth() {
|
|
|
refetchInterval: 30000, // Check every 30 seconds
|
|
refetchInterval: 30000, // Check every 30 seconds
|
|
|
retry: 3, // Retry failed requests 3 times
|
|
retry: 3, // Retry failed requests 3 times
|
|
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
|
|
retryDelay: (attemptIndex) => Math.min(1000 * 2 ** attemptIndex, 30000), // Exponential backoff
|
|
|
|
|
+ enabled: !isRestarting, // Pause health checks during restart
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
const restartMutation = useMutation({
|
|
const restartMutation = useMutation({
|
|
|
mutationFn: () => post("/restart", {}),
|
|
mutationFn: () => post("/restart", {}),
|
|
|
|
|
+ onMutate: () => {
|
|
|
|
|
+ // Set restarting state and pause all queries
|
|
|
|
|
+ setIsRestarting(true);
|
|
|
|
|
+ queryClient.cancelQueries(); // Cancel all in-flight queries
|
|
|
|
|
+ },
|
|
|
onSuccess: () => {
|
|
onSuccess: () => {
|
|
|
toast.success("API service restarting...");
|
|
toast.success("API service restarting...");
|
|
|
setShowRestartConfirm(false);
|
|
setShowRestartConfirm(false);
|
|
|
- // Invalidate all queries to prevent stale data
|
|
|
|
|
- queryClient.invalidateQueries();
|
|
|
|
|
- // Retry health check after delays to see if service is back
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- queryClient.refetchQueries({ queryKey: ["api", "health"] });
|
|
|
|
|
- }, 3000);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- queryClient.refetchQueries({ queryKey: ["api", "health"] });
|
|
|
|
|
- }, 5000);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- queryClient.refetchQueries({ queryKey: ["api", "health"] });
|
|
|
|
|
- }, 8000);
|
|
|
|
|
|
|
+ // Wait for server to come back, then resume
|
|
|
|
|
+ attemptReconnect();
|
|
|
},
|
|
},
|
|
|
onError: (error: any) => {
|
|
onError: (error: any) => {
|
|
|
// Expected to fail since server is restarting
|
|
// Expected to fail since server is restarting
|
|
|
toast.success("API service restart initiated...");
|
|
toast.success("API service restart initiated...");
|
|
|
setShowRestartConfirm(false);
|
|
setShowRestartConfirm(false);
|
|
|
- // Invalidate all queries to prevent stale data
|
|
|
|
|
- queryClient.invalidateQueries();
|
|
|
|
|
// Still try to reconnect
|
|
// Still try to reconnect
|
|
|
- setTimeout(() => {
|
|
|
|
|
- queryClient.refetchQueries({ queryKey: ["api", "health"] });
|
|
|
|
|
- }, 3000);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- queryClient.refetchQueries({ queryKey: ["api", "health"] });
|
|
|
|
|
- }, 5000);
|
|
|
|
|
- setTimeout(() => {
|
|
|
|
|
- queryClient.refetchQueries({ queryKey: ["api", "health"] });
|
|
|
|
|
- }, 8000);
|
|
|
|
|
|
|
+ attemptReconnect();
|
|
|
},
|
|
},
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
|
|
+ const attemptReconnect = () => {
|
|
|
|
|
+ let attempts = 0;
|
|
|
|
|
+ const maxAttempts = 10;
|
|
|
|
|
+ const checkHealth = async () => {
|
|
|
|
|
+ attempts++;
|
|
|
|
|
+ try {
|
|
|
|
|
+ await get("/health");
|
|
|
|
|
+ // Success! Server is back
|
|
|
|
|
+ setIsRestarting(false);
|
|
|
|
|
+ toast.success("API service reconnected");
|
|
|
|
|
+ queryClient.invalidateQueries(); // Refresh all data
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ if (attempts < maxAttempts) {
|
|
|
|
|
+ // Try again with exponential backoff
|
|
|
|
|
+ setTimeout(checkHealth, Math.min(1000 * Math.pow(1.5, attempts), 10000));
|
|
|
|
|
+ } else {
|
|
|
|
|
+ setIsRestarting(false);
|
|
|
|
|
+ toast.error("Failed to reconnect to API service");
|
|
|
|
|
+ queryClient.invalidateQueries();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ };
|
|
|
|
|
+ // Start checking after 2 seconds
|
|
|
|
|
+ setTimeout(checkHealth, 2000);
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
const isHealthy = data?.status === "healthy";
|
|
const isHealthy = data?.status === "healthy";
|
|
|
|
|
|
|
|
return (
|
|
return (
|
|
@@ -72,7 +85,7 @@ export default function ApiHealth() {
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
<div className="text-2xl font-bold text-white">
|
|
<div className="text-2xl font-bold text-white">
|
|
|
- {isLoading ? (
|
|
|
|
|
|
|
+ {isLoading || isRestarting ? (
|
|
|
<div className="flex justify-center">
|
|
<div className="flex justify-center">
|
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
|
<div className="animate-spin rounded-full h-5 w-5 border-b-2 border-white"></div>
|
|
|
</div>
|
|
</div>
|
|
@@ -86,8 +99,10 @@ export default function ApiHealth() {
|
|
|
</span>
|
|
</span>
|
|
|
)}
|
|
)}
|
|
|
</div>
|
|
</div>
|
|
|
- <div className="text-sm font-medium text-gray-400">API Health</div>
|
|
|
|
|
- {responseTime !== null && (
|
|
|
|
|
|
|
+ <div className="text-sm font-medium text-gray-400">
|
|
|
|
|
+ {isRestarting ? "Restarting..." : "API Health"}
|
|
|
|
|
+ </div>
|
|
|
|
|
+ {responseTime !== null && !isRestarting && (
|
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
<div className="text-xs text-gray-500 mt-1">
|
|
|
{responseTime}ms response
|
|
{responseTime}ms response
|
|
|
</div>
|
|
</div>
|
|
@@ -96,12 +111,12 @@ export default function ApiHealth() {
|
|
|
</div>
|
|
</div>
|
|
|
<button
|
|
<button
|
|
|
onClick={() => setShowRestartConfirm(true)}
|
|
onClick={() => setShowRestartConfirm(true)}
|
|
|
- disabled={restartMutation.isPending}
|
|
|
|
|
|
|
+ disabled={restartMutation.isPending || isRestarting}
|
|
|
className="px-3 py-1 rounded text-xs font-medium transition-colors bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
|
className="px-3 py-1 rounded text-xs font-medium transition-colors bg-blue-600 hover:bg-blue-700 text-white disabled:opacity-50 disabled:cursor-not-allowed flex items-center gap-1"
|
|
|
title="Restart API service"
|
|
title="Restart API service"
|
|
|
>
|
|
>
|
|
|
<ArrowPathIcon
|
|
<ArrowPathIcon
|
|
|
- className={`h-3 w-3 ${restartMutation.isPending ? "animate-spin" : ""}`}
|
|
|
|
|
|
|
+ className={`h-3 w-3 ${restartMutation.isPending || isRestarting ? "animate-spin" : ""}`}
|
|
|
/>
|
|
/>
|
|
|
Restart
|
|
Restart
|
|
|
</button>
|
|
</button>
|