"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 Button from "@/shared/components/Button"; import { EditConnectionModal } from "@/shared/components"; import { USAGE_SUPPORTED_PROVIDERS } from "@/shared/constants/providers"; const REFRESH_INTERVAL_MS = 60000; // 60 seconds export default function ProviderLimits() { const [connections, setConnections] = useState([]); const [quotaData, setQuotaData] = useState({}); const [loading, setLoading] = useState({}); const [errors, setErrors] = useState({}); const [autoRefresh, setAutoRefresh] = useState(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 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 only supported OAuth providers const oauthConnections = conns.filter( (conn) => USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth", ); // Fetch quota for supported OAuth connections only await Promise.all( oauthConnections.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 oauthConnections = conns.filter( (conn) => USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth", ); // Mark all as loading before fetching const loadingState = {}; oauthConnections.forEach((conn) => { loadingState[conn.id] = true; }); setLoading(loadingState); await Promise.all( oauthConnections.map((conn) => fetchQuota(conn.id, conn.provider)), ); setLastUpdated(new Date()); }; initializeData(); }, []); // eslint-disable-line react-hooks/exhaustive-deps // 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]); // Format last updated time const formatLastUpdated = useCallback(() => { if (!lastUpdated) return "Never"; const now = new Date(); const diffMs = now - lastUpdated; const diffMinutes = Math.floor(diffMs / (1000 * 60)); const diffHours = Math.floor(diffMinutes / 60); const diffDays = Math.floor(diffHours / 24); if (diffDays > 0) return `${diffDays}d ago`; if (diffHours > 0) return `${diffHours}h ago`; if (diffMinutes > 0) return `${diffMinutes}m ago`; return "Just now"; }, [lastUpdated]); // Filter only supported providers const filteredConnections = connections.filter( (conn) => USAGE_SUPPORTED_PROVIDERS.includes(conn.provider) && conn.authType === "oauth", ); // Sort providers by USAGE_SUPPORTED_PROVIDERS order, then alphabetically const sortedConnections = [...filteredConnections].sort((a, b) => { 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); }); // 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

Last updated: {formatLastUpdated()}
{/* 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); }} />
); }