diff --git a/src/app/(dashboard)/dashboard/proxy-fitness/page.js b/src/app/(dashboard)/dashboard/proxy-fitness/page.js index 5d886489..36a71044 100644 --- a/src/app/(dashboard)/dashboard/proxy-fitness/page.js +++ b/src/app/(dashboard)/dashboard/proxy-fitness/page.js @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useMemo, useRef, useState } from "react"; -import { Badge, Button, Card, CardSkeleton, Input, ConfirmModal } from "@/shared/components"; +import { Badge, Button, Card, CardSkeleton, Input, ConfirmModal, Toggle } from "@/shared/components"; import ProviderIcon from "@/shared/components/ProviderIcon"; import { useNotificationStore } from "@/store/notificationStore"; @@ -69,6 +69,8 @@ export default function ProxyFitnessPage() { const [clearingScope, setClearingScope] = useState(null); // poolId::scope while clearing const [confirmClearAll, setConfirmClearAll] = useState(false); const [clearingAll, setClearingAll] = useState(false); + const [geoEnabled, setGeoEnabled] = useState(true); + const [geoUpdating, setGeoUpdating] = useState(false); const providerMenuRef = useRef(null); const notify = useNotificationStore(); @@ -109,6 +111,36 @@ export default function ProxyFitnessPage() { fetchAll(); }, [fetchAll]); + useEffect(() => { + fetch("/api/settings", { cache: "no-store" }) + .then((r) => (r.ok ? r.json() : {})) + .then((s) => { + if (typeof s.poolGeoProbeEnabled === "boolean") setGeoEnabled(s.poolGeoProbeEnabled); + }) + .catch(() => {}); + }, []); + + const handleGeoToggle = async (next) => { + setGeoUpdating(true); + setGeoEnabled(next); + try { + const res = await fetch("/api/settings", { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ poolGeoProbeEnabled: next }), + }); + if (!res.ok) { + setGeoEnabled(!next); + throw new Error(`HTTP ${res.status}`); + } + notify.success(next ? "Geo probe enabled" : "Geo probe disabled"); + } catch (err) { + notify.error(`Update failed: ${err.message}`); + } finally { + setGeoUpdating(false); + } + }; + const records = useMemo(() => buildRecords(fitness, pools), [fitness, pools]); // Providers present in the fitness data (filter options) @@ -215,6 +247,14 @@ export default function ProxyFitnessPage() { Clear All{providerFilter !== "all" ? ` (${providerFilter})` : ""} )} + diff --git a/src/lib/network/poolEgressProbe.js b/src/lib/network/poolEgressProbe.js index 43a84eb6..61b1e4b7 100644 --- a/src/lib/network/poolEgressProbe.js +++ b/src/lib/network/poolEgressProbe.js @@ -10,6 +10,7 @@ import { getProxyPools } from "@/models"; import { probePoolGeo, setPoolGeo, getPoolGeo } from "open-sse/services/poolGeo.js"; import { isNonServerRuntime } from "@/sse/services/backgroundTokenRefresh.js"; +import { getSettings } from "@/lib/localDb"; const PROBE_INTERVAL_MS = 30 * 60 * 1000; const INITIAL_DELAY_MS = 15 * 1000; @@ -44,6 +45,13 @@ async function probeAll() { if (probing) return; probing = true; try { + // UI toggle (settings.poolGeoProbeEnabled) gates the whole feature; the + // env var remains a hard override for headless setups. + try { + const settings = await getSettings(); + if (settings?.poolGeoProbeEnabled === false) return; + } catch { /* DB hiccup — keep probing (fail-open) */ } + const pools = await getProxyPools({ isActive: true }); const now = Date.now(); const active = (pools || []).filter((p) => !!p?.proxyUrl);