"use client"; import { useCallback, useEffect, useMemo, useState, useRef } from "react"; import { Badge, Button, Card, CardSkeleton, Input, Modal, Toggle, ConfirmModal } 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 [showCloudflareModal, setShowCloudflareModal] = useState(false); const [showDenoModal, setShowDenoModal] = useState(false); const [showRelayMenu, setShowRelayMenu] = 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 [cloudflareForm, setCloudflareForm] = useState({ accountId: "", apiToken: "", projectName: "cloudflare-relay" }); const [denoForm, setDenoForm] = useState({ denoToken: "", orgDomain: "", projectName: "" }); const [saving, setSaving] = useState(false); const [importing, setImporting] = useState(false); const [deploying, setDeploying] = useState(false); const [testingId, setTestingId] = useState(null); const [selectedIds, setSelectedIds] = useState([]); const [healthChecking, setHealthChecking] = useState(false); const [healthProgress, setHealthProgress] = useState({ current: 0, total: 0 }); const [bulkBusy, setBulkBusy] = useState(false); const [confirmState, setConfirmState] = useState(null); const relayMenuRef = useRef(null); const notify = useNotificationStore(); useEffect(() => { const handleClickOutside = (e) => { if (relayMenuRef.current && !relayMenuRef.current.contains(e.target)) { setShowRelayMenu(false); } }; if (showRelayMenu) { document.addEventListener("mousedown", handleClickOutside); } return () => document.removeEventListener("mousedown", handleClickOutside); }, [showRelayMenu]); 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) => { setConfirmState({ title: "Delete Proxy Pool", message: `Delete proxy pool "${proxyPool.name}"?`, onConfirm: async () => { setConfirmState(null); 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 handleToggleActive = async (pool) => { const next = !pool.isActive; setProxyPools((prev) => prev.map((p) => p.id === pool.id ? { ...p, isActive: next } : p)); try { const res = await fetch(`/api/proxy-pools/${pool.id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive: next }), }); if (!res.ok) { setProxyPools((prev) => prev.map((p) => p.id === pool.id ? { ...p, isActive: pool.isActive } : p)); notify.error("Failed to update active state"); } } catch (error) { console.log("Error toggling active:", error); setProxyPools((prev) => prev.map((p) => p.id === pool.id ? { ...p, isActive: pool.isActive } : p)); } }; const allSelected = proxyPools.length > 0 && selectedIds.length === proxyPools.length; const toggleSelect = (id) => setSelectedIds((prev) => prev.includes(id) ? prev.filter((x) => x !== id) : [...prev, id]); const toggleSelectAll = () => setSelectedIds(allSelected ? [] : proxyPools.map((p) => p.id)); const clearSelection = () => setSelectedIds([]); const bulkSetActive = async (isActive) => { const targets = selectedIds.length > 0 ? selectedIds : proxyPools.map((p) => p.id); if (targets.length === 0) return; setBulkBusy(true); try { let ok = 0; let failed = 0; for (const id of targets) { try { const res = await fetch(`/api/proxy-pools/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive }), }); if (res.ok) ok += 1; else failed += 1; } catch { failed += 1; } } await fetchProxyPools(); notify.success(`${isActive ? "Activated" : "Deactivated"} ${ok}${failed ? `, failed ${failed}` : ""}`); } finally { setBulkBusy(false); } }; const bulkDelete = async () => { if (selectedIds.length === 0) return; setConfirmState({ title: "Delete Proxy Pools", message: `Delete ${selectedIds.length} proxy pool(s)?`, onConfirm: async () => { setConfirmState(null); setBulkBusy(true); try { let ok = 0; let blocked = 0; let failed = 0; for (const id of selectedIds) { try { const res = await fetch(`/api/proxy-pools/${id}`, { method: "DELETE" }); if (res.ok) ok += 1; else if (res.status === 409) blocked += 1; else failed += 1; } catch { failed += 1; } } await fetchProxyPools(); clearSelection(); notify.success(`Deleted ${ok}${blocked ? `, ${blocked} bound` : ""}${failed ? `, ${failed} failed` : ""}`); } finally { setBulkBusy(false); } } }); }; const handleHealthCheck = async () => { const targets = selectedIds.length > 0 ? proxyPools.filter((p) => selectedIds.includes(p.id)) : proxyPools; if (targets.length === 0) return; setHealthChecking(true); setHealthProgress({ current: 0, total: targets.length }); let alive = 0; const deadIds = []; let done = 0; const CONCURRENCY = 10; const queue = [...targets]; const worker = async () => { while (queue.length > 0) { const pool = queue.shift(); if (!pool) break; try { const res = await fetch(`/api/proxy-pools/${pool.id}/test`, { method: "POST" }); const data = await res.json(); if (res.ok && data.ok) alive += 1; else deadIds.push(pool.id); } catch { deadIds.push(pool.id); } finally { done += 1; setHealthProgress({ current: done, total: targets.length }); } } }; await Promise.all(Array.from({ length: Math.min(CONCURRENCY, targets.length) }, worker)); await fetchProxyPools(); setHealthChecking(false); setHealthProgress({ current: 0, total: 0 }); if (deadIds.length > 0) { setConfirmState({ title: "Disable Dead Proxies", message: `Alive: ${alive}, Dead: ${deadIds.length}.\n\nDisable ${deadIds.length} dead proxies?`, onConfirm: async () => { setConfirmState(null); setBulkBusy(true); try { for (const id of deadIds) { try { await fetch(`/api/proxy-pools/${id}`, { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ isActive: false }), }); } catch {} } await fetchProxyPools(); notify.success(`Disabled ${deadIds.length} dead proxies`); } finally { setBulkBusy(false); } } }); } else { notify.success(`Health check done. Alive: ${alive}, Dead: ${deadIds.length}`); } }; // Cleanup selectedIds when pools change useEffect(() => { setSelectedIds((prev) => prev.filter((id) => proxyPools.some((p) => p.id === id))); }, [proxyPools]); 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 openCloudflareModal = () => { setCloudflareForm({ accountId: "", apiToken: "", projectName: "cloudflare-relay" }); setShowCloudflareModal(true); }; const closeCloudflareModal = () => { if (deploying) return; setShowCloudflareModal(false); }; const openDenoModal = () => { setDenoForm({ denoToken: "", orgDomain: "", projectName: "" }); setShowDenoModal(true); }; const closeDenoModal = () => { if (deploying) return; setShowDenoModal(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 handleCloudflareDeploy = async () => { if (!cloudflareForm.accountId.trim() || !cloudflareForm.apiToken.trim()) return; setDeploying(true); try { const res = await fetch("/api/proxy-pools/cloudflare-deploy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(cloudflareForm), }); const data = await res.json(); if (res.ok) { await fetchProxyPools(); closeCloudflareModal(); notify.success(`Deployed: ${data.deployUrl}`); } else { notify.error(data.error || "Deploy failed"); } } catch (error) { console.log("Error deploying Cloudflare relay:", error); notify.error("Deploy failed"); } finally { setDeploying(false); } }; const handleDenoDeploy = async () => { if (!denoForm.denoToken.trim()) return; setDeploying(true); try { const res = await fetch("/api/proxy-pools/deno-deploy", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(denoForm), }); const data = await res.json(); if (res.ok) { await fetchProxyPools(); closeDenoModal(); notify.success(`Deployed: ${data.deployUrl}`); } else { notify.error(data.error || "Deploy failed"); } } catch (error) { console.log("Error deploying Deno 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

{showRelayMenu && (
)}
{proxyPools.length > 0 && ( )} Total: {proxyPools.length} Active: {activeCount}
{(selectedIds.length > 0 || healthChecking) && (
checklist {selectedIds.length > 0 ? `${selectedIds.length} selected` : "All pools"}
{selectedIds.length > 0 && ( <> )}
)} {proxyPools.length === 0 ? (

No proxy pool entries yet

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

) : (
{proxyPools.map((pool) => (
toggleSelect(pool.id)} className="mt-1 size-4 shrink-0 rounded border-black/20 dark:border-white/20" />

{pool.name}

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

{pool.proxyUrl}

{pool.noProxy ? (

No proxy: {pool.noProxy}

) : null}

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

handleToggleActive(pool)} title={pool.isActive ? "Disable" : "Enable"} />
))}
)}