import React from "react"; import { Activity, ChevronDown, Download, Loader2, Pencil, Play, Plus, Save, Square, Trash2, } from "lucide-react"; import { toast } from "sonner"; import { useHosts, useTranslation } from "@termix/plugin-sdk/frontend"; import { Button, FakeSwitch, Input, Select2 } from "@termix/plugin-sdk/ui"; import type { C2STunnelPreset, TunnelConnection, TunnelMode, TunnelStatus, } from "../shared/types"; import { clientTunnelName } from "../shared/tunnel-naming"; import { getTunnelModeDescription, getTunnelPortLabels, getTunnelTypeForMode, } from "./tunnel-form-utils"; import { getTunnelMode, normalizeClientTunnel, stripClientTunnelDiagnostics, type ClientTunnel, } from "./c2s-tunnel-config-utils"; import { apiErrorMessage, createC2STunnelPreset, deleteC2STunnelPreset, getC2STunnelPresets, updateC2STunnelPreset, } from "./api"; interface SshHostOption { id: number; name: string; syncId?: string | null; connectionType?: string | null; } function getErrorMessage(error: unknown, fallback: string): string { return error instanceof Error && error.message ? error.message : fallback; } function sortPresets(presets: C2STunnelPreset[]) { return [...presets].sort((a, b) => a.name.localeCompare(b.name)); } function sameConfig(a: TunnelConnection[], b: TunnelConnection[]) { return JSON.stringify(a) === JSON.stringify(b); } function isValidIPv4(value: string) { const parts = value.split("."); return ( parts.length === 4 && parts.every((part) => { if (!/^\d+$/.test(part)) return false; const value = Number(part); return value >= 0 && value <= 255; }) ); } function isValidPort(value: unknown) { const port = Number(value); return Number.isInteger(port) && port >= 1 && port <= 65535; } function getEffectiveAddress(address?: string) { const trimmedAddress = address?.trim(); return trimmedAddress || "127.0.0.1"; } function formatDateTime(value?: string) { if (!value) return ""; const date = new Date(value); if (Number.isNaN(date.getTime())) return ""; return date.toLocaleString(); } function createClientTunnel(): ClientTunnel { return { scope: "c2s", mode: "local", tunnelType: "local", localAddress: "", remoteAddress: "", bindHost: "", sourcePort: 8080, endpointPort: 22, endpointHost: "", maxRetries: 3, retryInterval: 10, autoStart: false, }; } function getStatusKind(status?: TunnelStatus) { const value = status?.status?.toUpperCase() || "DISCONNECTED"; if (value === "CONNECTED") return "connected"; if (value === "ERROR" || value === "FAILED") return "error"; if ( value === "CONNECTING" || value === "DISCONNECTING" || value === "RETRYING" || value === "WAITING" ) { return "connecting"; } return "disconnected"; } function getStatusTitle( status: TunnelStatus | undefined, statusText: string, t: ReturnType["t"], ) { if (!status) return statusText; const details = []; if (status.reason) details.push(status.reason); if (status.retryCount && status.maxRetries) { details.push( t("tunnels.attempt", { current: status.retryCount, max: status.maxRetries, }), ); } if (status.nextRetryIn) { details.push(t("tunnels.nextRetryIn", { seconds: status.nextRetryIn })); } if (status.errorType && !status.reason) details.push(status.errorType); return details.length > 0 ? details.join("\n") : statusText; } export function C2STunnelPresetManager(): React.ReactElement { const { t } = useTranslation(); const [localConfig, setLocalConfig] = React.useState([]); const [savedLocalConfig, setSavedLocalConfig] = React.useState< ClientTunnel[] >([]); const { hosts: pluginHosts, loaded: hostsLoaded } = useHosts(); const hosts = React.useMemo( () => pluginHosts.map((host) => ({ id: Number(host.id), name: host.name, syncId: host.syncId as string | null | undefined, connectionType: host.connectionType as string | null | undefined, })), [pluginHosts], ); const hostsRef = React.useRef(hosts); hostsRef.current = hosts; const [presets, setPresets] = React.useState([]); const [tunnelStatuses, setTunnelStatuses] = React.useState< Record >({}); const [tunnelActions, setTunnelActions] = React.useState< Record >({}); const [tunnelTests, setTunnelTests] = React.useState>( {}, ); const previousTunnelStatusesRef = React.useRef>( {}, ); const [selectedPresetId, setSelectedPresetId] = React.useState(""); const [presetName, setPresetName] = React.useState(""); const [openTunnels, setOpenTunnels] = React.useState>(new Set()); const isElectron = typeof window !== "undefined" && window.electronAPI?.isElectron === true; const handleC2SRemoteAuthError = React.useCallback( (result: { code?: string; error?: string }) => { const error = result.error || ""; const isExpired = result.code === "REMOTE_SESSION_EXPIRED" || result.code === "SESSION_EXPIRED" || error === "Termix session expired. Please log in again." || error === "Remote Termix session expired. Reconnect Remote Sync and try again." || error === "Authentication required"; if (!isExpired) return false; toast.warning(t("tunnels.remoteSessionExpired")); return true; }, [t], ); const sshHosts = React.useMemo( () => hosts.filter( (host) => host.id && (host.connectionType || "ssh") === "ssh", ), [hosts], ); const selectedPreset = React.useMemo( () => presets.find((preset) => String(preset.id) === selectedPresetId) || null, [presets, selectedPresetId], ); const selectedMatchesCurrent = React.useMemo(() => { return selectedPreset ? sameConfig( selectedPreset.config.map(normalizeClientTunnel), localConfig.map(stripClientTunnelDiagnostics), ) : false; }, [localConfig, selectedPreset]); const hasUnsavedLocalChanges = React.useMemo( () => !sameConfig(savedLocalConfig, localConfig), [savedLocalConfig, localConfig], ); const hasPresets = presets.length > 0; function toggleTunnel(index: number) { setOpenTunnels((current) => { const next = new Set(current); if (next.has(index)) { next.delete(index); } else { next.add(index); } return next; }); } const getTunnelName = clientTunnelName; const getEndpointName = React.useCallback( (tunnel: ClientTunnel) => { const host = sshHosts.find((item) => item.id === tunnel.sourceHostId); return ( tunnel.sourceHostName || host?.name || tunnel.endpointHost || t("tunnels.endpointSshHost") ); }, [sshHosts, t], ); const getTunnelDisplayName = React.useCallback( (tunnel: ClientTunnel, index: number) => { if (tunnel.displayName?.trim()) return tunnel.displayName.trim(); const mode = getTunnelMode(tunnel); const endpointName = getEndpointName(tunnel); if (mode === "remote") { return t("tunnels.autoNameClientRemote", { endpoint: endpointName, remotePort: tunnel.sourcePort, localPort: tunnel.endpointPort, }); } if (mode === "dynamic") { return t("tunnels.autoNameClientDynamic", { localPort: tunnel.sourcePort, endpoint: endpointName, }); } return t("tunnels.autoNameClientLocal", { localPort: tunnel.sourcePort, endpoint: endpointName, remotePort: tunnel.endpointPort, index: index + 1, }); }, [getEndpointName, t], ); const withSourceHostIdentity = React.useCallback( (tunnel: ClientTunnel): ClientTunnel => { const normalized = normalizeClientTunnel(tunnel); const host = sshHosts.find( (item) => item.id === normalized.sourceHostId || (normalized.sourceHostSyncId && item.syncId === normalized.sourceHostSyncId), ); if (!host) return normalized; return normalizeClientTunnel({ ...normalized, sourceHostId: host.id, sourceHostSyncId: host.syncId || normalized.sourceHostSyncId, sourceHostName: host.name || normalized.sourceHostName, endpointHost: host.name || normalized.endpointHost, }); }, [sshHosts], ); const getLocalAddressPlaceholder = React.useCallback( (mode: string) => { if (mode === "remote") return t("placeholders.localTargetHost"); if (mode === "dynamic") return t("placeholders.socksListenerHost"); return t("placeholders.localListenerHost"); }, [t], ); const getRemoteAddressPlaceholder = React.useCallback( (mode: string) => { if (mode === "remote") return t("placeholders.remoteBindHost"); return t("placeholders.remoteTargetHost"); }, [t], ); const getRemoteAddressLabel = React.useCallback( (mode: string) => { if (mode === "remote") return t("tunnels.remoteBindAddress"); return t("tunnels.destinationAddress"); }, [t], ); const getTunnelSummary = React.useCallback( (tunnel: ClientTunnel) => { const mode = getTunnelMode(tunnel); const localAddress = getEffectiveAddress(tunnel.localAddress); const remoteAddress = getEffectiveAddress(tunnel.remoteAddress); const endpointName = getEndpointName(tunnel); if (mode === "remote") { return t("tunnels.summaryClientRemote", { endpoint: endpointName, remoteHost: remoteAddress, remotePort: tunnel.sourcePort, localHost: localAddress, localPort: tunnel.endpointPort, }); } if (mode === "dynamic") { return t("tunnels.summaryClientDynamic", { localHost: localAddress, localPort: tunnel.sourcePort, endpoint: endpointName, }); } return t("tunnels.summaryClientLocal", { localHost: localAddress, localPort: tunnel.sourcePort, endpoint: endpointName, remoteHost: remoteAddress, remotePort: tunnel.endpointPort, }); }, [getEndpointName, t], ); const refreshPresets = React.useCallback(async () => { const nextPresets = await getC2STunnelPresets(); setPresets(sortPresets(nextPresets)); }, []); const refreshLocalConfig = React.useCallback(async () => { if (!isElectron) return; const [config, defaultName] = await Promise.all([ window.electronAPI.getC2STunnelConfig(), window.electronAPI.getC2STunnelPresetDefaultName(), ]); const nextHosts = hostsRef.current; const normalizedConfig = Array.isArray(config) ? (config as TunnelConnection[]) .filter((tunnel) => tunnel.scope === "c2s") .map((tunnel) => { const normalized = normalizeClientTunnel(tunnel); const host = nextHosts.find((item) => normalized.sourceHostSyncId ? item.syncId === normalized.sourceHostSyncId : item.id === normalized.sourceHostId, ); if (!host) return normalized; return normalizeClientTunnel({ ...normalized, sourceHostId: host.id, sourceHostSyncId: host.syncId || normalized.sourceHostSyncId, sourceHostName: host.name || normalized.sourceHostName, endpointHost: host.name || normalized.endpointHost, }); }) : []; setLocalConfig(normalizedConfig); setSavedLocalConfig(normalizedConfig); setPresetName((current) => current || defaultName); }, [isElectron]); React.useEffect(() => { if (!isElectron || !hostsLoaded) return; Promise.all([refreshLocalConfig(), refreshPresets()]).catch(() => { setPresets([]); }); }, [isElectron, hostsLoaded, refreshLocalConfig, refreshPresets]); React.useEffect(() => { if (!isElectron) return; const refreshStatuses = async () => { const statuses = await window.electronAPI.getC2STunnelStatuses(); setTunnelStatuses(statuses as Record); }; refreshStatuses().catch(() => {}); const unsubscribe = window.electronAPI.onC2STunnelStatuses?.((statuses) => { setTunnelStatuses(statuses as Record); }); return () => unsubscribe?.(); }, [isElectron]); React.useEffect(() => { const previousStatuses = previousTunnelStatusesRef.current; for (const [tunnelName, status] of Object.entries(tunnelStatuses)) { const previous = previousStatuses[tunnelName]; const statusChanged = previous?.status !== status.status || previous?.reason !== status.reason || previous?.retryCount !== status.retryCount; if (!statusChanged) continue; const statusValue = status.status?.toUpperCase(); const hasFailureDetail = statusValue === "ERROR" || statusValue === "FAILED" || (Boolean(status.errorType) && Boolean(status.reason) && previous?.reason !== status.reason); if (hasFailureDetail) { const message = status.reason || t("tunnels.manualControlError"); if (handleC2SRemoteAuthError({ error: message })) { continue; } toast.error(message, { id: `client-tunnel-error-${tunnelName}` }); } } previousTunnelStatusesRef.current = tunnelStatuses; }, [handleC2SRemoteAuthError, t, tunnelStatuses]); const validateLocalConfig = (config: ClientTunnel[]) => { const autoStartListeners = new Set(); for (const tunnel of config) { const localAddress = getEffectiveAddress(tunnel.localAddress); const remoteAddress = getEffectiveAddress(tunnel.remoteAddress); const mode = getTunnelMode(tunnel); if (!isValidIPv4(localAddress)) { return mode === "remote" ? t("tunnels.invalidLocalTargetIp") : t("tunnels.invalidBindIp"); } if (mode !== "dynamic" && !remoteAddress) { return t("tunnels.invalidRemoteAddress"); } if (!isValidPort(tunnel.sourcePort)) { return mode === "remote" ? t("tunnels.invalidRemotePort") : t("tunnels.invalidLocalPort"); } if (mode !== "dynamic" && !isValidPort(tunnel.endpointPort)) { return mode === "remote" ? t("tunnels.invalidLocalTargetPort") : t("tunnels.invalidEndpointPort"); } if (!tunnel.sourceHostId) { return t("tunnels.endpointSshHostRequired"); } if (tunnel.autoStart) { const listenerKey = mode === "remote" ? `${tunnel.sourceHostId}:${remoteAddress}:${tunnel.sourcePort}` : `${localAddress}:${tunnel.sourcePort}`; if (autoStartListeners.has(listenerKey)) { return t("tunnels.duplicateAutoStartBind", { bind: mode === "remote" ? `${tunnel.sourceHostName || tunnel.sourceHostId}:${remoteAddress}:${tunnel.sourcePort}` : listenerKey, }); } autoStartListeners.add(listenerKey); } } return null; }; const saveLocalConfig = async (config: ClientTunnel[]) => { const normalizedConfig = config.map((tunnel) => withSourceHostIdentity(tunnel), ); const validationError = validateLocalConfig(normalizedConfig); if (validationError) throw new Error(validationError); const result = await window.electronAPI.saveC2STunnelConfig(normalizedConfig); if (!result.success) throw new Error(result.error || t("tunnels.localSaveError")); setLocalConfig(normalizedConfig); setSavedLocalConfig(normalizedConfig); }; const updateTunnel = ( index: number, updates: Partial, ): void => { setLocalConfig((current) => current.map((tunnel, tunnelIndex) => tunnelIndex === index ? normalizeClientTunnel({ ...tunnel, ...updates }) : tunnel, ), ); }; const handleEndpointChange = (index: number, hostId: string) => { const host = sshHosts.find((item) => String(item.id) === hostId); if (!host) return; updateTunnel(index, { sourceHostId: host.id, sourceHostSyncId: host.syncId || undefined, sourceHostName: host.name, endpointHost: host.name, endpointPort: 22, }); }; const handleSaveLocal = async () => { try { await saveLocalConfig(localConfig); toast.success(t("tunnels.localSaved")); } catch (error) { toast.error(getErrorMessage(error, t("tunnels.localSaveError"))); } }; const setTunnelMetadata = ( index: number, updates: Partial, ): void => { setLocalConfig((current) => current.map((tunnel, tunnelIndex) => tunnelIndex === index ? normalizeClientTunnel({ ...tunnel, ...updates }) : tunnel, ), ); }; const handleTunnelTest = async (tunnel: ClientTunnel, index: number) => { const tunnelName = getTunnelName(tunnel, index); const normalizedTunnel = { ...withSourceHostIdentity(tunnel), name: tunnelName, }; const validationError = validateLocalConfig([normalizedTunnel]); if (validationError) { toast.error(validationError); setTunnelMetadata(index, { lastError: validationError }); return; } setTunnelTests((current) => ({ ...current, [tunnelName]: true })); try { const result = await window.electronAPI.testC2STunnel( normalizedTunnel, index, ); if (!result.success && handleC2SRemoteAuthError(result)) { return; } if (!result.success) throw new Error(result.error || t("tunnels.tunnelTestFailed")); setTunnelMetadata(index, { lastTestedAt: new Date().toISOString(), lastError: "", }); toast.success(t("tunnels.tunnelTestSucceeded")); } catch (error) { const message = getErrorMessage(error, t("tunnels.tunnelTestFailed")); setTunnelMetadata(index, { lastError: message }); toast.error(message); } finally { setTunnelTests((current) => ({ ...current, [tunnelName]: false })); } }; const handleTunnelStart = async (tunnel: ClientTunnel, index: number) => { const tunnelName = getTunnelName(tunnel, index); const normalizedTunnel = { ...withSourceHostIdentity(tunnel), name: tunnelName, }; const validationError = validateLocalConfig([normalizedTunnel]); if (validationError) { toast.error(validationError); setTunnelMetadata(index, { lastError: validationError }); return; } setTunnelActions((current) => ({ ...current, [tunnelName]: true })); try { const result = await window.electronAPI.startC2STunnel( normalizedTunnel, index, ); if (!result.success && handleC2SRemoteAuthError(result)) { return; } if (!result.success) throw new Error(result.error || t("tunnels.manualControlError")); const statuses = await window.electronAPI.getC2STunnelStatuses(); setTunnelStatuses(statuses as Record); setTunnelMetadata(index, { lastStartedAt: new Date().toISOString(), lastError: "", }); toast.success(t("tunnels.clientTunnelStarted")); } catch (error) { const message = error instanceof Error ? error.message : t("tunnels.manualControlError"); setTunnelMetadata(index, { lastError: message }); toast.error(message); } finally { setTunnelActions((current) => ({ ...current, [tunnelName]: false })); } }; const handleTunnelStop = async (tunnel: ClientTunnel, index: number) => { const tunnelName = getTunnelName(tunnel, index); setTunnelActions((current) => ({ ...current, [tunnelName]: true })); try { const result = await window.electronAPI.stopC2STunnel(tunnelName); if (!result.success) throw new Error(result.error || t("tunnels.manualControlError")); const statuses = await window.electronAPI.getC2STunnelStatuses(); setTunnelStatuses(statuses as Record); toast.success(t("tunnels.clientTunnelStopped")); } catch (error) { toast.error( error instanceof Error ? error.message : t("tunnels.manualControlError"), ); } finally { setTunnelActions((current) => ({ ...current, [tunnelName]: false })); } }; const handleSavePreset = async () => { if (!presetName.trim()) return; try { const normalizedConfig = localConfig.map((tunnel) => withSourceHostIdentity(tunnel), ); await saveLocalConfig(normalizedConfig); await createC2STunnelPreset({ name: presetName.trim(), config: normalizedConfig.map(stripClientTunnelDiagnostics), }); await refreshPresets(); toast.success(t("profile.c2sPresetSaved")); } catch (error) { toast.error( apiErrorMessage( error, getErrorMessage(error, t("tunnels.localSaveError")), ), ); } }; const handleLoadPreset = async () => { if (!selectedPreset || selectedMatchesCurrent) return; try { await saveLocalConfig(selectedPreset.config.map(normalizeClientTunnel)); toast.success(t("profile.c2sPresetLoaded")); } catch (error) { toast.error( error instanceof Error ? error.message : t("profile.c2sPresetLoadError"), ); } }; const handleRenamePreset = async () => { if (!selectedPreset || !presetName.trim()) return; try { await updateC2STunnelPreset(selectedPreset.id, { name: presetName.trim(), }); await refreshPresets(); toast.success(t("profile.c2sPresetRenamed")); } catch (error) { toast.error(apiErrorMessage(error, t("profile.c2sPresetRenameError"))); } }; const handleDeletePreset = async () => { if (!selectedPreset) return; try { await deleteC2STunnelPreset(selectedPreset.id); setSelectedPresetId(""); await refreshPresets(); toast.success(t("profile.c2sPresetDeleted")); } catch (error) { toast.error(apiErrorMessage(error, t("profile.c2sPresetDeleteError"))); } }; if (!isElectron) { return (

{t("profile.c2sTunnelPresetsUnavailable")}

); } return (
{/* Tunnel list header */}
{t("tunnels.clientTunnels")}
{hasUnsavedLocalChanges && ( {t("common.unsavedChanges")} )}
{/* Tunnel items */}
{localConfig.length === 0 ? (
{t("tunnels.noClientTunnels")}
) : ( localConfig.map((tunnel, index) => { const mode = getTunnelMode(tunnel); const tunnelName = getTunnelName(tunnel, index); const tunnelStatus = tunnelStatuses[tunnelName]; const isTunnelActionLoading = Boolean(tunnelActions[tunnelName]); const isTunnelTestLoading = Boolean(tunnelTests[tunnelName]); const startDisabled = !tunnel.sourceHostId; const { sourcePortLabel, endpointPortLabel } = getTunnelPortLabels( "client", mode, t, ); const tunnelSummary = getTunnelSummary(tunnel); const modeDescription = getTunnelModeDescription( "client", mode, { sourcePort: tunnel.sourcePort, endpointPort: tunnel.endpointPort, }, t, ); const statusError = tunnelStatus?.reason || (tunnelStatus?.errorType ? String(tunnelStatus.errorType) : ""); const lastError = statusError || tunnel.lastError || ""; const lastStarted = formatDateTime(tunnel.lastStartedAt); const lastTested = formatDateTime(tunnel.lastTestedAt); const isOpen = openTunnels.has(index); const kind = getStatusKind(tunnelStatus); const isDisconnected = kind === "disconnected"; const statusText = kind === "connected" ? t("tunnels.connected") : kind === "connecting" ? t("tunnels.connecting") : kind === "error" ? t("tunnels.error") : t("tunnels.disconnected"); const statusTitle = getStatusTitle(tunnelStatus, statusText, t); const statusClass = kind === "connected" ? "text-accent-brand border-accent-brand/40 bg-accent-brand/10" : kind === "connecting" ? "text-blue-400 border-blue-400/40 bg-blue-400/10" : kind === "error" ? "text-destructive border-destructive/40 bg-destructive/10" : "text-muted-foreground border-border bg-muted/30"; return (
{/* Always-visible header */} {/* Expanded body */} {isOpen && (
{/* Action buttons */}
{isTunnelActionLoading ? ( ) : isDisconnected ? ( ) : ( )}
{/* Display name */}
updateTunnel(index, { displayName: e.target.value }) } placeholder={getTunnelDisplayName(tunnel, index)} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
{/* Mode */}
updateTunnel(index, { mode: e.target.value as TunnelMode, tunnelType: getTunnelTypeForMode( e.target.value as TunnelMode, ), }) } className="px-2 py-1 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring w-full h-7" > {modeDescription}
{/* Local side */}
updateTunnel(index, { sourcePort: Number(e.target.value), }) } placeholder={t("placeholders.defaultPort")} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
updateTunnel(index, { localAddress: e.target.value.trim(), bindHost: e.target.value.trim(), }) } placeholder={getLocalAddressPlaceholder(mode)} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
{/* Intermediate host */}
handleEndpointChange(index, e.target.value) } className="px-2 py-1 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring w-full h-7" > {sshHosts.map((host) => ( ))}
{/* Destination side */} {mode !== "dynamic" && (
updateTunnel(index, { remoteAddress: e.target.value.trim(), targetHost: e.target.value.trim(), }) } placeholder={getRemoteAddressPlaceholder(mode)} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
updateTunnel(index, { endpointPort: Number(e.target.value), }) } placeholder={t("placeholders.defaultEndpointPort")} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
)} {/* Route summary */}
{t("tunnels.route")} {" "} {tunnelSummary}
{mode === "remote" && (

{t("tunnels.clientRemoteServerNote")}

)} {/* Last activity */} {(lastError || lastStarted || lastTested) && (
{lastStarted && ( {t("tunnels.lastStarted")}: {lastStarted} )} {lastTested && ( {t("tunnels.lastTested")}: {lastTested} )} {lastError && ( {t("tunnels.lastError")}: {lastError} )}
)} {/* Retries */}
updateTunnel(index, { maxRetries: Number(e.target.value), }) } placeholder={t("placeholders.maxRetries")} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
updateTunnel(index, { retryInterval: Number(e.target.value), }) } placeholder={t("placeholders.retryInterval")} className="h-7 text-xs bg-muted/50 border-border rounded-none" />
{/* Auto-start */}
{t("tunnels.autoStart")} {t( tunnel.autoStart ? "tunnels.clientAutoStartDesc" : "tunnels.clientManualStartDesc", )}
updateTunnel(index, { autoStart: checked }) } />
)}
); }) )}
{/* Save local config button */} {/* Presets section */}
{t("profile.c2sTunnelPresets")}
setPresetName(e.target.value)} placeholder={t("profile.c2sPresetNamePlaceholder")} className="h-7 text-xs bg-muted/50 border-border rounded-none flex-1 min-w-0" />
{t("profile.c2sCurrentLocalConfig", { count: localConfig.length })}
{ setSelectedPresetId(e.target.value); const preset = presets.find( (p) => String(p.id) === e.target.value, ); if (preset) setPresetName(preset.name); }} className="px-2 py-1 text-xs bg-background border border-border text-foreground outline-none focus:ring-1 focus:ring-ring w-full h-7 disabled:opacity-50 disabled:cursor-not-allowed" > {presets.map((preset) => ( ))} {t("profile.c2sPresetSyncNote")}
); }