import { useState, useEffect, useCallback } from 'react'
import { Pencil, Play, Plus, Shield, Trash2, X, Unlock, Heart, HeartOff, Activity } from 'lucide-react'
import clsx from 'clsx'
import { useI18n } from '../../i18n'
async function readApiResponse(res, nonJsonMessage) {
const contentType = String(res.headers.get('content-type') || '').toLowerCase()
const raw = await res.text()
const trimmed = raw.trim()
if (!trimmed) {
return {}
}
if (contentType.includes('application/json')) {
try {
return JSON.parse(trimmed)
} catch (_err) {
if (!res.ok) {
return { detail: trimmed }
}
throw new Error(nonJsonMessage)
}
}
if (!res.ok) {
return { detail: trimmed }
}
throw new Error(nonJsonMessage)
}
const EMPTY_FORM = {
name: '',
type: 'socks5h',
host: '',
port: 1080,
username: '',
password: '',
}
function createEmptyProxyForm() {
return { ...EMPTY_FORM }
}
function ProxyStatusBadge({ t, result, testing = false, disabled = false, health = null }) {
if (testing) {
return (
{t('proxyManager.testing')}
)
}
if (disabled) {
return (
{t('proxyManager.banned')}
)
}
if (health) {
if (health.healthy) {
return (
{t('proxyManager.healthy', { time: health.response_time ?? 0 })}
)
}
return (
{t('proxyManager.unhealthy')}
)
}
if (result) {
return (
{result.success
? t('proxyManager.testSuccessShort', { time: result.response_time ?? 0 })
: t('proxyManager.testFailedShort')}
)
}
return (
{t('proxyManager.untested')}
)
}
function ProxiesTable({
t,
proxies,
testing,
testResults,
onCreate,
onTest,
onEdit,
onDelete,
onUnban,
unbanning,
onCheckAll,
checkingAll,
}) {
return (
{t('proxyManager.title')}
{t('proxyManager.desc')}
{proxies.length === 0 ? (
{t('proxyManager.noProxies')}
) : (
{proxies.map((proxy) => {
const result = testResults[proxy.id]
const isDisabled = proxy.disabled || false
const health = proxy.health || null
return (
{proxy.name || `${proxy.host}:${proxy.port}`}
{proxy.type}
{proxy.username && (
{proxy.username}
)}
{proxy.host}:{proxy.port}
{proxy.has_password && (
{t('proxyManager.authEnabled')}
)}
{health?.message && (
{health.message}
)}
{!health?.message && result?.message && (
{result.message}
)}
{health?.checked_at && (
{t('proxyManager.lastChecked', { time: new Date(health.checked_at).toLocaleTimeString() })}
)}
{isDisabled && (
)}
{!isDisabled && (
)}
)
})}
)}
)
}
function ProxyFormModal({
show,
t,
form,
setForm,
editingProxy,
loading,
onClose,
onSubmit,
}) {
if (!show) {
return null
}
const isEditing = Boolean(editingProxy?.id)
return (
{isEditing ? t('proxyManager.modalEditTitle') : t('proxyManager.modalAddTitle')}
{t('proxyManager.modalDesc')}
{t('proxyManager.typeHelp')}
)
}
export default function ProxyManagerContainer({ config, onRefresh, onMessage, authFetch }) {
const { t } = useI18n()
const apiFetch = authFetch || fetch
const [showModal, setShowModal] = useState(false)
const [editingProxy, setEditingProxy] = useState(null)
const [form, setForm] = useState(createEmptyProxyForm())
const [saving, setSaving] = useState(false)
const [testing, setTesting] = useState({})
const [testResults, setTestResults] = useState({})
const [proxyList, setProxyList] = useState([])
const [unbanning, setUnbanning] = useState({})
const [checkingAll, setCheckingAll] = useState(false)
const fetchProxyList = useCallback(async () => {
try {
const res = await apiFetch('/admin/proxies')
if (res.ok) {
const data = await res.json()
setProxyList(data.items || [])
}
} catch (_e) {
// Silently fail; the config-based fallback still works.
}
}, [apiFetch])
useEffect(() => {
fetchProxyList()
}, [fetchProxyList, config])
// Prefer the enriched proxy list (with health + disabled) from /admin/proxies.
// Fall back to config.proxies if the API call hasn't returned yet.
const proxies = proxyList.length > 0 ? proxyList : (config?.proxies || [])
const disabledCount = proxies.filter(p => p.disabled).length
const healthyCount = proxies.filter(p => p.health?.healthy && !p.disabled).length
const openCreate = () => {
setEditingProxy(null)
setForm(createEmptyProxyForm())
setShowModal(true)
}
const openEdit = (proxy) => {
setEditingProxy(proxy)
setForm({
name: proxy.name || '',
type: proxy.type || 'socks5h',
host: proxy.host || '',
port: proxy.port || 1080,
username: proxy.username || '',
password: '',
})
setShowModal(true)
}
const closeModal = () => {
setShowModal(false)
setEditingProxy(null)
setForm(createEmptyProxyForm())
}
const saveProxy = async () => {
if (!form.host || !form.port) {
onMessage('error', t('proxyManager.requiredFields'))
return
}
setSaving(true)
try {
const url = editingProxy?.id
? `/admin/proxies/${encodeURIComponent(editingProxy.id)}`
: '/admin/proxies'
const method = editingProxy?.id ? 'PUT' : 'POST'
const res = await apiFetch(url, {
method,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
name: form.name,
type: form.type,
host: form.host,
port: Number(form.port),
username: form.username,
password: form.password,
}),
})
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
if (!res.ok) {
onMessage('error', data.detail || t('messages.requestFailed'))
return
}
await onRefresh?.()
fetchProxyList()
onMessage('success', editingProxy?.id ? t('proxyManager.updateSuccess') : t('proxyManager.addSuccess'))
closeModal()
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
} finally {
setSaving(false)
}
}
const deleteProxy = async (proxy) => {
if (!confirm(t('proxyManager.deleteConfirm', { name: proxy.name || `${proxy.host}:${proxy.port}` }))) return
try {
const res = await apiFetch(`/admin/proxies/${encodeURIComponent(proxy.id)}`, { method: 'DELETE' })
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
if (!res.ok) {
onMessage('error', data.detail || t('messages.deleteFailed'))
return
}
await onRefresh?.()
fetchProxyList()
onMessage('success', t('messages.deleted'))
setTestResults(prev => {
const next = { ...prev }
delete next[proxy.id]
return next
})
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
}
}
const testProxy = async (proxy) => {
setTesting(prev => ({ ...prev, [proxy.id]: true }))
try {
const res = await apiFetch('/admin/proxies/test', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ proxy_id: proxy.id }),
})
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
setTestResults(prev => ({ ...prev, [proxy.id]: data }))
onMessage(data.success ? 'success' : 'error', data.message || t('messages.requestFailed'))
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
} finally {
setTesting(prev => ({ ...prev, [proxy.id]: false }))
}
}
const unbanProxy = async (proxy) => {
setUnbanning(prev => ({ ...prev, [proxy.id]: true }))
try {
const res = await apiFetch(`/admin/proxies/${encodeURIComponent(proxy.id)}/unban`, { method: 'POST' })
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
if (!res.ok) {
onMessage('error', data.detail || t('messages.requestFailed'))
return
}
await onRefresh?.()
fetchProxyList()
onMessage('success', t('proxyManager.unbanSuccess', { name: proxy.name || `${proxy.host}:${proxy.port}` }))
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
} finally {
setUnbanning(prev => ({ ...prev, [proxy.id]: false }))
}
}
const checkAllProxies = async () => {
setCheckingAll(true)
try {
const res = await apiFetch('/admin/proxies/check-all', { method: 'POST' })
const data = await readApiResponse(res, t('settings.nonJsonResponse', { status: res.status }))
if (!res.ok) {
onMessage('error', data.detail || t('messages.requestFailed'))
return
}
await onRefresh?.()
fetchProxyList()
const items = data.items || []
const healthy = items.filter(i => i.healthy && !i.disabled).length
const banned = items.filter(i => i.disabled).length
onMessage('success', t('proxyManager.checkAllResult', { healthy, banned, total: items.length }))
} catch (err) {
onMessage('error', err?.message || t('messages.networkError'))
} finally {
setCheckingAll(false)
}
}
return (
{t('proxyManager.totalProxies')}
{proxies.length}
{t('proxyManager.healthyCount')}
{healthyCount}
{t('proxyManager.bannedCount')}
{disabledCount}
{t('proxyManager.authProxyCount')}
{proxies.filter(proxy => proxy.username || proxy.has_password).length}
)
}