feat(proxy-fitness): on/off toggle for egress geo probe
Header toggle in Proxy Fitness persists settings.poolGeoProbeEnabled (default on); the probe scheduler reads it each pass and skips when off. The env POOL_GEO_PROBE_DISABLED remains a hard override. Smart rotation is unaffected — it runs off the fitness registry from real request failures.
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
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 ProviderIcon from "@/shared/components/ProviderIcon";
|
||||||
import { useNotificationStore } from "@/store/notificationStore";
|
import { useNotificationStore } from "@/store/notificationStore";
|
||||||
|
|
||||||
@@ -69,6 +69,8 @@ export default function ProxyFitnessPage() {
|
|||||||
const [clearingScope, setClearingScope] = useState(null); // poolId::scope while clearing
|
const [clearingScope, setClearingScope] = useState(null); // poolId::scope while clearing
|
||||||
const [confirmClearAll, setConfirmClearAll] = useState(false);
|
const [confirmClearAll, setConfirmClearAll] = useState(false);
|
||||||
const [clearingAll, setClearingAll] = useState(false);
|
const [clearingAll, setClearingAll] = useState(false);
|
||||||
|
const [geoEnabled, setGeoEnabled] = useState(true);
|
||||||
|
const [geoUpdating, setGeoUpdating] = useState(false);
|
||||||
const providerMenuRef = useRef(null);
|
const providerMenuRef = useRef(null);
|
||||||
const notify = useNotificationStore();
|
const notify = useNotificationStore();
|
||||||
|
|
||||||
@@ -109,6 +111,36 @@ export default function ProxyFitnessPage() {
|
|||||||
fetchAll();
|
fetchAll();
|
||||||
}, [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]);
|
const records = useMemo(() => buildRecords(fitness, pools), [fitness, pools]);
|
||||||
|
|
||||||
// Providers present in the fitness data (filter options)
|
// Providers present in the fitness data (filter options)
|
||||||
@@ -215,6 +247,14 @@ export default function ProxyFitnessPage() {
|
|||||||
Clear All{providerFilter !== "all" ? ` (${providerFilter})` : ""}
|
Clear All{providerFilter !== "all" ? ` (${providerFilter})` : ""}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
<Toggle
|
||||||
|
size="sm"
|
||||||
|
checked={geoEnabled}
|
||||||
|
onChange={handleGeoToggle}
|
||||||
|
disabled={geoUpdating}
|
||||||
|
label="Geo probe"
|
||||||
|
description="Probe egress IP/country per pool ±30 min (pemakaian kuota geo-API)"
|
||||||
|
/>
|
||||||
<Button variant="secondary" size="sm" icon="refresh" onClick={fetchAll}>Refresh</Button>
|
<Button variant="secondary" size="sm" icon="refresh" onClick={fetchAll}>Refresh</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -10,6 +10,7 @@
|
|||||||
import { getProxyPools } from "@/models";
|
import { getProxyPools } from "@/models";
|
||||||
import { probePoolGeo, setPoolGeo, getPoolGeo } from "open-sse/services/poolGeo.js";
|
import { probePoolGeo, setPoolGeo, getPoolGeo } from "open-sse/services/poolGeo.js";
|
||||||
import { isNonServerRuntime } from "@/sse/services/backgroundTokenRefresh.js";
|
import { isNonServerRuntime } from "@/sse/services/backgroundTokenRefresh.js";
|
||||||
|
import { getSettings } from "@/lib/localDb";
|
||||||
|
|
||||||
const PROBE_INTERVAL_MS = 30 * 60 * 1000;
|
const PROBE_INTERVAL_MS = 30 * 60 * 1000;
|
||||||
const INITIAL_DELAY_MS = 15 * 1000;
|
const INITIAL_DELAY_MS = 15 * 1000;
|
||||||
@@ -44,6 +45,13 @@ async function probeAll() {
|
|||||||
if (probing) return;
|
if (probing) return;
|
||||||
probing = true;
|
probing = true;
|
||||||
try {
|
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 pools = await getProxyPools({ isActive: true });
|
||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
const active = (pools || []).filter((p) => !!p?.proxyUrl);
|
const active = (pools || []).filter((p) => !!p?.proxyUrl);
|
||||||
|
|||||||
Reference in New Issue
Block a user