import { useState, useEffect } from 'react'; import { useTranslation } from 'react-i18next'; import { request } from '../../utils/request'; import { showToast } from '../common/ToastContainer'; import { Plus, Network, Upload, RefreshCw, Link2, SlidersHorizontal, Trash2, Power } from 'lucide-react'; import { ProxyPoolConfig, ProxyEntry } from '../../types/config'; import ProxyList from './proxy/ProxyList'; import ProxyEditModal from './proxy/ProxyEditModal'; import BatchImportModal from './proxy/BatchImportModal'; import ProxyBindingManager from './proxy/ProxyBindingManager'; import { useAccountStore } from '../../stores/useAccountStore'; interface ProxyPoolSettingsProps { config: ProxyPoolConfig; onChange: (config: ProxyPoolConfig, silent?: boolean) => void; } export default function ProxyPoolSettings({ config, onChange }: ProxyPoolSettingsProps) { const { t } = useTranslation(); const { accounts, fetchAccounts } = useAccountStore(); const [isAddModalOpen, setIsAddModalOpen] = useState(false); const [isBatchImportOpen, setIsBatchImportOpen] = useState(false); const [isBindingManagerOpen, setIsBindingManagerOpen] = useState(false); const [isTesting, setIsTesting] = useState(false); const [accountBindings, setAccountBindings] = useState>({}); const [selectedIds, setSelectedIds] = useState>(new Set()); // Fetch bindings and accounts on mount useEffect(() => { fetchBindings(); fetchAccounts(); }, []); // Refresh bindings when manager closes useEffect(() => { if (!isBindingManagerOpen) { fetchBindings(); } }, [isBindingManagerOpen]); // [FIX] Polling for proxy pool status // Now only updates volatile status (is_healthy, latency) to avoid race condition regressions useEffect(() => { let interval: any; if (config.enabled) { // Only poll if proxy pool is enabled interval = setInterval(async () => { try { const liveConfig = await request('get_proxy_pool_config'); if (liveConfig && liveConfig.proxies) { // Create a map for quick lookups const liveMap = new Map(liveConfig.proxies.map(p => [p.id, p])); // Check if any status actually changed let hasChanges = false; const updatedProxies = config.proxies.map(p => { const live = liveMap.get(p.id); if (live && (live.is_healthy !== p.is_healthy || live.latency !== p.latency || live.last_check_time !== p.last_check_time)) { hasChanges = true; return { ...p, is_healthy: live.is_healthy, latency: live.latency, last_check_time: live.last_check_time }; } return p; }); if (hasChanges) { // Only update volatile status, DO NOT trigger heavy onChange which saves to disk // This internal change will eventually be captured by next manual save or // simply keep the UI fresh without risking rolling back user's structural changes (add/delete) onChange({ ...config, proxies: updatedProxies }, true); // Pass 'true' as silent flag if onChange supports it, or use a separate state } } } catch (e) { // Ignore if service not running or other errors console.error('Failed to poll proxy pool config:', e); } }, 5000); // Poll every 5s } return () => clearInterval(interval); }, [config.enabled, config.proxies]); // Depend on config.enabled and config.proxies to re-evaluate polling const fetchBindings = async () => { try { const bindings = await request>('get_all_account_bindings'); if (bindings) setAccountBindings(bindings); } catch (e) { console.error('Fetch bindings failed:', e); } }; const safeConfig: ProxyPoolConfig = { enabled: config?.enabled ?? false, proxies: config?.proxies ?? [], health_check_interval: config?.health_check_interval ?? 300, auto_failover: config?.auto_failover ?? true, strategy: config?.strategy ?? 'priority', }; const handleUpdateProxies = (proxies: ProxyEntry[]) => { onChange({ ...safeConfig, proxies }); }; const handleAddProxy = (entry: ProxyEntry) => { onChange({ ...safeConfig, proxies: [...safeConfig.proxies, entry] }); }; const handleBatchImport = async (newProxies: ProxyEntry[]) => { const updatedProxies = [...safeConfig.proxies, ...newProxies]; await onChange({ ...safeConfig, proxies: updatedProxies }); // Auto-trigger test after import is fully committed handleTestAll(); }; const handleBatchDelete = () => { if (selectedIds.size === 0) return; if (confirm(t('settings.proxy_pool.confirm_batch_delete', 'Are you sure you want to delete selected proxies?'))) { const newProxies = safeConfig.proxies.filter(p => !selectedIds.has(p.id)); onChange({ ...safeConfig, proxies: newProxies }); setSelectedIds(new Set()); showToast(t('common.deleted', 'Deleted successfully'), 'success'); } }; const handleBatchToggleEnabled = (enabled: boolean) => { if (selectedIds.size === 0) return; const newProxies = safeConfig.proxies.map(p => selectedIds.has(p.id) ? { ...p, enabled } : p ); onChange({ ...safeConfig, proxies: newProxies }); showToast(t(enabled ? 'common.enabled' : 'common.disabled', enabled ? 'Enabled' : 'Disabled'), 'success'); }; const handleTestAll = async () => { setIsTesting(true); try { const liveConfig = await request('check_proxy_health'); if (liveConfig && liveConfig.proxies) { // [FIX] Use incremental merge to prevent race condition rollbacks const liveMap = new Map(liveConfig.proxies.map(p => [p.id, p])); const updatedProxies = config.proxies.map(p => { const live = liveMap.get(p.id); if (live) { return { ...p, is_healthy: live.is_healthy, latency: live.latency, last_check_time: live.last_check_time }; } return p; }); // Update local UI state silently (syncing health stats only) onChange({ ...config, proxies: updatedProxies }, true); } showToast(t('settings.proxy_pool.test_completed', 'Health check completed'), 'success'); } catch (error) { console.error('Test all failed:', error); showToast(t('settings.proxy_pool.test_failed', 'Health check failed'), 'error'); } finally { setIsTesting(false); } }; return (
{/* Consolidated Header & Toolbar */}
{/* Left: Component Identity & Feature Toggle */}

{t('settings.proxy_pool.title', 'Proxy Pool')}

{/* Middle: Configuration Parameters (Strategy & Interval) */}
{ const val = parseInt(e.target.value) || 60; if (val !== safeConfig.health_check_interval) { onChange({ ...safeConfig, health_check_interval: val }); } }} className="w-10 text-[10px] bg-transparent border-none p-0 focus:ring-0 font-black text-gray-700 dark:text-gray-300 text-right group-hover:text-emerald-600 transition-colors" /> {t('settings.proxy_pool.seconds', 'Sec')}
{/* Right: Actions or Selection Toolbar */}
{selectedIds.size > 0 ? (
{selectedIds.size} {t('common.selected', 'Selected')}
) : ( <>
)}
{/* Proxy List - Always visible, with status context */}
{!safeConfig.enabled && (
{t('settings.proxy_pool.inactive_notice', 'Proxy Pool Inactive')}
)}
{isAddModalOpen && ( setIsAddModalOpen(false)} onSave={handleAddProxy} isEditing={false} /> )} {isBatchImportOpen && ( setIsBatchImportOpen(false)} onImport={handleBatchImport} /> )} {isBindingManagerOpen && ( setIsBindingManagerOpen(false)} proxies={safeConfig.proxies} /> )}
); }