"use client"; import { useState, useEffect, useCallback, useRef } from "react"; import ProviderIcon from "@/shared/components/ProviderIcon"; import QuotaTable from "./QuotaTable"; import Toggle from "@/shared/components/Toggle"; import { parseQuotaData, calculatePercentage } from "./utils"; import Card from "@/shared/components/Card"; import { EditConnectionModal } from "@/shared/components"; import { USAGE_SUPPORTED_PROVIDERS, USAGE_APIKEY_PROVIDERS } from "@/shared/constants/providers"; // Connection is eligible for the quota page when it uses OAuth or is an apikey provider whitelisted for quota const isUsageEligible = (conn) => USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && (conn.authType === "oauth" || USAGE_APIKEY_PROVIDERS.includes(conn.provider)); const REFRESH_INTERVAL_MS = 60000; // 60 seconds const DEPLETED_QUOTA_THRESHOLD = 5; // percent const AUTO_REFRESH_STORAGE_KEY = "quotaAutoRefresh"; export default function ProviderLimits() { const [connections, setConnections] = useState([]); const [quotaData, setQuotaData] = useState({}); const [loading, setLoading] = useState({}); const [errors, setErrors] = useState({}); const [autoRefresh, setAutoRefresh] = useState(() => { if (typeof window === "undefined") return true; const stored = window.localStorage.getItem(AUTO_REFRESH_STORAGE_KEY); return stored === null ? true : stored === "true"; }); const [lastUpdated, setLastUpdated] = useState(null); const [refreshingAll, setRefreshingAll] = useState(false); const [countdown, setCountdown] = useState(60); const [connectionsLoading, setConnectionsLoading] = useState(true); const [deletingId, setDeletingId] = useState(null); const [togglingId, setTogglingId] = useState(null); const [showEditModal, setShowEditModal] = useState(false); const [selectedConnection, setSelectedConnection] = useState(null); const [proxyPools, setProxyPools] = useState([]); const [providerFilter, setProviderFilter] = useState("all"); const [expiringFirst, setExpiringFirst] = useState(false); const [providerMenuOpen, setProviderMenuOpen] = useState(false); const [bulkToggling, setBulkToggling] = useState(false); const intervalRef = useRef(null); const countdownRef = useRef(null); // Fetch all provider connections const fetchConnections = useCallback(async () => { try { const response = await fetch("/api/providers/client"); if (!response.ok) throw new Error("Failed to fetch connections"); const data = await response.json(); const connectionList = data.connections || []; setConnections(connectionList); return connectionList; } catch (error) { console.error("Error fetching connections:", error); setConnections([]); return []; } }, []); // Fetch quota for a specific connection const fetchQuota = useCallback(async (connectionId, provider) => { setLoading((prev) => ({ ...prev, [connectionId]: true })); setErrors((prev) => ({ ...prev, [connectionId]: null })); try { console.log( `[ProviderLimits] Fetching quota for ${provider} (${connectionId})`, ); const response = await fetch(`/api/usage/${connectionId}`); if (!response.ok) { const errorData = await response.json().catch(() => ({})); const errorMsg = errorData.error || response.statusText; // Handle different error types gracefully if (response.status === 404) { // Connection not found - skip silently console.warn( `[ProviderLimits] Connection not found for ${provider}, skipping`, ); return; } if (response.status === 401) { // Auth error - show message instead of throwing console.warn( `[ProviderLimits] Auth error for ${provider}:`, errorMsg, ); setQuotaData((prev) => ({ ...prev, [connectionId]: { quotas: [], message: errorMsg, }, })); return; } throw new Error(`HTTP ${response.status}: ${errorMsg}`); } const data = await response.json(); console.log(`[ProviderLimits] Got quota for ${provider}:`, data); // Parse quota data using provider-specific parser const parsedQuotas = parseQuotaData(provider, data); setQuotaData((prev) => ({ ...prev, [connectionId]: { quotas: parsedQuotas, plan: data.plan || null, message: data.message || null, raw: data, }, })); } catch (error) { console.error( `[ProviderLimits] Error fetching quota for ${provider} (${connectionId}):`, error, ); setErrors((prev) => ({ ...prev, [connectionId]: error.message || "Failed to fetch quota", })); } finally { setLoading((prev) => ({ ...prev, [connectionId]: false })); } }, []); // Refresh quota for a specific provider const refreshProvider = useCallback( async (connectionId, provider) => { await fetchQuota(connectionId, provider); setLastUpdated(new Date()); }, [fetchQuota], ); const handleDeleteConnection = useCallback(async (id) => { if (!confirm("Delete this connection?")) return; setDeletingId(id); try { const res = await fetch(`/api/providers/${id}`, { method: "DELETE" }); if (res.ok) { setConnections((prev) => prev.filter((c) => c.id !== id)); setQuotaData((prev) => { const next = { ...prev }; delete next[id]; return next; }); setLoading((prev) => { const next = { ...prev }; delete next[id]; return next; }); setErrors((prev) => { const next = { ...prev }; delete next[id]; return next; }); } } catch (error) { console.error("Error deleting connection:", error); } finally { setDeletingId(null); } }, []); const handleToggleConnectionActive = useCallback(async (id, isActive) => { setTogglingId(id); try { const res = await fetch(`/api/providers/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive }), }); if (res.ok) { setConnections((prev) => prev.map((c) => (c.id === id ? { ...c, isActive } : c)), ); } } catch (error) { console.error("Error updating connection status:", error); } finally { setTogglingId(null); } }, []); const handleUpdateConnection = useCallback( async (formData) => { if (!selectedConnection?.id) return; const connectionId = selectedConnection.id; const provider = selectedConnection.provider; try { const res = await fetch(`/api/providers/${connectionId}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify(formData), }); if (res.ok) { await fetchConnections(); setShowEditModal(false); setSelectedConnection(null); if (USAGE_SUPPORTED_PROVIDERS.includes(provider)) { await fetchQuota(connectionId, provider); } } } catch (error) { console.error("Error saving connection:", error); } }, [selectedConnection, fetchConnections, fetchQuota], ); useEffect(() => { let cancelled = false; fetch("/api/proxy-pools?isActive=true", { cache: "no-store" }) .then((res) => res.json()) .then((data) => { if (!cancelled && data?.proxyPools) { setProxyPools(data.proxyPools); } }) .catch(() => {}); return () => { cancelled = true; }; }, []); // Refresh all providers const refreshAll = useCallback(async () => { if (refreshingAll) return; setRefreshingAll(true); setCountdown(60); try { const conns = await fetchConnections(); // Filter eligible connections (OAuth + whitelisted apikey) const eligibleConnections = conns.filter(isUsageEligible); await Promise.all( eligibleConnections.map((conn) => fetchQuota(conn.id, conn.provider)), ); setLastUpdated(new Date()); } catch (error) { console.error("Error refreshing all providers:", error); } finally { setRefreshingAll(false); } }, [refreshingAll, fetchConnections, fetchQuota]); // Initial load: fetch connections first so cards render immediately, then fetch quotas useEffect(() => { const initializeData = async () => { setConnectionsLoading(true); const conns = await fetchConnections(); setConnectionsLoading(false); const eligibleConnections = conns.filter(isUsageEligible); // Mark all as loading before fetching const loadingState = {}; eligibleConnections.forEach((conn) => { loadingState[conn.id] = true; }); setLoading(loadingState); await Promise.all( eligibleConnections.map((conn) => fetchQuota(conn.id, conn.provider)), ); setLastUpdated(new Date()); }; initializeData(); }, []); // eslint-disable-line react-hooks/exhaustive-deps // Persist auto-refresh preference useEffect(() => { if (typeof window === "undefined") return; window.localStorage.setItem(AUTO_REFRESH_STORAGE_KEY, String(autoRefresh)); }, [autoRefresh]); // Auto-refresh interval useEffect(() => { if (!autoRefresh) { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } if (countdownRef.current) { clearInterval(countdownRef.current); countdownRef.current = null; } return; } // Main refresh interval intervalRef.current = setInterval(() => { refreshAll(); }, REFRESH_INTERVAL_MS); // Countdown interval countdownRef.current = setInterval(() => { setCountdown((prev) => { if (prev <= 1) return 60; return prev - 1; }); }, 1000); return () => { if (intervalRef.current) clearInterval(intervalRef.current); if (countdownRef.current) clearInterval(countdownRef.current); }; }, [autoRefresh, refreshAll]); // Pause auto-refresh when tab is hidden (Page Visibility API) useEffect(() => { const handleVisibilityChange = () => { if (document.hidden) { if (intervalRef.current) { clearInterval(intervalRef.current); intervalRef.current = null; } if (countdownRef.current) { clearInterval(countdownRef.current); countdownRef.current = null; } } else if (autoRefresh) { // Resume auto-refresh when tab becomes visible intervalRef.current = setInterval(refreshAll, REFRESH_INTERVAL_MS); countdownRef.current = setInterval(() => { setCountdown((prev) => (prev <= 1 ? 60 : prev - 1)); }, 1000); } }; document.addEventListener("visibilitychange", handleVisibilityChange); return () => { document.removeEventListener("visibilitychange", handleVisibilityChange); }; }, [autoRefresh, refreshAll]); // Filter eligible connections (OAuth + whitelisted apikey) const filteredConnections = connections.filter(isUsageEligible); const providerFilteredConnections = filteredConnections.filter( (conn) => providerFilter === "all" || conn.provider === providerFilter, ); const getEarliestResetTime = (conn) => { const resetTimes = (quotaData[conn.id]?.quotas || []) .map((quota) => quota.resetAt ? new Date(quota.resetAt).getTime() : Number.POSITIVE_INFINITY) .filter((time) => Number.isFinite(time)); return resetTimes.length > 0 ? Math.min(...resetTimes) : Number.POSITIVE_INFINITY; }; // Sort providers by USAGE_SUPPORTED_PROVIDERS order, then alphabetically. // Optionally surface accounts with quotas expiring soonest first. const sortedConnections = [...providerFilteredConnections].sort((a, b) => { if (expiringFirst) { const expiryDiff = getEarliestResetTime(a) - getEarliestResetTime(b); if (expiryDiff !== 0) return expiryDiff; } const orderA = USAGE_SUPPORTED_PROVIDERS.indexOf(a.provider); const orderB = USAGE_SUPPORTED_PROVIDERS.indexOf(b.provider); if (orderA !== orderB) return orderA - orderB; return a.provider.localeCompare(b.provider); }); // Connection is depleted when any quota entry hit the threshold const isConnectionDepleted = (conn) => { const quotas = quotaData[conn.id]?.quotas; if (!quotas?.length) return false; return quotas.some((q) => { if (!q.total || q.total <= 0) return false; return calculatePercentage(q.used, q.total) <= DEPLETED_QUOTA_THRESHOLD; }); }; const bulkSetActive = useCallback( async (targetIds, isActive) => { if (!targetIds.length || bulkToggling) return; setBulkToggling(true); try { await Promise.all( targetIds.map((id) => fetch(`/api/providers/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive }), }), ), ); setConnections((prev) => prev.map((c) => (targetIds.includes(c.id) ? { ...c, isActive } : c)), ); } catch (error) { console.error("Error bulk toggling connections:", error); } finally { setBulkToggling(false); } }, [bulkToggling], ); const handleDisableDepleted = () => { const ids = sortedConnections .filter((c) => (c.isActive ?? true) && isConnectionDepleted(c)) .map((c) => c.id); bulkSetActive(ids, false); }; const handleEnableAvailable = () => { const ids = sortedConnections .filter((c) => !(c.isActive ?? true) && !isConnectionDepleted(c)) .map((c) => c.id); bulkSetActive(ids, true); }; const providerOptions = Array.from(new Set(filteredConnections.map((conn) => conn.provider))).sort(); const selectedProviderLabel = providerFilter === "all" ? "All providers" : providerFilter; // Calculate summary stats const totalProviders = sortedConnections.length; const activeWithLimits = Object.values(quotaData).filter( (data) => data?.quotas?.length > 0, ).length; // Count low quotas (remaining < 30%) const lowQuotasCount = Object.values(quotaData).reduce((count, data) => { if (!data?.quotas) return count; const hasLowQuota = data.quotas.some((quota) => { const percentage = calculatePercentage(quota.used, quota.total); return percentage < 30 && quota.total > 0; }); return count + (hasLowQuota ? 1 : 0); }, 0); // Empty state if (!connectionsLoading && sortedConnections.length === 0) { return (
cloud_off

No Providers Connected

Connect to providers with OAuth to track your API quota limits and usage.

); } return (
{/* Header Controls */}

Provider Limits

{providerMenuOpen && ( <>
{providerOptions.map((provider) => ( ))}
)}
{/* Bulk: disable depleted */} {/* Bulk: enable available */} {/* Auto-refresh toggle */} {/* Refresh all button */}
{/* Provider cards: 2 columns, compact */}
{sortedConnections.map((conn) => { const quota = quotaData[conn.id]; const isLoading = loading[conn.id]; const error = errors[conn.id]; // Use table layout for all providers const isInactive = conn.isActive === false; const rowBusy = deletingId === conn.id || togglingId === conn.id; return (

{conn.provider}

{(() => { const isEmail = (v) => typeof v === "string" && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v); const label = isEmail(conn.email) ? conn.email : (isEmail(conn.name) ? conn.name : conn.name); return label ? (

{label}

) : null; })()}
handleToggleConnectionActive(conn.id, nextActive) } />
{isLoading ? (
progress_activity
) : error ? (
error

{error}

) : quota?.message ? (

{quota.message}

) : ( )}
); })}
{ setShowEditModal(false); setSelectedConnection(null); }} />
); }