diff --git a/apps/client/src/hooks/useServerHealth.ts b/apps/client/src/hooks/useServerHealth.ts new file mode 100644 index 0000000..b57f6db --- /dev/null +++ b/apps/client/src/hooks/useServerHealth.ts @@ -0,0 +1,77 @@ +import { useRef, useEffect } from "react"; +import { useHealthQuery } from "@/generated/graphql"; +import { useToast } from "@/contexts/ToastContext"; + +type ServerHealthStatus = "checking" | "online" | "offline"; + +/** + * Hook for checking server health and providing liveness indicator + * Uses Apollo's generated useHealthQuery hook with minimal wrapper logic + */ +export function useServerHealth() { + const { showError, showSuccess } = useToast(); + const initialCheckRef = useRef(false); + const lastErrorToastRef = useRef(null); + const wasOfflineRef = useRef(false); + + // Use Apollo's generated hook with polling + const healthQuery = useHealthQuery({ + fetchPolicy: "network-only", + errorPolicy: "ignore", + pollInterval: 30000, // Poll every 30 seconds + notifyOnNetworkStatusChange: true, + }); + + // Handle toast notifications only + useEffect(() => { + const { data, error } = healthQuery; + const now = new Date(); + + if (error && !data) { + // Only show error toast if: + // 1. It's not the initial check (to avoid showing error immediately on app load) + // 2. We haven't shown an error toast in the last 60 seconds (to avoid spam) + const shouldShowError = initialCheckRef.current && + (!lastErrorToastRef.current || + (now.getTime() - lastErrorToastRef.current.getTime()) > 60000); + + if (shouldShowError) { + lastErrorToastRef.current = now; + const serverUrl = import.meta.env["VITE_SERVER_URL"] || "http://localhost:3000"; + showError("Server Unreachable", `Cannot connect to ${serverUrl}. Check your connection and try again.`); + } + + wasOfflineRef.current = true; + } else if (data) { + // Show success toast only when recovering from offline state + if (wasOfflineRef.current) { + const serverUrl = import.meta.env["VITE_SERVER_URL"] || "http://localhost:3000"; + showSuccess("Server Connected", `Successfully connected to ${serverUrl}`); + } + + wasOfflineRef.current = false; + initialCheckRef.current = true; + } + }, [healthQuery, showError, showSuccess]); + + // Compute derived state from Apollo's data + const status: ServerHealthStatus = + healthQuery.loading && !healthQuery.data ? "checking" : + healthQuery.error && !healthQuery.data ? "offline" : + "online"; + + return { + // Derived state for convenience + status, + lastChecked: healthQuery.data ? new Date() : null, + isOnline: status === "online", + isOffline: status === "offline", + isChecking: status === "checking", + wasOffline: wasOfflineRef.current, + serverTime: healthQuery.data?.health?.timestamp ?? null, + serverTimezone: healthQuery.data?.health?.timezone ?? null, + serverUptime: healthQuery.data?.health?.uptime ?? null, + // Expose Apollo's native properties for advanced usage + ...healthQuery, + }; +}