+ )}
+
{/* OAuth Providers */}
+ {oauthEntries.length > 0 && (
+ )}
{/* Free Tier Providers */}
+ {(freeEntries.length > 0 || freeTierEntries.length > 0) && (
@@ -314,7 +363,7 @@ export default function ProvidersPage() {
- {Object.entries(FREE_PROVIDERS).map(([key, info]) => (
+ {freeEntries.map(([key, info]) => (
handleToggleProvider(key, "oauth", active)}
/>
))}
- {Object.entries(FREE_TIER_PROVIDERS).map(([key, info]) => (
+ {freeTierEntries.map(([key, info]) => (
+ )}
{/* API Key Providers — fixed list */}
+ {apikeyEntries.length > 0 && (
@@ -363,20 +414,19 @@ export default function ProvidersPage() {
- {Object.entries(APIKEY_PROVIDERS)
- .filter(([, info]) => (info.serviceKinds ?? ["llm"]).includes("llm"))
- .map(([key, info]) => (
-
handleToggleProvider(key, "apikey", active)}
- />
- ))}
+ {apikeyEntries.map(([key, info]) => (
+ handleToggleProvider(key, "apikey", active)}
+ />
+ ))}
+ )}
{/* Web Cookie Providers — use browser subscription cookie instead of API key */}
{/*
diff --git a/src/app/(dashboard)/dashboard/proxy-pools/page.js b/src/app/(dashboard)/dashboard/proxy-pools/page.js
index 383cf562..6eab6b6c 100644
--- a/src/app/(dashboard)/dashboard/proxy-pools/page.js
+++ b/src/app/(dashboard)/dashboard/proxy-pools/page.js
@@ -41,6 +41,10 @@ export default function ProxyPoolsPage() {
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 notify = useNotificationStore();
const fetchProxyPools = useCallback(async () => {
@@ -162,6 +166,136 @@ export default function ProxyPoolsPage() {
}
};
+ 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;
+ if (!confirm(`Delete ${selectedIds.length} proxy pool(s)?`)) return;
+ 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 && confirm(`Alive: ${alive}, Dead: ${deadIds.length}.\n\nDisable ${deadIds.length} dead proxies?`)) {
+ 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);
@@ -354,13 +488,57 @@ export default function ProxyPoolsPage() {
-
-
- Total: {proxyPools.length}
- Active: {activeCount}
-
+
+ {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
@@ -372,8 +550,15 @@ export default function ProxyPoolsPage() {
) : (
{proxyPools.map((pool) => (
-
-
+
+
+
toggleSelect(pool.id)}
+ className="mt-1 size-4 shrink-0 rounded border-black/20 dark:border-white/20"
+ />
+
{pool.name}
@@ -397,9 +582,16 @@ export default function ProxyPoolsPage() {
Last tested: {formatDateTime(pool.lastTestedAt)}
{pool.lastError ? ` · ${pool.lastError}` : ""}
+
-
+
+
handleToggleActive(pool)}
+ title={pool.isActive ? "Disable" : "Enable"}
+ />