"use client"; import { useCallback, useEffect, useMemo, useState } from "react"; import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle } from "@/shared/components"; import { useNotificationStore } from "@/store/notificationStore"; function getStatusVariant(status) { if (status === "active") return "success"; if (status === "error") return "error"; return "default"; } function formatDateTime(value) { if (!value) return "Never"; const date = new Date(value); if (Number.isNaN(date.getTime())) return "Never"; return date.toLocaleString(); } function normalizeFormData(data = {}) { return { name: data.name || "", proxyUrl: data.proxyUrl || "", noProxy: data.noProxy || "", isActive: data.isActive !== false, strictProxy: data.strictProxy === true, }; } export default function ProxyPoolsPage() { const [proxyPools, setProxyPools] = useState([]); const [loading, setLoading] = useState(true); const [showFormModal, setShowFormModal] = useState(false); const [showBatchImportModal, setShowBatchImportModal] = useState(false); const [showVercelModal, setShowVercelModal] = useState(false); const [editingProxyPool, setEditingProxyPool] = useState(null); const [formData, setFormData] = useState(normalizeFormData()); const [batchImportText, setBatchImportText] = useState(""); const [vercelForm, setVercelForm] = useState({ vercelToken: "", projectName: "vercel-relay" }); const [saving, setSaving] = useState(false); const [importing, setImporting] = useState(false); const [deploying, setDeploying] = useState(false); const [testingId, setTestingId] = useState(null); const notify = useNotificationStore(); const fetchProxyPools = useCallback(async () => { try { const res = await fetch("/api/proxy-pools?includeUsage=true", { cache: "no-store" }); const data = await res.json(); if (res.ok) { setProxyPools(data.proxyPools || []); } } catch (error) { console.log("Error fetching proxy pools:", error); } finally { setLoading(false); } }, []); useEffect(() => { fetchProxyPools(); }, [fetchProxyPools]); const resetForm = () => { setEditingProxyPool(null); setFormData(normalizeFormData()); }; const openCreateModal = () => { resetForm(); setShowFormModal(true); }; const openEditModal = (proxyPool) => { setEditingProxyPool(proxyPool); setFormData(normalizeFormData(proxyPool)); setShowFormModal(true); }; const closeFormModal = () => { setShowFormModal(false); resetForm(); }; const handleSave = async () => { const payload = { name: formData.name.trim(), proxyUrl: formData.proxyUrl.trim(), noProxy: formData.noProxy.trim(), isActive: formData.isActive === true, strictProxy: formData.strictProxy === true, }; if (!payload.name || !payload.proxyUrl) return; setSaving(true); try { const isEdit = !!editingProxyPool; const res = await fetch(isEdit ? `/api/proxy-pools/${editingProxyPool.id}` : "/api/proxy-pools", { method: isEdit ? "PUT" : "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }); if (res.ok) { await fetchProxyPools(); closeFormModal(); notify.success(editingProxyPool ? "Proxy pool updated" : "Proxy pool created"); } else { const data = await res.json(); notify.error(data.error || "Failed to save proxy pool"); } } catch (error) { console.log("Error saving proxy pool:", error); } finally { setSaving(false); } }; const handleDelete = async (proxyPool) => { const deleting = confirm(`Delete proxy pool \"${proxyPool.name}\"?`); if (!deleting) return; try { const res = await fetch(`/api/proxy-pools/${proxyPool.id}`, { method: "DELETE" }); if (res.ok) { setProxyPools((prev) => prev.filter((item) => item.id !== proxyPool.id)); notify.success("Proxy pool deleted"); return; } const data = await res.json(); if (res.status === 409) { notify.warning(`Cannot delete: ${data.boundConnectionCount || 0} connection(s) are still using this pool.`); } else { notify.error(data.error || "Failed to delete proxy pool"); } } catch (error) { console.log("Error deleting proxy pool:", error); notify.error("Failed to delete proxy pool"); } }; const handleTest = async (proxyPoolId) => { setTestingId(proxyPoolId); try { const res = await fetch(`/api/proxy-pools/${proxyPoolId}/test`, { method: "POST" }); const data = await res.json(); if (!res.ok) { notify.error(data.error || "Failed to test proxy"); return; } await fetchProxyPools(); notify.success(data.ok ? "Proxy test passed" : "Proxy test failed"); } catch (error) { console.log("Error testing proxy pool:", error); notify.error("Failed to test proxy"); } finally { setTestingId(null); } }; const openBatchImportModal = () => { setBatchImportText(""); setShowBatchImportModal(true); }; const closeBatchImportModal = () => { if (importing) return; setShowBatchImportModal(false); }; const openVercelModal = () => { setVercelForm({ vercelToken: "", projectName: "vercel-relay" }); setShowVercelModal(true); }; const closeVercelModal = () => { if (deploying) return; setShowVercelModal(false); }; const handleVercelDeploy = async () => { if (!vercelForm.vercelToken.trim()) return; setDeploying(true); try { const res = await fetch("/api/proxy-pools/vercel-deploy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(vercelForm), }); const data = await res.json(); if (res.ok) { await fetchProxyPools(); closeVercelModal(); notify.success(`Deployed: ${data.deployUrl}`); } else { notify.error(data.error || "Deploy failed"); } } catch (error) { console.log("Error deploying Vercel relay:", error); notify.error("Deploy failed"); } finally { setDeploying(false); } }; const parseProxyLine = (line) => { const trimmed = line.trim(); if (!trimmed) return null; if (trimmed.includes("://")) { const parsed = new URL(trimmed); const hostLabel = parsed.port ? `${parsed.hostname}:${parsed.port}` : parsed.hostname; return { proxyUrl: parsed.toString(), name: `Imported ${hostLabel}`, }; } const parts = trimmed.split(":"); if (parts.length === 4) { const [host, port, username, password] = parts; if (!host || !port || !username || !password) { throw new Error("Invalid host:port:user:pass format"); } const proxyUrl = `http://${encodeURIComponent(username)}:${encodeURIComponent(password)}@${host}:${port}`; const parsed = new URL(proxyUrl); return { proxyUrl: parsed.toString(), name: `Imported ${host}:${port}`, }; } throw new Error("Unsupported format"); }; const handleBatchImport = async () => { const lines = batchImportText .split(/\r?\n/) .map((line) => line.trim()) .filter(Boolean); if (lines.length === 0) { notify.warning("Please paste at least one proxy line."); return; } const parsedEntries = []; const invalidLines = []; lines.forEach((line, index) => { try { const parsed = parseProxyLine(line); if (parsed) { parsedEntries.push({ ...parsed, lineNumber: index + 1, }); } } catch (error) { invalidLines.push(`Line ${index + 1}: ${error.message}`); } }); if (invalidLines.length > 0) { notify.error(`Invalid proxy format:\n${invalidLines.join("\n")}`); return; } setImporting(true); try { const existingKeys = new Set( proxyPools.map((pool) => `${(pool.proxyUrl || "").trim()}|||${(pool.noProxy || "").trim()}`) ); let created = 0; let skipped = 0; let failed = 0; for (const entry of parsedEntries) { const dedupeKey = `${entry.proxyUrl}|||`; if (existingKeys.has(dedupeKey)) { skipped += 1; continue; } const res = await fetch("/api/proxy-pools", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ name: entry.name, proxyUrl: entry.proxyUrl, noProxy: "", isActive: true, }), }); if (res.ok) { created += 1; existingKeys.add(dedupeKey); } else { failed += 1; } } await fetchProxyPools(); setShowBatchImportModal(false); notify.success(`Batch import completed: Created ${created}, Skipped ${skipped}, Failed ${failed}`); } catch (error) { console.log("Error batch importing proxies:", error); notify.error("Batch import failed"); } finally { setImporting(false); } }; const activeCount = useMemo( () => proxyPools.filter((pool) => pool.isActive === true).length, [proxyPools] ); if (loading) { return (
); } return (

Proxy Pools

Manage reusable per-connection proxies and bind them to provider connections.

Total: {proxyPools.length} Active: {activeCount}
{proxyPools.length === 0 ? (

No proxy pool entries yet

Create a proxy pool entry, then assign it to connections.

) : (
{proxyPools.map((pool) => (

{pool.name}

{pool.testStatus || "unknown"} {pool.isActive ? "active" : "inactive"} {pool.type === "vercel" && ( vercel relay )} {pool.boundConnectionCount || 0} bound

{pool.proxyUrl}

{pool.noProxy ? (

No proxy: {pool.noProxy}

) : null}

Last tested: {formatDateTime(pool.lastTestedAt)} {pool.lastError ? ` · ${pool.lastError}` : ""}

))}
)}