diff --git a/src/backend/ssh/terminal.ts b/src/backend/ssh/terminal.ts index 2ea759d9..70fb0721 100644 --- a/src/backend/ssh/terminal.ts +++ b/src/backend/ssh/terminal.ts @@ -77,12 +77,6 @@ const wss = new WebSocketServer({ }, }); -sshLogger.success("SSH Terminal WebSocket server started with authentication", { - operation: "server_start", - port: 30002, - features: ["JWT_auth", "connection_limits", "data_access_control"], -}); - wss.on("connection", async (ws: WebSocket, req) => { let userId: string | undefined; let userPayload: any; diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json index e0f941e8..a2efc073 100644 --- a/src/locales/en/translation.json +++ b/src/locales/en/translation.json @@ -88,6 +88,19 @@ "leaveEmptyToKeepCurrent": "Leave empty to keep current value", "uploadKeyFile": "Upload Key File", "generateKeyPairButton": "Generate Key Pair", + "generateKeyPair": "Generate Key Pair", + "generateKeyPairDescription": "Generate a new SSH key pair. If you want to protect the key with a passphrase, enter it in the Key Password field below first.", + "deploySSHKey": "Deploy SSH Key", + "deploySSHKeyDescription": "Deploy public key to target server", + "sourceCredential": "Source Credential", + "targetHost": "Target Host", + "deploymentProcess": "Deployment Process", + "deploymentProcessDescription": "This will safely add the public key to the target host's ~/.ssh/authorized_keys file without overwriting existing keys. The operation is reversible.", + "chooseHostToDeploy": "Choose a host to deploy to...", + "deploying": "Deploying...", + "name": "Name", + "noHostsAvailable": "No hosts available", + "noHostsMatchSearch": "No hosts match your search", "sshKeyGenerationNotImplemented": "SSH key generation feature coming soon", "connectionTestingNotImplemented": "Connection testing feature coming soon", "testConnection": "Test Connection", @@ -266,6 +279,9 @@ "sshTools": "SSH Tools", "english": "English", "chinese": "Chinese", + "cancel": "Cancel", + "username": "Username", + "name": "Name", "login": "Login", "logout": "Logout", "register": "Register", @@ -966,6 +982,7 @@ "connecting": "Connecting...", "disconnecting": "Disconnecting...", "unknownTunnelStatus": "Unknown", + "unknown": "Unknown", "error": "Error", "failed": "Failed", "retrying": "Retrying", @@ -1011,7 +1028,10 @@ "disconnect": "Disconnect", "connect": "Connect", "canceling": "Canceling...", - "endpointHostNotFound": "Endpoint host not found" + "endpointHostNotFound": "Endpoint host not found", + "discord": "Discord", + "githubIssue": "GitHub issue", + "forHelp": "for help" }, "serverStats": { "title": "Server Statistics", diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json index bc09b619..40c9eda8 100644 --- a/src/locales/zh/translation.json +++ b/src/locales/zh/translation.json @@ -88,6 +88,19 @@ "leaveEmptyToKeepCurrent": "留空以保持当前值", "uploadKeyFile": "上传密钥文件", "generateKeyPairButton": "生成密钥对", + "generateKeyPair": "生成密钥对", + "generateKeyPairDescription": "生成新的SSH密钥对。如果您想用密码保护密钥,请先在下面的密钥密码字段中输入密码。", + "deploySSHKey": "部署SSH密钥", + "deploySSHKeyDescription": "将公钥部署到目标服务器", + "sourceCredential": "源凭据", + "targetHost": "目标主机", + "deploymentProcess": "部署过程", + "deploymentProcessDescription": "这将安全地将公钥添加到目标主机的~/.ssh/authorized_keys文件中,而不会覆盖现有密钥。此操作是可逆的。", + "chooseHostToDeploy": "选择要部署到的主机...", + "deploying": "部署中...", + "name": "名称", + "noHostsAvailable": "没有可用的主机", + "noHostsMatchSearch": "没有匹配搜索的主机", "sshKeyGenerationNotImplemented": "SSH密钥生成功能即将推出", "connectionTestingNotImplemented": "连接测试功能即将推出", "testConnection": "测试连接", @@ -260,6 +273,9 @@ "sshTools": "SSH 工具", "english": "英语", "chinese": "中文", + "cancel": "取消", + "username": "用户名", + "name": "名称", "login": "登录", "logout": "登出", "register": "注册", @@ -438,7 +454,6 @@ "verificationCompleted": "兼容性验证完成 - 未修改任何数据", "verificationInProgress": "验证完成", "dataMigrationCompleted": "数据迁移完成!", - "migrationCompleted": "迁移完成", "verificationFailed": "兼容性验证失败", "migrationFailed": "迁移失败", "runningVerification": "正在进行兼容性验证...", @@ -760,7 +775,6 @@ "renaming": "重命名中...", "fileUploadedSuccessfully": "文件 \"{{name}}\" 上传成功", "failedToUploadFile": "上传文件失败", - "fileDownloadedSuccessfully": "文件 \"{{name}}\" 下载成功", "failedToDownloadFile": "下载文件失败", "noFileContent": "未收到文件内容", "filePath": "文件路径", @@ -786,12 +800,8 @@ "noSSHConnection": "无SSH连接可用", "enterFolderName": "输入文件夹名称:", "enterFileName": "输入文件名称:", - "copy": "复制", "cut": "剪切", - "paste": "粘贴", - "delete": "删除", "properties": "属性", - "preview": "预览", "refresh": "刷新", "downloadFiles": "下载 {{count}} 个文件", "copyFiles": "复制 {{count}} 个项目", @@ -811,13 +821,6 @@ "failedToDeleteItem": "删除项目失败", "itemRenamedSuccessfully": "{{type}}重命名成功", "failedToRenameItem": "重命名项目失败", - "upload": "上传", - "download": "下载", - "delete": "删除", - "permissions": "权限", - "size": "大小", - "modified": "修改时间", - "path": "路径", "confirmDelete": "确定要删除 {{name}} 吗?", "uploadSuccess": "文件上传成功", "uploadFailed": "文件上传失败", @@ -834,7 +837,6 @@ "noSshSessionId": "没有可用的 SSH 会话 ID", "noFilePath": "没有可用的文件路径", "noCurrentHost": "没有可用的当前主机", - "fileSavedSuccessfully": "文件保存成功", "saveTimeout": "保存操作超时。文件可能已成功保存,但操作用时过长。请检查 Docker 日志以确认。", "failedToSaveFile": "保存文件失败", "deletedSuccessfully": "删除成功", @@ -889,10 +891,8 @@ "unpinFile": "取消固定", "removeShortcut": "移除快捷方式", "saveFilesToSystem": "另存 {{count}} 个文件为...", - "saveToSystem": "另存为...", "pinFile": "固定文件", "addToShortcuts": "添加到快捷方式", - "selectLocationToSave": "选择位置保存", "downloadToDefaultLocation": "下载到默认位置", "pasteFailed": "粘贴失败", "noUndoableActions": "没有可撤销的操作", @@ -910,7 +910,6 @@ "editPath": "编辑路径", "confirm": "确认", "cancel": "取消", - "folderName": "文件夹名", "find": "查找...", "replaceWith": "替换为...", "replace": "替换", @@ -936,15 +935,9 @@ "outdent": "减少缩进", "autoComplete": "自动补全", "imageLoadError": "图片加载失败", - "zoomIn": "放大", - "zoomOut": "缩小", "rotate": "旋转", "originalSize": "原始大小", "startTyping": "开始输入...", - "fileSavedSuccessfully": "文件保存成功", - "autoSaveFailed": "自动保存失败", - "fileAutoSaved": "文件已自动保存", - "fileDownloadedSuccessfully": "文件下载成功", "moveFileFailed": "移动 {{name}} 失败", "moveOperationFailed": "移动操作失败", "canOnlyCompareFiles": "只能对比两个文件", @@ -980,6 +973,7 @@ "connecting": "连接中...", "disconnecting": "断开连接中...", "unknownTunnelStatus": "未知", + "unknown": "未知", "error": "错误", "failed": "失败", "retrying": "重试中", @@ -1015,7 +1009,10 @@ "remote": "远程", "dynamic": "动态", "portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}", - "endpointHostNotFound": "未找到端点主机" + "endpointHostNotFound": "未找到端点主机", + "discord": "Discord", + "githubIssue": "GitHub 问题", + "forHelp": "寻求帮助" }, "serverStats": { "title": "服务器统计", @@ -1283,101 +1280,30 @@ "discord": "Discord", "connectToSshForOperations": "连接 SSH 以使用文件操作", "uploadFile": "上传文件", - "newFile": "新建文件", - "newFolder": "新建文件夹", "rename": "重命名", - "deleteItem": "删除项目", - "createNewFile": "创建新文件", - "createNewFolder": "创建新文件夹", - "renameItem": "重命名项目", - "clickToSelectFile": "点击选择文件", "noSshHosts": "没有 SSH 主机", - "sshHosts": "SSH 主机", "importSshHosts": "从 JSON 导入 SSH 主机", - "clientId": "客户端 ID", - "clientSecret": "客户端密钥", "error": "错误", - "warning": "警告", - "deleteAccount": "删除账户", - "closeDeleteAccount": "关闭删除账户", - "cannotDeleteAccount": "无法删除账户", - "confirmPassword": "确认密码", - "deleting": "删除中...", "externalAuth": "外部认证 (OIDC)", - "configureExternalProvider": "配置外部身份提供者", - "waitingForRetry": "等待重试", - "retryingConnection": "重试连接中", "resetSplitSizes": "重置分屏大小", "sshManagerAlreadyOpen": "SSH 管理器已打开", "disabledDuringSplitScreen": "分屏期间禁用", - "unknown": "未知", - "connected": "已连接", - "disconnected": "已断开连接", "maxRetriesExhausted": "已达到最大重试次数", - "endpointHostNotFound": "未找到端点主机", "administrator": "管理员", - "user": "用户", - "external": "外部", - "local": "本地", - "saving": "保存中...", - "saveConfiguration": "保存配置", - "loading": "加载中...", - "refresh": "刷新", - "adding": "添加中...", - "makeAdmin": "设为管理员", "verifying": "验证中...", - "verifyAndEnable": "验证并启用", "secretKey": "密钥", "totpQrCode": "TOTP 二维码", - "passwordRequired": "使用密码认证时需要密码", - "sshKeyRequired": "使用密钥认证时需要 SSH 私钥", - "keyTypeRequired": "使用密钥认证时需要密钥类型", "validSshConfigRequired": "必须从列表中选择有效的 SSH 配置", - "updateHost": "更新主机", - "addHost": "添加主机", - "editHost": "编辑主机", "pinConnection": "固定连接", - "authentication": "认证", - "password": "密码", - "key": "密钥", - "sshPrivateKey": "SSH 私钥", - "keyPassword": "密钥密码", - "keyType": "密钥类型", - "enableTerminal": "启用终端", - "enableTunnel": "启用隧道", - "enableFileManager": "启用文件管理器", - "defaultPath": "默认路径", - "tunnelConnections": "隧道连接", - "maxRetries": "最大重试次数", - "upload": "上传", - "updateKey": "更新密钥", - "sshpassRequired": "密码认证需要 Sshpass", - "sshServerConfigRequired": "需要 SSH 服务器配置", "productionFolder": "生产环境", "databaseServer": "数据库服务器", "developmentServer": "开发服务器", "developmentFolder": "开发环境", "webServerProduction": "Web 服务器 - 生产环境", - "unknownError": "未知错误", - "failedToInitiatePasswordReset": "启动密码重置失败", - "failedToVerifyResetCode": "验证重置代码失败", - "failedToCompletePasswordReset": "完成密码重置失败", - "invalidTotpCode": "无效的 TOTP 代码", "failedToStartOidcLogin": "启动 OIDC 登录失败", "failedToGetUserInfoAfterOidc": "OIDC 登录后获取用户信息失败", "loginWithExternalProvider": "使用外部提供者登录", - "loginWithExternal": "使用外部提供者登录", - "sendResetCode": "发送重置代码", - "verifyCode": "验证代码", - "resetPassword": "重置密码", - "login": "登录", - "signUp": "注册", - "failedToUpdateOidcConfig": "更新 OIDC 配置失败", - "failedToMakeUserAdmin": "设为管理员失败", - "failedToStartTotpSetup": "启动 TOTP 设置失败", - "invalidVerificationCode": "无效的验证码", - "failedToDisableTotp": "禁用 TOTP 失败", - "failedToGenerateBackupCodes": "生成备用码失败" + "failedToStartTotpSetup": "启动 TOTP 设置失败" }, "mobile": { "selectHostToStart": "选择一个主机以开始您的终端会话", diff --git a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx index 804c60c5..9303c0b8 100644 --- a/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx +++ b/src/ui/Desktop/Apps/Credentials/CredentialEditor.tsx @@ -377,7 +377,6 @@ export function CredentialEditor({ }; const [tagInput, setTagInput] = useState(""); - const [keyGenerationPassphrase, setKeyGenerationPassphrase] = useState(""); const [folderDropdownOpen, setFolderDropdownOpen] = useState(false); const folderInputRef = useRef(null); @@ -440,10 +439,10 @@ export function CredentialEditor({ - + {t("credentials.basicInformation")} -
+
- + {t("credentials.organization")} -
+
- + {t("credentials.authentication")} -
- {/* Generate Key Pair Buttons */} -
- +
+
+ {t("credentials.generateKeyPair")} - {/* Key Generation Passphrase Input */} -
- - {t("credentials.keyPassword")} ( - {t("credentials.optional")}) - - - setKeyGenerationPassphrase(e.target.value) - } - className="max-w-xs" - /> -
- {t("credentials.keyPassphraseOptional")} +
+
+ {t("credentials.generateKeyPairDescription")}
@@ -703,24 +688,20 @@ export function CredentialEditor({ size="sm" onClick={async () => { try { + const currentKeyPassword = + form.watch("keyPassword"); const result = await generateKeyPair( "ssh-ed25519", undefined, - keyGenerationPassphrase, + currentKeyPassword, ); if (result.success) { form.setValue("key", result.privateKey); form.setValue("publicKey", result.publicKey); - if (keyGenerationPassphrase) { - form.setValue( - "keyPassword", - keyGenerationPassphrase, - ); - } debouncedKeyDetection( result.privateKey, - keyGenerationPassphrase, + currentKeyPassword, ); debouncedPublicKeyDetection(result.publicKey); toast.success( @@ -754,24 +735,20 @@ export function CredentialEditor({ size="sm" onClick={async () => { try { + const currentKeyPassword = + form.watch("keyPassword"); const result = await generateKeyPair( "ecdsa-sha2-nistp256", undefined, - keyGenerationPassphrase, + currentKeyPassword, ); if (result.success) { form.setValue("key", result.privateKey); form.setValue("publicKey", result.publicKey); - if (keyGenerationPassphrase) { - form.setValue( - "keyPassword", - keyGenerationPassphrase, - ); - } debouncedKeyDetection( result.privateKey, - keyGenerationPassphrase, + currentKeyPassword, ); debouncedPublicKeyDetection(result.publicKey); toast.success( @@ -805,24 +782,20 @@ export function CredentialEditor({ size="sm" onClick={async () => { try { + const currentKeyPassword = + form.watch("keyPassword"); const result = await generateKeyPair( "ssh-rsa", 2048, - keyGenerationPassphrase, + currentKeyPassword, ); if (result.success) { form.setValue("key", result.privateKey); form.setValue("publicKey", result.publicKey); - if (keyGenerationPassphrase) { - form.setValue( - "keyPassword", - keyGenerationPassphrase, - ); - } debouncedKeyDetection( result.privateKey, - keyGenerationPassphrase, + currentKeyPassword, ); debouncedPublicKeyDetection(result.publicKey); toast.success( @@ -851,20 +824,17 @@ export function CredentialEditor({ {t("credentials.generateRSA")}
-
- {t("credentials.generateKeyPairNote")} -
-
+
( - - + + {t("credentials.sshPrivateKey")} -
+
( - - - {t("credentials.sshPublicKey")} ( - {t("credentials.optional")}) + + + {t("credentials.sshPublicKey")} -
+
-
- {t("credentials.publicKeyNote")} -
{detectedPublicKeyType && field.value && (
@@ -1131,7 +1097,7 @@ export function CredentialEditor({ )} />
-
+
([]); const [selectedHostId, setSelectedHostId] = useState(""); const [deployLoading, setDeployLoading] = useState(false); + const [hostSearchQuery, setHostSearchQuery] = useState(""); + const [dropdownOpen, setDropdownOpen] = useState(false); + const dropdownRef = useRef(null); const dragCounter = useRef(0); useEffect(() => { @@ -98,6 +101,40 @@ export function CredentialsManager({ fetchHosts(); }, []); + useEffect(() => { + if (showDeployDialog) { + setDropdownOpen(false); + setHostSearchQuery(""); + setTimeout(() => { + const activeElement = document.activeElement as HTMLElement; + if (activeElement && activeElement.blur) { + activeElement.blur(); + } + }, 100); + } + }, [showDeployDialog]); + + useEffect(() => { + function handleClickOutside(event: MouseEvent) { + if ( + dropdownRef.current && + !dropdownRef.current.contains(event.target as Node) + ) { + setDropdownOpen(false); + } + } + + if (dropdownOpen) { + document.addEventListener("mousedown", handleClickOutside); + } else { + document.removeEventListener("mousedown", handleClickOutside); + } + + return () => { + document.removeEventListener("mousedown", handleClickOutside); + }; + }, [dropdownOpen]); + const fetchHosts = async () => { try { const hosts = await getSSHHosts(); @@ -137,6 +174,8 @@ export function CredentialsManager({ } setDeployingCredential(credential); setSelectedHostId(""); + setHostSearchQuery(""); + setDropdownOpen(false); setShowDeployDialog(true); }; @@ -789,145 +828,209 @@ export function CredentialsManager({ - - -
- -
-
-
Deploy SSH Key
-
- Deploy public key to target server +
+
+
+
+
-
- - - -
- {deployingCredential && ( -
-

- - Source Credential -

-
-
-
- -
-
-
- Name -
-
- {deployingCredential.name || - deployingCredential.username} -
-
+
+
+ {t("credentials.deploySSHKey")}
-
-
- -
-
-
- Username -
-
- {deployingCredential.username} -
-
-
-
-
- -
-
-
- Key Type -
-
- {deployingCredential.keyType || "SSH Key"} -
-
+
+ {t("credentials.deploySSHKeyDescription")}
- )} - -
- -
-
-
- -
-

Deployment Process

-

- This will safely add the public key to the target host's - ~/.ssh/authorized_keys file without overwriting existing - keys. The operation is reversible. -

+
+ {deployingCredential && ( +
+

+ + {t("credentials.sourceCredential")} +

+
+
+
+ +
+
+
+ {t("common.name")} +
+
+ {deployingCredential.name || + deployingCredential.username} +
+
+
+
+
+ +
+
+
+ {t("common.username")} +
+
+ {deployingCredential.username} +
+
+
+
+
+ +
+
+
+ {t("credentials.keyType")} +
+
+ {deployingCredential.keyType || "SSH Key"} +
+
+
+
+
+ )} + +
+ +
+ { + setHostSearchQuery(e.target.value); + if (e.target.value.trim() !== "") { + setDropdownOpen(true); + } else { + setDropdownOpen(false); + } + }} + onFocus={() => { + setDropdownOpen(true); + }} + className="w-full" + autoFocus={false} + /> + {dropdownOpen && ( +
+ {availableHosts.length === 0 ? ( +
+ {t("credentials.noHostsAvailable")} +
+ ) : availableHosts.filter( + (host) => + !hostSearchQuery || + host.name + ?.toLowerCase() + .includes(hostSearchQuery.toLowerCase()) || + host.ip + ?.toLowerCase() + .includes(hostSearchQuery.toLowerCase()) || + host.username + ?.toLowerCase() + .includes(hostSearchQuery.toLowerCase()), + ).length === 0 ? ( +
+ {t("credentials.noHostsMatchSearch")} +
+ ) : ( + availableHosts + .filter( + (host) => + !hostSearchQuery || + host.name + ?.toLowerCase() + .includes(hostSearchQuery.toLowerCase()) || + host.ip + ?.toLowerCase() + .includes(hostSearchQuery.toLowerCase()) || + host.username + ?.toLowerCase() + .includes(hostSearchQuery.toLowerCase()), + ) + .map((host) => ( +
{ + setSelectedHostId(host.id.toString()); + setHostSearchQuery(host.name || host.ip); + setDropdownOpen(false); + }} + > +
+ +
+
+
+ {host.name || host.ip} +
+
+ {host.username}@{host.ip}:{host.port} +
+
+
+ )) + )} +
+ )} +
+
+ +
+
+ +
+

+ {t("credentials.deploymentProcess")} +

+

+ {t("credentials.deploymentProcessDescription")} +

+
+
+
+ +
+
+ +
- - - - -
diff --git a/src/ui/Desktop/Apps/File Manager/FileManager.tsx b/src/ui/Desktop/Apps/File Manager/FileManager.tsx index 234d08ab..faa8a036 100644 --- a/src/ui/Desktop/Apps/File Manager/FileManager.tsx +++ b/src/ui/Desktop/Apps/File Manager/FileManager.tsx @@ -96,6 +96,7 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { const [viewMode, setViewMode] = useState<"grid" | "list">("grid"); const [pinnedFiles, setPinnedFiles] = useState>(new Set()); const [sidebarRefreshTrigger, setSidebarRefreshTrigger] = useState(0); + const [isClosing, setIsClosing] = useState(false); const [contextMenu, setContextMenu] = useState<{ x: number; @@ -179,6 +180,18 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } }, []); + const handleCloseWithError = useCallback( + (errorMessage: string) => { + if (isClosing) return; // Prevent duplicate calls + setIsClosing(true); + toast.error(errorMessage); + if (onClose) { + onClose(); + } + }, + [isClosing, onClose], + ); + useEffect(() => { if (currentHost) { initializeSSHConnection(); @@ -286,12 +299,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { } } catch (error: any) { console.error("SSH connection failed:", error); - toast.error( + handleCloseWithError( t("fileManager.failedToConnect") + ": " + (error.message || error), ); - if (onClose) { - onClose(); - } } finally { setIsLoading(false); } @@ -342,9 +352,11 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { error.message?.includes("connection") || error.message?.includes("SSH") ) { - if (onClose) { - onClose(); - } + handleCloseWithError( + t("fileManager.failedToLoadDirectory") + + ": " + + (error.message || error), + ); } } } finally { @@ -1104,9 +1116,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) { }); } } catch (error) { - if (onClose) { - onClose(); - } + handleCloseWithError( + `SSH connection failed. Please check your connection to ${currentHost?.name} (${currentHost?.ip}:${currentHost?.port})`, + ); throw error; } finally { setIsReconnecting(false); diff --git a/src/ui/Desktop/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx index 9eeac6d3..ce5ec323 100644 --- a/src/ui/Desktop/Apps/Server/Server.tsx +++ b/src/ui/Desktop/Apps/Server/Server.tsx @@ -136,10 +136,8 @@ export function Server({ fetchStatus(); fetchMetrics(); intervalId = window.setInterval(() => { - if (isVisible) { - fetchStatus(); - fetchMetrics(); - } + fetchStatus(); + fetchMetrics(); }, 30000); } diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx index dfc69ec1..5459a9d1 100644 --- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx +++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx @@ -617,9 +617,6 @@ export const Terminal = forwardRef(function SSHTerminal( const jwtToken = getCookie("jwt"); if (!jwtToken || jwtToken.trim() === "") { - console.warn( - "WebSocket connection delayed - no authentication token", - ); setIsConnected(false); setIsConnecting(false); setConnectionError("Authentication required"); diff --git a/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx b/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx index 1fede1e1..45c6f7cf 100644 --- a/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx +++ b/src/ui/Desktop/Apps/Tunnel/Tunnel.tsx @@ -1,4 +1,5 @@ import React, { useState, useEffect, useCallback } from "react"; +import { useTranslation } from "react-i18next"; import { TunnelViewer } from "@/ui/Desktop/Apps/Tunnel/TunnelViewer.tsx"; import { getSSHHosts, @@ -15,6 +16,7 @@ import type { } from "../../../types/index.js"; export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { + const { t } = useTranslation(); const [allHosts, setAllHosts] = useState([]); const [visibleHosts, setVisibleHosts] = useState([]); const [tunnelStatuses, setTunnelStatuses] = useState< @@ -114,7 +116,7 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { useEffect(() => { fetchTunnelStatuses(); - const interval = setInterval(fetchTunnelStatuses, 500); + const interval = setInterval(fetchTunnelStatuses, 5000); return () => clearInterval(interval); }, [fetchTunnelStatuses]); @@ -137,7 +139,7 @@ export function Tunnel({ filterHostKey }: SSHTunnelProps): React.ReactElement { ); if (!endpointHost) { - throw new Error("Endpoint host not found"); + throw new Error(t("tunnels.endpointHostNotFound")); } const tunnelConfig = { diff --git a/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx b/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx index adb38a9e..f079b8e1 100644 --- a/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx +++ b/src/ui/Desktop/Apps/Tunnel/TunnelObject.tsx @@ -237,7 +237,7 @@ export function TunnelObject({ rel="noopener noreferrer" className="underline text-blue-600 dark:text-blue-400" > - Discord + {t("tunnels.discord")} {" "} or create a{" "} - GitHub issue + {t("tunnels.githubIssue")} {" "} - for help. + {t("tunnels.forHelp")}.
)} @@ -471,7 +471,7 @@ export function TunnelObject({ rel="noopener noreferrer" className="underline text-blue-600 dark:text-blue-400" > - Discord + {t("tunnels.discord")} {" "} or create a{" "} - GitHub issue + {t("tunnels.githubIssue")} {" "} - for help. + {t("tunnels.forHelp")}.
)} diff --git a/src/ui/Desktop/Navigation/Hosts/Host.tsx b/src/ui/Desktop/Navigation/Hosts/Host.tsx index 1d36c51a..50688a36 100644 --- a/src/ui/Desktop/Navigation/Hosts/Host.tsx +++ b/src/ui/Desktop/Navigation/Hosts/Host.tsx @@ -46,7 +46,7 @@ export function Host({ host }: HostProps): React.ReactElement { fetchStatus(); - intervalId = window.setInterval(fetchStatus, 10000); + intervalId = window.setInterval(fetchStatus, 30000); return () => { cancelled = true; diff --git a/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx b/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx index d31ee25c..f59f8fb9 100644 --- a/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx +++ b/src/ui/Mobile/Apps/Navigation/Hosts/Host.tsx @@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement { fetchStatus(); - intervalId = window.setInterval(fetchStatus, 10000); + intervalId = window.setInterval(fetchStatus, 30000); return () => { cancelled = true; diff --git a/src/ui/Mobile/Apps/Terminal/Terminal.tsx b/src/ui/Mobile/Apps/Terminal/Terminal.tsx index fe445b31..c2470658 100644 --- a/src/ui/Mobile/Apps/Terminal/Terminal.tsx +++ b/src/ui/Mobile/Apps/Terminal/Terminal.tsx @@ -271,9 +271,6 @@ export const Terminal = forwardRef(function SSHTerminal( const jwtToken = getCookie("jwt"); if (!jwtToken || jwtToken.trim() === "") { - console.warn( - "WebSocket connection delayed - no authentication token", - ); setIsConnected(false); setIsConnecting(false); setConnectionError("Authentication required"); diff --git a/src/ui/Mobile/Navigation/Hosts/Host.tsx b/src/ui/Mobile/Navigation/Hosts/Host.tsx index 921c6fed..aa674830 100644 --- a/src/ui/Mobile/Navigation/Hosts/Host.tsx +++ b/src/ui/Mobile/Navigation/Hosts/Host.tsx @@ -46,7 +46,7 @@ export function Host({ host, onHostConnect }: HostProps): React.ReactElement { fetchStatus(); - intervalId = window.setInterval(fetchStatus, 10000); + intervalId = window.setInterval(fetchStatus, 30000); return () => { cancelled = true;