diff --git a/src/components/CIAnalytics.tsx b/src/components/CIAnalytics.tsx index 73e5559..8f787ad 100644 --- a/src/components/CIAnalytics.tsx +++ b/src/components/CIAnalytics.tsx @@ -16,8 +16,27 @@ export default function CIAnalytics() { const [data, setData] = useState(null); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); + const [rateLimitResetTime, setRateLimitResetTime] = useState(null); + const [isRateLimited, setIsRateLimited] = useState(false); + + useEffect(() => { + if (!rateLimitResetTime) return; + const msUntilReset = rateLimitResetTime.getTime() - Date.now(); + if (msUntilReset <= 0) { + setIsRateLimited(false); + setRateLimitResetTime(null); + return; + } + const timer = setTimeout(() => { + setIsRateLimited(false); + setRateLimitResetTime(null); + setError(null); + }, msUntilReset); + return () => clearTimeout(timer); + }, [rateLimitResetTime]); const fetchCIAnalytics = useCallback(() => { + if (isRateLimited) return; setLoading(true); setError(null); @@ -28,17 +47,39 @@ export default function CIAnalytics() { fetch(`/api/metrics/ci${accountParam}`) .then((res) => { - if (!res.ok) { - throw new Error("API error"); + if (res.status === 403) { + const resetHeader = res.headers.get("X-RateLimit-Reset"); + if (resetHeader) { + const resetDate = new Date(parseInt(resetHeader, 10) * 1000); + const resetTimeStr = resetDate.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + }); + setRateLimitResetTime(resetDate); + setIsRateLimited(true); + throw new Error( + `GitHub API rate limit reached. Resets at ${resetTimeStr}. Try again later.` + ); + } + throw new Error("GitHub API rate limit reached. Please try again later."); } + if (!res.ok) throw new Error("API error"); return res.json(); }) - .then((payload: CIAnalyticsData) => setData(payload)) - .catch(() => - setError("CI data unavailable - ensure Actions are enabled on your repos") - ) + .then((payload: CIAnalyticsData) => { + setData(payload); + setIsRateLimited(false); + setRateLimitResetTime(null); + }) + .catch((err: Error) => { + setError( + err.message.includes("rate limit") + ? err.message + : "CI data unavailable - ensure Actions are enabled on your repos" + ); + }) .finally(() => setLoading(false)); - }, [selectedAccount]); + }, [selectedAccount, isRateLimited]); useEffect(() => { fetchCIAnalytics(); @@ -53,6 +94,15 @@ export default function CIAnalytics() { ] : []; + const refreshLabel = isRateLimited + ? rateLimitResetTime + ? `Retry at ${rateLimitResetTime.toLocaleTimeString([], { + hour: "2-digit", + minute: "2-digit", + })}` + : "Rate limited" + : "Refresh"; + return (
@@ -67,9 +117,11 @@ export default function CIAnalytics() {
@@ -90,15 +142,23 @@ export default function CIAnalytics() { ))}
) : error ? ( -
+

{error}

- + {!isRateLimited && ( + + )}
) : data ? (