From 8a1943f84e1cb01d3e844803e291d2446ef531be Mon Sep 17 00:00:00 2001 From: zhom <2717306+zhom@users.noreply.github.com> Date: Tue, 25 Nov 2025 14:37:17 +0400 Subject: [PATCH] feat: add proxy check button --- package.json | 1 + pnpm-lock.yaml | 8 + src-tauri/Cargo.lock | 7 + src-tauri/Cargo.toml | 1 + src-tauri/src/lib.rs | 17 ++ src-tauri/src/proxy_manager.rs | 274 +++++++++++++++++++++ src/app/layout.tsx | 1 + src/components/flag-icon.tsx | 25 ++ src/components/profile-data-table.tsx | 201 ++++++++++----- src/components/proxy-check-button.tsx | 156 ++++++++++++ src/components/proxy-management-dialog.tsx | 52 +++- src/lib/flag-utils.ts | 34 +++ src/types.ts | 9 + 13 files changed, 717 insertions(+), 69 deletions(-) create mode 100644 src/components/flag-icon.tsx create mode 100644 src/components/proxy-check-button.tsx create mode 100644 src/lib/flag-utils.ts diff --git a/package.json b/package.json index a0064e0..c370b08 100644 --- a/package.json +++ b/package.json @@ -47,6 +47,7 @@ "clsx": "^2.1.1", "cmdk": "^1.1.1", "color": "^5.0.2", + "flag-icons": "^7.5.0", "motion": "^12.23.24", "next": "^15.5.6", "next-themes": "^0.4.6", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5708d37..58e602c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -77,6 +77,9 @@ importers: color: specifier: ^5.0.2 version: 5.0.2 + flag-icons: + specifier: ^7.5.0 + version: 7.5.0 motion: specifier: ^12.23.24 version: 12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -2117,6 +2120,9 @@ packages: resolution: {integrity: sha512-nynXZnqCBtBbEgqqdHS5mGm+R9JRRJxNun+lpZlCxGVt0BzgQJGibOvYCe5I54hIIVsaTldZ+jOb4btRPfPD6g==} engines: {node: '>=16.0.0'} + flag-icons@7.5.0: + resolution: {integrity: sha512-kd+MNXviFIg5hijH766tt+3x76ele1AXlo4zDdCxIvqWZhKt4T83bOtxUOOMlTx/EcFdUMH5yvQgYlFh1EqqFg==} + framer-motion@12.23.24: resolution: {integrity: sha512-HMi5HRoRCTou+3fb3h9oTLyJGBxHfW+HnNE25tAXOvVx/IvwMHK0cx7IR4a2ZU6sh3IX1Z+4ts32PcYBOqka8w==} peerDependencies: @@ -4756,6 +4762,8 @@ snapshots: header-generator: 2.1.76 tslib: 2.8.1 + flag-icons@7.5.0: {} + framer-motion@12.23.24(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: motion-dom: 12.23.23 diff --git a/src-tauri/Cargo.lock b/src-tauri/Cargo.lock index 5582c7f..94dce46 100644 --- a/src-tauri/Cargo.lock +++ b/src-tauri/Cargo.lock @@ -1125,6 +1125,7 @@ dependencies = [ "tower", "tower-http", "url", + "urlencoding", "uuid", "windows 0.62.2", "winreg", @@ -5487,6 +5488,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "urlpattern" version = "0.3.0" diff --git a/src-tauri/Cargo.toml b/src-tauri/Cargo.toml index 5863b7f..8f36b1b 100644 --- a/src-tauri/Cargo.toml +++ b/src-tauri/Cargo.toml @@ -48,6 +48,7 @@ msi-extract = "0" uuid = { version = "1.18", features = ["v4", "serde"] } url = "2.5" +urlencoding = "2.1" chrono = { version = "0.4", features = ["serde"] } axum = "0.8.4" tower = "0.5" diff --git a/src-tauri/src/lib.rs b/src-tauri/src/lib.rs index 24fff61..92f2724 100644 --- a/src-tauri/src/lib.rs +++ b/src-tauri/src/lib.rs @@ -221,6 +221,21 @@ async fn delete_stored_proxy(app_handle: tauri::AppHandle, proxy_id: String) -> .map_err(|e| format!("Failed to delete stored proxy: {e}")) } +#[tauri::command] +async fn check_proxy_validity( + proxy_id: String, + proxy_settings: crate::browser::ProxySettings, +) -> Result { + crate::proxy_manager::PROXY_MANAGER + .check_proxy_validity(&proxy_id, &proxy_settings) + .await +} + +#[tauri::command] +fn get_cached_proxy_check(proxy_id: String) -> Option { + crate::proxy_manager::PROXY_MANAGER.get_cached_proxy_check(&proxy_id) +} + #[tauri::command] async fn is_geoip_database_available() -> Result { Ok(GeoIPDownloader::is_geoip_database_available()) @@ -682,6 +697,8 @@ pub fn run() { get_stored_proxies, update_stored_proxy, delete_stored_proxy, + check_proxy_validity, + get_cached_proxy_check, update_camoufox_config, get_profile_groups, get_groups_with_profile_counts, diff --git a/src-tauri/src/proxy_manager.rs b/src-tauri/src/proxy_manager.rs index ff05882..a189ab2 100644 --- a/src-tauri/src/proxy_manager.rs +++ b/src-tauri/src/proxy_manager.rs @@ -5,6 +5,7 @@ use std::collections::HashMap; use std::fs; use std::path::PathBuf; use std::sync::Mutex; +use std::time::{SystemTime, UNIX_EPOCH}; use tauri::Emitter; use tauri_plugin_shell::ShellExt; @@ -23,6 +24,17 @@ pub struct ProxyInfo { pub profile_name: Option, } +// Proxy check result cache +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ProxyCheckResult { + pub ip: String, + pub city: Option, + pub country: Option, + pub country_code: Option, + pub timestamp: u64, + pub is_valid: bool, +} + // Stored proxy configuration with name and ID for reuse #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StoredProxy { @@ -91,6 +103,115 @@ impl ProxyManager { path } + // Get the path to the proxy check cache directory + fn get_proxy_check_cache_dir(&self) -> Result> { + let mut path = self.base_dirs.cache_dir().to_path_buf(); + path.push(if cfg!(debug_assertions) { + "DonutBrowserDev" + } else { + "DonutBrowser" + }); + path.push("proxy_checks"); + fs::create_dir_all(&path)?; + Ok(path) + } + + // Get the path to a specific proxy check cache file + fn get_proxy_check_cache_file( + &self, + proxy_id: &str, + ) -> Result> { + let cache_dir = self.get_proxy_check_cache_dir()?; + Ok(cache_dir.join(format!("{proxy_id}.json"))) + } + + // Load cached proxy check result + fn load_proxy_check_cache(&self, proxy_id: &str) -> Option { + let cache_file = match self.get_proxy_check_cache_file(proxy_id) { + Ok(file) => file, + Err(_) => return None, + }; + + if !cache_file.exists() { + return None; + } + + let content = match fs::read_to_string(&cache_file) { + Ok(content) => content, + Err(_) => return None, + }; + + serde_json::from_str::(&content).ok() + } + + // Save proxy check result to cache + fn save_proxy_check_cache( + &self, + proxy_id: &str, + result: &ProxyCheckResult, + ) -> Result<(), Box> { + let cache_file = self.get_proxy_check_cache_file(proxy_id)?; + let content = serde_json::to_string_pretty(result)?; + fs::write(&cache_file, content)?; + Ok(()) + } + + // Get current timestamp + fn get_current_timestamp() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap_or_default() + .as_secs() + } + + // Get geolocation for an IP address + async fn get_ip_geolocation( + ip: &str, + ) -> Result<(Option, Option, Option), String> { + // Use ip-api.com (free, no API key required) + let url = format!( + "http://ip-api.com/json/{}?fields=status,message,country,countryCode,city", + ip + ); + + let client = reqwest::Client::builder() + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| format!("Failed to create HTTP client: {e}"))?; + + match client.get(&url).send().await { + Ok(response) => { + if response.status().is_success() { + match response.json::().await { + Ok(json) => { + if json.get("status").and_then(|s| s.as_str()) == Some("success") { + let country = json + .get("country") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let country_code = json + .get("countryCode") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + let city = json + .get("city") + .and_then(|v| v.as_str()) + .map(|s| s.to_string()); + Ok((city, country, country_code)) + } else { + Ok((None, None, None)) + } + } + Err(e) => Err(format!("Failed to parse geolocation response: {e}")), + } + } else { + Ok((None, None, None)) + } + } + Err(e) => Err(format!("Failed to fetch geolocation: {e}")), + } + } + // Get the path to a specific proxy file fn get_proxy_file_path(&self, proxy_id: &str) -> PathBuf { self.get_proxies_dir().join(format!("{proxy_id}.json")) @@ -278,6 +399,159 @@ impl ProxyManager { .map(|p| p.proxy_settings.clone()) } + // Build proxy URL string from ProxySettings + fn build_proxy_url(proxy_settings: &ProxySettings) -> String { + let mut url = format!("{}://", proxy_settings.proxy_type); + + if let (Some(username), Some(password)) = (&proxy_settings.username, &proxy_settings.password) { + url.push_str(&urlencoding::encode(username)); + url.push(':'); + url.push_str(&urlencoding::encode(password)); + url.push('@'); + } else if let Some(username) = &proxy_settings.username { + url.push_str(&urlencoding::encode(username)); + url.push('@'); + } + + url.push_str(&proxy_settings.host); + url.push(':'); + url.push_str(&proxy_settings.port.to_string()); + + url + } + + // Validate IP address (IPv4 or IPv6) + fn validate_ip(ip: &str) -> bool { + // IPv4 validation + if ip.matches('.').count() == 3 { + let parts: Vec<&str> = ip.split('.').collect(); + if parts.len() == 4 { + return parts.iter().all(|part| part.parse::().is_ok()); + } + } + + // IPv6 validation (simplified - checks for colons and hex digits) + if ip.matches(':').count() >= 2 { + let parts: Vec<&str> = ip.split(':').collect(); + return parts + .iter() + .all(|part| part.is_empty() || part.chars().all(|c| c.is_ascii_hexdigit())); + } + + false + } + + // Check if a proxy is valid by making HTTP requests through it + pub async fn check_proxy_validity( + &self, + proxy_id: &str, + proxy_settings: &ProxySettings, + ) -> Result { + let proxy_url = Self::build_proxy_url(proxy_settings); + + // List of IP check endpoints to try + let ip_check_urls = vec![ + "https://api.ipify.org", + "https://checkip.amazonaws.com", + "https://ipinfo.io/ip", + "https://icanhazip.com", + "https://ifconfig.co/ip", + "https://ipecho.net/plain", + ]; + + // Create HTTP client with proxy + // reqwest::Proxy::all expects http/https URLs, but we need to handle socks proxies differently + let proxy = match proxy_settings.proxy_type.as_str() { + "socks4" | "socks5" => { + // For SOCKS proxies, reqwest doesn't support them directly via Proxy::all + // We'll need to use a different approach or return an error + return Err("SOCKS proxy validation not yet supported".to_string()); + } + _ => reqwest::Proxy::all(&proxy_url).map_err(|e| format!("Failed to create proxy: {e}"))?, + }; + + let client = reqwest::Client::builder() + .proxy(proxy) + .timeout(std::time::Duration::from_secs(5)) + .build() + .map_err(|e| format!("Failed to create HTTP client: {e}"))?; + + // Try each endpoint until one succeeds + let mut last_error = None; + let mut ip: Option = None; + + for url_str in ip_check_urls { + match client.get(url_str).send().await { + Ok(response) => { + if response.status().is_success() { + match response.text().await { + Ok(ip_text) => { + let ip_str = ip_text.trim(); + if Self::validate_ip(ip_str) { + ip = Some(ip_str.to_string()); + break; + } else { + last_error = Some(format!("Invalid IP address returned: {ip_str}")); + } + } + Err(e) => { + last_error = Some(format!("Failed to read response from {url_str}: {e}")); + } + } + } else { + last_error = Some(format!("HTTP error from {url_str}: {}", response.status())); + } + } + Err(e) => { + last_error = Some(format!("Request to {url_str} failed: {e}")); + } + } + } + + let ip = match ip { + Some(ip) => ip, + None => { + // Save failed check result + let failed_result = ProxyCheckResult { + ip: String::new(), + city: None, + country: None, + country_code: None, + timestamp: Self::get_current_timestamp(), + is_valid: false, + }; + let _ = self.save_proxy_check_cache(proxy_id, &failed_result); + return Err( + last_error.unwrap_or_else(|| "Failed to get public IP from any endpoint".to_string()), + ); + } + }; + + // Get geolocation + let (city, country, country_code): (Option, Option, Option) = + Self::get_ip_geolocation(&ip).await.unwrap_or_default(); + + // Create successful result + let result = ProxyCheckResult { + ip: ip.clone(), + city, + country, + country_code, + timestamp: Self::get_current_timestamp(), + is_valid: true, + }; + + // Save to cache + let _ = self.save_proxy_check_cache(proxy_id, &result); + + Ok(result) + } + + // Get cached proxy check result + pub fn get_cached_proxy_check(&self, proxy_id: &str) -> Option { + self.load_proxy_check_cache(proxy_id) + } + // Start a proxy for given proxy settings and associate it with a browser process ID // If proxy_settings is None, starts a direct proxy for traffic monitoring pub async fn start_proxy( diff --git a/src/app/layout.tsx b/src/app/layout.tsx index 34fe7cc..a4a5a44 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -1,6 +1,7 @@ "use client"; import { Geist, Geist_Mono } from "next/font/google"; import "@/styles/globals.css"; +import "flag-icons/css/flag-icons.min.css"; import { CustomThemeProvider } from "@/components/theme-provider"; import { Toaster } from "@/components/ui/sonner"; import { TooltipProvider } from "@/components/ui/tooltip"; diff --git a/src/components/flag-icon.tsx b/src/components/flag-icon.tsx new file mode 100644 index 0000000..fb180c3 --- /dev/null +++ b/src/components/flag-icon.tsx @@ -0,0 +1,25 @@ +import { getFlagIconClass } from "@/lib/flag-utils"; +import { cn } from "@/lib/utils"; + +interface FlagIconProps { + countryCode?: string; + className?: string; + squared?: boolean; +} + +export function FlagIcon({ + countryCode, + className, + squared = false, +}: FlagIconProps) { + if (!countryCode) { + return null; + } + + const flagClass = getFlagIconClass(countryCode); + if (!flagClass) { + return null; + } + + return ; +} diff --git a/src/components/profile-data-table.tsx b/src/components/profile-data-table.tsx index b44a73c..15166a2 100644 --- a/src/components/profile-data-table.tsx +++ b/src/components/profile-data-table.tsx @@ -61,9 +61,10 @@ import { } from "@/lib/browser-utils"; import { trimName } from "@/lib/name-utils"; import { cn } from "@/lib/utils"; -import type { BrowserProfile, StoredProxy } from "@/types"; +import type { BrowserProfile, ProxyCheckResult, StoredProxy } from "@/types"; import { LoadingButton } from "./loading-button"; import MultipleSelector, { type Option } from "./multiple-selector"; +import { ProxyCheckButton } from "./proxy-check-button"; import { Input } from "./ui/input"; import { RippleButton } from "./ui/ripple"; @@ -99,6 +100,8 @@ type TableMeta = { profileId: string, proxyId: string | null, ) => void | Promise; + checkingProxyId: string | null; + proxyCheckResults: Record; // Selection helpers isProfileSelected: (id: string) => boolean; @@ -437,6 +440,42 @@ export function ProfilesDataTable({ const [openProxySelectorFor, setOpenProxySelectorFor] = React.useState< string | null >(null); + const [checkingProxyId, setCheckingProxyId] = React.useState( + null, + ); + const [proxyCheckResults, setProxyCheckResults] = React.useState< + Record + >({}); + + // Load cached check results for proxies + React.useEffect(() => { + const loadCachedResults = async () => { + const results: Record = {}; + const proxyIds = new Set(); + for (const profile of profiles) { + if (profile.proxy_id) { + proxyIds.add(profile.proxy_id); + } + } + for (const proxyId of proxyIds) { + try { + const cached = await invoke( + "get_cached_proxy_check", + { proxyId }, + ); + if (cached) { + results[proxyId] = cached; + } + } catch (_error) { + // Ignore errors + } + } + setProxyCheckResults(results); + }; + if (profiles.length > 0) { + void loadCachedResults(); + } + }, [profiles]); const loadAllTags = React.useCallback(async () => { try { @@ -779,6 +818,8 @@ export function ProfilesDataTable({ proxyOverrides, storedProxies, handleProxySelection, + checkingProxyId, + proxyCheckResults, // Selection helpers isProfileSelected: (id: string) => selectedProfiles.includes(id), @@ -823,6 +864,8 @@ export function ProfilesDataTable({ proxyOverrides, storedProxies, handleProxySelection, + checkingProxyId, + proxyCheckResults, handleToggleAll, handleCheckboxChange, handleIconClick, @@ -1275,90 +1318,114 @@ export function ProfilesDataTable({ } return ( - - meta.setOpenProxySelectorFor(open ? profile.id : null) - } - > - - - - +
+ + meta.setOpenProxySelectorFor(open ? profile.id : null) + } + > + + + - {profileHasProxy - ? trimName(displayName, 10) - : displayName} - - - - - {tooltipText && {tooltipText}} - - - {!isDisabled && ( - - - - - No proxies found. - - - void meta.handleProxySelection(profile.id, null) - } + - - No Proxy - - {meta.storedProxies.map((proxy) => ( + {profileHasProxy + ? trimName(displayName, 10) + : displayName} + + + + + {tooltipText && ( + {tooltipText} + )} + + + {!isDisabled && ( + + + + + No proxies found. + - void meta.handleProxySelection( - profile.id, - proxy.id, - ) + void meta.handleProxySelection(profile.id, null) } > - {proxy.name} + No Proxy - ))} - - - - + {meta.storedProxies.map((proxy) => ( + + void meta.handleProxySelection( + profile.id, + proxy.id, + ) + } + > + + {proxy.name} + + ))} + + + + + )} + + {profileHasProxy && effectiveProxy && !isDisabled && ( + { + setProxyCheckResults((prev) => ({ + ...prev, + [effectiveProxy.id]: result, + })); + }} + onCheckFailed={(result) => { + setProxyCheckResults((prev) => ({ + ...prev, + [effectiveProxy.id]: result, + })); + }} + /> )} - +
); }, }, diff --git a/src/components/proxy-check-button.tsx b/src/components/proxy-check-button.tsx new file mode 100644 index 0000000..aae5cba --- /dev/null +++ b/src/components/proxy-check-button.tsx @@ -0,0 +1,156 @@ +"use client"; + +import { invoke } from "@tauri-apps/api/core"; +import * as React from "react"; +import { FiCheck } from "react-icons/fi"; +import { toast } from "sonner"; +import { FlagIcon } from "@/components/flag-icon"; +import { Button } from "@/components/ui/button"; +import { + Tooltip, + TooltipContent, + TooltipTrigger, +} from "@/components/ui/tooltip"; +import { formatRelativeTime } from "@/lib/flag-utils"; +import type { ProxyCheckResult, StoredProxy } from "@/types"; + +interface ProxyCheckButtonProps { + proxy: StoredProxy; + checkingProxyId: string | null; + cachedResult?: ProxyCheckResult; + onCheckComplete?: (result: ProxyCheckResult) => void; + onCheckFailed?: (result: ProxyCheckResult) => void; + disabled?: boolean; + setCheckingProxyId?: (id: string | null) => void; +} + +export function ProxyCheckButton({ + proxy, + checkingProxyId, + cachedResult, + onCheckComplete, + onCheckFailed, + disabled = false, + setCheckingProxyId, +}: ProxyCheckButtonProps) { + const [localResult, setLocalResult] = React.useState< + ProxyCheckResult | undefined + >(cachedResult); + + React.useEffect(() => { + setLocalResult(cachedResult); + }, [cachedResult]); + + const handleCheck = React.useCallback(async () => { + if (checkingProxyId === proxy.id) return; + + setCheckingProxyId?.(proxy.id); + try { + const result = await invoke("check_proxy_validity", { + proxyId: proxy.id, + proxySettings: proxy.proxy_settings, + }); + setLocalResult(result); + onCheckComplete?.(result); + + // Show toast with location + const locationParts: string[] = []; + if (result.city) locationParts.push(result.city); + if (result.country) locationParts.push(result.country); + const location = + locationParts.length > 0 ? locationParts.join(", ") : "Unknown"; + + toast.success( +
+ Your proxy location is: + {location} + {result.country_code && ( + + )} +
, + ); + } catch (error) { + const errorMessage = + error instanceof Error ? error.message : String(error); + toast.error(`Proxy check failed: ${errorMessage}`); + + // Save failed check result + const failedResult: ProxyCheckResult = { + ip: "", + city: undefined, + country: undefined, + country_code: undefined, + timestamp: Math.floor(Date.now() / 1000), + is_valid: false, + }; + setLocalResult(failedResult); + onCheckFailed?.(failedResult); + } finally { + setCheckingProxyId?.(null); + } + }, [ + proxy, + checkingProxyId, + onCheckComplete, + onCheckFailed, + setCheckingProxyId, + ]); + + const isCurrentlyChecking = checkingProxyId === proxy.id; + const result = localResult; + + return ( + + + + + + {isCurrentlyChecking ? ( +

Checking proxy...

+ ) : result?.is_valid ? ( +
+

+ {result.country_code && ( + + )} + {[result.city, result.country].filter(Boolean).join(", ") || + "Unknown"} +

+

IP: {result.ip}

+

+ Checked {formatRelativeTime(result.timestamp)} +

+
+ ) : result && !result.is_valid ? ( +
+

Proxy check failed

+

+ Failed {formatRelativeTime(result.timestamp)} +

+
+ ) : ( +

Check proxy validity

+ )} +
+
+ ); +} diff --git a/src/components/proxy-management-dialog.tsx b/src/components/proxy-management-dialog.tsx index 20f3b95..b9099c2 100644 --- a/src/components/proxy-management-dialog.tsx +++ b/src/components/proxy-management-dialog.tsx @@ -2,6 +2,7 @@ import { invoke } from "@tauri-apps/api/core"; import { emit } from "@tauri-apps/api/event"; +import * as React from "react"; import { useCallback, useState } from "react"; import { FiEdit2, FiPlus, FiTrash2, FiWifi } from "react-icons/fi"; import { toast } from "sonner"; @@ -24,7 +25,8 @@ import { } from "@/components/ui/tooltip"; import { useProxyEvents } from "@/hooks/use-proxy-events"; import { trimName } from "@/lib/name-utils"; -import type { StoredProxy } from "@/types"; +import type { ProxyCheckResult, StoredProxy } from "@/types"; +import { ProxyCheckButton } from "./proxy-check-button"; import { RippleButton } from "./ui/ripple"; interface ProxyManagementDialogProps { @@ -40,9 +42,37 @@ export function ProxyManagementDialog({ const [editingProxy, setEditingProxy] = useState(null); const [proxyToDelete, setProxyToDelete] = useState(null); const [isDeleting, setIsDeleting] = useState(false); + const [checkingProxyId, setCheckingProxyId] = useState(null); + const [proxyCheckResults, setProxyCheckResults] = useState< + Record + >({}); const { storedProxies, proxyUsage, isLoading } = useProxyEvents(); + // Load cached check results on mount and when proxies change + React.useEffect(() => { + const loadCachedResults = async () => { + const results: Record = {}; + for (const proxy of storedProxies) { + try { + const cached = await invoke( + "get_cached_proxy_check", + { proxyId: proxy.id }, + ); + if (cached) { + results[proxy.id] = cached; + } + } catch (_error) { + // Ignore errors + } + } + setProxyCheckResults(results); + }; + if (storedProxies.length > 0) { + void loadCachedResults(); + } + }, [storedProxies]); + const handleDeleteProxy = useCallback((proxy: StoredProxy) => { // Open in-app confirmation dialog setProxyToDelete(proxy); @@ -163,7 +193,25 @@ export function ProxyManagementDialog({ {proxyUsage[proxy.id] ?? 0} -
+
+ { + setProxyCheckResults((prev) => ({ + ...prev, + [proxy.id]: result, + })); + }} + onCheckFailed={(result) => { + setProxyCheckResults((prev) => ({ + ...prev, + [proxy.id]: result, + })); + }} + />