v1.6.0 #221
@@ -18,7 +18,7 @@ http {
|
||||
index index.html index.htm;
|
||||
}
|
||||
|
||||
location /users/ {
|
||||
location ~ ^/users(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
@@ -27,7 +27,7 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /version/ {
|
||||
location ~ ^/version(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
@@ -36,7 +36,7 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /releases/ {
|
||||
location ~ ^/releases(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
@@ -45,7 +45,16 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /alerts/ {
|
||||
location ~ ^/alerts(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location ~ ^/credentials(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8081;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
@@ -129,7 +138,7 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /status/ {
|
||||
location ~ ^/status(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8085;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
@@ -138,7 +147,7 @@ http {
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
|
||||
location /metrics/ {
|
||||
location ~ ^/metrics(/.*)?$ {
|
||||
proxy_pass http://127.0.0.1:8085;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
|
||||
@@ -129,7 +129,7 @@ ipcMain.handle('save-server-config', (event, config) => {
|
||||
|
||||
ipcMain.handle('test-server-connection', async (event, serverUrl) => {
|
||||
try {
|
||||
const { default: fetch } = await import('node-fetch');
|
||||
const fetch = require('node-fetch');
|
||||
|
||||
// Try multiple endpoints to test the connection
|
||||
const testUrls = [
|
||||
|
||||
@@ -507,9 +507,11 @@
|
||||
"unknownError": "Unknown error occurred",
|
||||
"messageParseError": "Failed to parse server message",
|
||||
"websocketError": "WebSocket connection error",
|
||||
"connecting": "Connecting...",
|
||||
"reconnecting": "Reconnecting... ({{attempt}}/{{max}})",
|
||||
"reconnected": "Reconnected successfully",
|
||||
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached"
|
||||
"maxReconnectAttemptsReached": "Maximum reconnection attempts reached",
|
||||
"connectionTimeout": "Connection timeout"
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "File Manager",
|
||||
@@ -816,7 +818,6 @@
|
||||
"oidcAuthFailed": "OIDC authentication failed",
|
||||
"noTokenReceived": "No token received from login",
|
||||
"invalidAuthUrl": "Invalid authorization URL received from backend",
|
||||
"connectionTimeout": "Connection timeout",
|
||||
"invalidInput": "Invalid input",
|
||||
"requiredField": "This field is required",
|
||||
"minLength": "Minimum length is {{min}}",
|
||||
|
||||
@@ -60,7 +60,6 @@
|
||||
"keyTypeRSA": "RSA",
|
||||
"keyTypeECDSA": "ECDSA",
|
||||
"keyTypeEd25519": "Ed25519",
|
||||
"updateCredential": "更新凭据",
|
||||
"basicInfo": "基本信息",
|
||||
"authentication": "认证方式",
|
||||
"organization": "组织管理",
|
||||
@@ -106,7 +105,6 @@
|
||||
"credentialSecuredDescription": "所有敏感数据均使用AES-256加密",
|
||||
"passwordAuthentication": "密码认证",
|
||||
"keyAuthentication": "密钥认证",
|
||||
"keyType": "密钥类型",
|
||||
"securityReminder": "安全提醒",
|
||||
"securityReminderText": "请勿分享您的凭据。所有数据均已静态加密。",
|
||||
"hostsUsingCredential": "使用此凭据的主机",
|
||||
@@ -181,7 +179,7 @@
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"success": "成功",
|
||||
"loading": "加载中",
|
||||
"loading": "加载中...",
|
||||
"required": "必填",
|
||||
"optional": "可选",
|
||||
"clear": "清除",
|
||||
@@ -195,8 +193,7 @@
|
||||
"updateAvailable": "有可用更新",
|
||||
"sshPath": "SSH 路径",
|
||||
"localPath": "本地路径",
|
||||
"loading": "加载中...",
|
||||
"noAuthCredentials": "此 SSH 主机没有可用的身份验证凭据",
|
||||
"noAuthCredentials": "此 SSH 主机没有可用的认证凭据",
|
||||
"noReleases": "没有发布版本",
|
||||
"updatesAndReleases": "更新与发布",
|
||||
"newVersionAvailable": "有新版本 ({{version}}) 可用。",
|
||||
@@ -209,13 +206,10 @@
|
||||
"resetPassword": "重置密码",
|
||||
"resetCode": "重置代码",
|
||||
"newPassword": "新密码",
|
||||
"sshPath": "SSH 路径",
|
||||
"localPath": "本地路径",
|
||||
"folder": "文件夹",
|
||||
"file": "文件",
|
||||
"renamedSuccessfully": "重命名成功",
|
||||
"deletedSuccessfully": "删除成功",
|
||||
"noAuthCredentials": "此 SSH 主机没有可用的认证凭据",
|
||||
"noTunnelConnections": "没有配置隧道连接",
|
||||
"sshTools": "SSH 工具",
|
||||
"english": "英语",
|
||||
@@ -236,22 +230,15 @@
|
||||
"edit": "编辑",
|
||||
"add": "添加",
|
||||
"search": "搜索",
|
||||
"loading": "加载中...",
|
||||
"error": "错误",
|
||||
"success": "成功",
|
||||
"warning": "警告",
|
||||
"info": "信息",
|
||||
"confirm": "确认",
|
||||
"yes": "是",
|
||||
"no": "否",
|
||||
"ok": "确定",
|
||||
"close": "关闭",
|
||||
"enabled": "已启用",
|
||||
"disabled": "已禁用",
|
||||
"important": "重要",
|
||||
"notEnabled": "未启用",
|
||||
"settingUp": "设置中...",
|
||||
"back": "返回",
|
||||
"next": "下一步",
|
||||
"previous": "上一步",
|
||||
"refresh": "刷新",
|
||||
@@ -297,7 +284,7 @@
|
||||
"userManagement": "用户管理",
|
||||
"makeAdmin": "设为管理员",
|
||||
"removeAdmin": "移除管理员",
|
||||
"deleteUser": "删除用户",
|
||||
"deleteUser": "删除用户 {{username}} 吗?此操作无法撤销。",
|
||||
"allowRegistration": "允许注册",
|
||||
"oidcSettings": "OIDC 设置",
|
||||
"clientId": "客户端 ID",
|
||||
@@ -346,7 +333,6 @@
|
||||
"removeAdminStatus": "移除 {{username}} 的管理员权限吗?",
|
||||
"adminStatusRemoved": "已移除 {{username}} 的管理员权限",
|
||||
"failedToRemoveAdminStatus": "移除管理员权限失败",
|
||||
"deleteUser": "删除用户 {{username}} 吗?此操作无法撤销。",
|
||||
"userDeletedSuccessfully": "用户 {{username}} 删除成功",
|
||||
"failedToDeleteUser": "删除用户失败",
|
||||
"overrideUserInfoUrl": "覆盖用户信息 URL(非必填)"
|
||||
@@ -380,7 +366,7 @@
|
||||
"importError": "导入错误",
|
||||
"failedToImportJson": "导入 JSON 文件失败",
|
||||
"connectionDetails": "连接详情",
|
||||
"organization": "组织",
|
||||
"organization": "组织管理",
|
||||
"ipAddress": "IP 地址",
|
||||
"port": "端口",
|
||||
"name": "名称",
|
||||
@@ -395,16 +381,11 @@
|
||||
"addHost": "添加主机",
|
||||
"editHost": "编辑主机",
|
||||
"deleteHost": "删除主机",
|
||||
"hostName": "主机名",
|
||||
"ipAddress": "IP 地址",
|
||||
"port": "端口",
|
||||
"authType": "认证类型",
|
||||
"passwordAuth": "密码",
|
||||
"keyAuth": "SSH 密钥",
|
||||
"keyPassword": "密钥密码",
|
||||
"keyType": "密钥类型",
|
||||
"folder": "文件夹",
|
||||
"tags": "标签",
|
||||
"pin": "固定",
|
||||
"enableTerminal": "启用终端",
|
||||
"enableTunnel": "启用隧道",
|
||||
@@ -418,8 +399,6 @@
|
||||
"connecting": "连接中...",
|
||||
"connectionFailed": "连接失败",
|
||||
"connectionSuccess": "连接成功",
|
||||
"connectionDetails": "连接详情",
|
||||
"organization": "组织管理",
|
||||
"addTags": "添加标签(空格添加)",
|
||||
"sourcePort": "源端口",
|
||||
"sourcePortDesc": "(源指通用标签页中的当前连接详情)",
|
||||
@@ -465,8 +444,6 @@
|
||||
"credentialRequired": "使用凭证认证时需要选择凭证",
|
||||
"credentialDescription": "选择凭证将覆盖当前用户名并使用凭证的认证详细信息。",
|
||||
"sshPrivateKey": "SSH 私钥",
|
||||
"keyPassword": "密钥密码",
|
||||
"keyType": "密钥类型",
|
||||
"maxRetriesDescription": "隧道连接的最大重试次数。",
|
||||
"retryIntervalDescription": "重试尝试之间的等待时间。",
|
||||
"otherInstallMethods": "其他安装方法:",
|
||||
@@ -533,7 +510,18 @@
|
||||
"error": "错误",
|
||||
"disconnected": "已断开连接",
|
||||
"connectionClosed": "连接已关闭",
|
||||
"connectionError": "连接错误"
|
||||
"connectionError": "连接错误",
|
||||
"connected": "已连接",
|
||||
"sshConnected": "SSH 连接已建立",
|
||||
"authError": "认证失败:{{message}}",
|
||||
"unknownError": "发生未知错误",
|
||||
"messageParseError": "解析服务器消息失败",
|
||||
"websocketError": "WebSocket 连接错误",
|
||||
"connecting": "连接中...",
|
||||
"reconnecting": "重新连接中... ({{attempt}}/{{max}})",
|
||||
"reconnected": "重新连接成功",
|
||||
"maxReconnectAttemptsReached": "已达到最大重连尝试次数",
|
||||
"connectionTimeout": "连接超时"
|
||||
},
|
||||
"fileManager": {
|
||||
"title": "文件管理器",
|
||||
@@ -580,16 +568,11 @@
|
||||
"failedToRenameItem": "重命名项目失败",
|
||||
"upload": "上传",
|
||||
"download": "下载",
|
||||
"newFile": "新建文件",
|
||||
"newFolder": "新建文件夹",
|
||||
"rename": "重命名",
|
||||
"delete": "删除",
|
||||
"permissions": "权限",
|
||||
"size": "大小",
|
||||
"modified": "修改时间",
|
||||
"path": "路径",
|
||||
"fileName": "文件名",
|
||||
"folderName": "文件夹名",
|
||||
"confirmDelete": "确定要删除 {{name}} 吗?",
|
||||
"uploadSuccess": "文件上传成功",
|
||||
"uploadFailed": "文件上传失败",
|
||||
@@ -609,10 +592,7 @@
|
||||
"fileSavedSuccessfully": "文件保存成功",
|
||||
"saveTimeout": "保存操作超时。文件可能已成功保存,但操作用时过长。请检查 Docker 日志以确认。",
|
||||
"failedToSaveFile": "保存文件失败",
|
||||
"folder": "文件夹",
|
||||
"file": "文件",
|
||||
"deletedSuccessfully": "删除成功",
|
||||
"failedToDeleteItem": "删除项目失败",
|
||||
"connectToServer": "连接到服务器",
|
||||
"selectServerToEdit": "从侧边栏选择服务器以开始编辑文件",
|
||||
"fileOperations": "文件操作",
|
||||
@@ -640,11 +620,11 @@
|
||||
"tunnels": {
|
||||
"title": "SSH 隧道",
|
||||
"noSshTunnels": "没有 SSH 隧道",
|
||||
"createFirstTunnelMessage": "您还没有创建任何 SSH 隧道。在主机管理器中配置隧道连接以开始使用。",
|
||||
"createFirstTunnelMessage": "创建您的第一个 SSH 隧道以开始使用。使用 SSH 管理器添加具有隧道连接的主机。",
|
||||
"connected": "已连接",
|
||||
"disconnected": "已断开",
|
||||
"disconnected": "已断开连接",
|
||||
"connecting": "连接中...",
|
||||
"disconnecting": "断开中...",
|
||||
"disconnecting": "断开连接中...",
|
||||
"unknown": "未知",
|
||||
"error": "错误",
|
||||
"failed": "失败",
|
||||
@@ -680,17 +660,7 @@
|
||||
"local": "本地",
|
||||
"remote": "远程",
|
||||
"dynamic": "动态",
|
||||
"noSshTunnels": "没有 SSH 隧道",
|
||||
"createFirstTunnelMessage": "创建您的第一个 SSH 隧道以开始使用。使用 SSH 管理器添加具有隧道连接的主机。",
|
||||
"unknown": "未知",
|
||||
"connected": "已连接",
|
||||
"connecting": "连接中...",
|
||||
"disconnecting": "断开连接中...",
|
||||
"disconnected": "已断开连接",
|
||||
"portMapping": "端口 {{sourcePort}} → {{endpointHost}}:{{endpointPort}}",
|
||||
"disconnect": "断开连接",
|
||||
"connect": "连接",
|
||||
"canceling": "取消中...",
|
||||
"endpointHostNotFound": "未找到端点主机"
|
||||
},
|
||||
"serverStats": {
|
||||
@@ -700,7 +670,7 @@
|
||||
"disk": "磁盘",
|
||||
"network": "网络",
|
||||
"uptime": "运行时间",
|
||||
"loadAverage": "平均负载",
|
||||
"loadAverage": "平均: {{avg1}}, {{avg5}}, {{avg15}}",
|
||||
"processes": "进程",
|
||||
"connections": "连接",
|
||||
"usage": "使用率",
|
||||
@@ -716,20 +686,20 @@
|
||||
"cpuCores_one": "{{count}} 个 CPU",
|
||||
"cpuCores_other": "{{count}} 个 CPU",
|
||||
"naCpus": "N/A CPU",
|
||||
"loadAverage": "平均: {{avg1}}, {{avg5}}, {{avg15}}",
|
||||
"loadAverageNA": "平均: N/A",
|
||||
"cpuUsage": "CPU 使用率",
|
||||
"memoryUsage": "内存使用率",
|
||||
"rootStorageSpace": "根目录存储空间",
|
||||
"of": "的",
|
||||
"feedbackMessage": "对服务器管理的下一步功能有想法?在这里分享吧",
|
||||
"failedToFetchHostConfig": "获取主机配置失败",
|
||||
"failedToFetchStatus": "获取服务器状态失败",
|
||||
"failedToFetchMetrics": "获取服务器指标失败",
|
||||
"loadingMetrics": "正在加载指标...",
|
||||
"refreshing": "正在刷新...",
|
||||
"serverOffline": "服务器离线",
|
||||
"cannotFetchMetrics": "无法从离线服务器获取指标",
|
||||
"load": "负载",
|
||||
"free": "空闲",
|
||||
"available": "可用"
|
||||
"load": "负载"
|
||||
},
|
||||
"auth": {
|
||||
"loginTitle": "登录 Termix",
|
||||
@@ -831,7 +801,6 @@
|
||||
"oidcAuthFailed": "OIDC 认证失败",
|
||||
"noTokenReceived": "登录未收到令牌",
|
||||
"invalidAuthUrl": "从后端收到无效的授权 URL",
|
||||
"connectionTimeout": "连接超时",
|
||||
"invalidInput": "输入无效",
|
||||
"requiredField": "此字段为必填项",
|
||||
"minLength": "最小长度为 {{min}}",
|
||||
@@ -876,6 +845,9 @@
|
||||
"external": "外部 (OIDC)",
|
||||
"selectPreferredLanguage": "选择您的界面首选语言"
|
||||
},
|
||||
"user": {
|
||||
"failedToLoadVersionInfo": "加载版本信息失败"
|
||||
},
|
||||
"placeholders": {
|
||||
"enterCode": "000000",
|
||||
"ipAddress": "127.0.0.1",
|
||||
@@ -953,7 +925,6 @@
|
||||
"deleteItem": "删除项目",
|
||||
"createNewFile": "创建新文件",
|
||||
"createNewFolder": "创建新文件夹",
|
||||
"deleteItem": "删除项目",
|
||||
"renameItem": "重命名项目",
|
||||
"clickToSelectFile": "点击选择文件",
|
||||
"noSshHosts": "没有 SSH 主机",
|
||||
@@ -1018,8 +989,6 @@
|
||||
"updateKey": "更新密钥",
|
||||
"sshpassRequired": "密码认证需要 Sshpass",
|
||||
"sshServerConfigRequired": "需要 SSH 服务器配置",
|
||||
"sshManagerAlreadyOpen": "SSH 管理器已打开",
|
||||
"disabledDuringSplitScreen": "分屏期间禁用",
|
||||
"productionFolder": "生产环境",
|
||||
"databaseServer": "数据库服务器",
|
||||
"unknownError": "未知错误",
|
||||
|
||||
@@ -306,9 +306,6 @@ export function Server({
|
||||
value={typeof metrics?.cpu?.percent === 'number' ? metrics!.cpu!.percent! : 0}
|
||||
className="h-2"
|
||||
/>
|
||||
{typeof metrics?.cpu?.percent === 'number' && metrics.cpu.percent > 80 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
@@ -347,9 +344,6 @@ export function Server({
|
||||
value={typeof metrics?.memory?.percent === 'number' ? metrics!.memory!.percent! : 0}
|
||||
className="h-2"
|
||||
/>
|
||||
{typeof metrics?.memory?.percent === 'number' && metrics.memory.percent > 85 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
@@ -390,9 +384,6 @@ export function Server({
|
||||
value={typeof metrics?.disk?.percent === 'number' ? metrics!.disk!.percent! : 0}
|
||||
className="h-2"
|
||||
/>
|
||||
{typeof metrics?.disk?.percent === 'number' && metrics.disk.percent > 90 && (
|
||||
<div className="absolute -top-1 -right-1 w-3 h-3 bg-red-500 rounded-full animate-pulse"></div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="text-xs text-gray-500">
|
||||
|
||||
@@ -29,6 +29,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const pingIntervalRef = useRef<NodeJS.Timeout | null>(null);
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [isConnected, setIsConnected] = useState(false);
|
||||
const [isConnecting, setIsConnecting] = useState(false);
|
||||
const [connectionError, setConnectionError] = useState<string | null>(null);
|
||||
const isVisibleRef = useRef<boolean>(false);
|
||||
const reconnectTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
@@ -36,7 +37,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
const maxReconnectAttempts = 3;
|
||||
const isUnmountingRef = useRef(false);
|
||||
const shouldNotReconnectRef = useRef(false);
|
||||
|
||||
const isReconnectingRef = useRef(false);
|
||||
const connectionTimeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const lastSentSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
const pendingSizeRef = useRef<{ cols: number; rows: number } | null>(null);
|
||||
@@ -76,6 +78,7 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
disconnect: () => {
|
||||
isUnmountingRef.current = true;
|
||||
shouldNotReconnectRef.current = true;
|
||||
isReconnectingRef.current = false;
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
@@ -84,8 +87,13 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
clearTimeout(reconnectTimeoutRef.current);
|
||||
reconnectTimeoutRef.current = null;
|
||||
}
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
webSocketRef.current?.close();
|
||||
setIsConnected(false);
|
||||
setIsConnecting(false); // Clear connecting state
|
||||
},
|
||||
fit: () => {
|
||||
fitAddonRef.current?.fit();
|
||||
@@ -135,31 +143,54 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
}
|
||||
|
||||
function attemptReconnection() {
|
||||
// Don't attempt reconnection if component is unmounting or if we shouldn't reconnect
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||
// Don't attempt reconnection if component is unmounting, shouldn't reconnect, or already reconnecting
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current || isReconnectingRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've already reached max attempts
|
||||
if (reconnectAttempts.current >= maxReconnectAttempts) {
|
||||
toast.error(t('terminal.maxReconnectAttemptsReached'));
|
||||
return;
|
||||
}
|
||||
|
||||
// Set reconnecting flag to prevent multiple simultaneous attempts
|
||||
isReconnectingRef.current = true;
|
||||
|
||||
// Clear terminal immediately to prevent showing last line
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
|
||||
// Increment attempt counter
|
||||
reconnectAttempts.current++;
|
||||
|
||||
// Show toast with current attempt number
|
||||
toast.info(t('terminal.reconnecting', { attempt: reconnectAttempts.current, max: maxReconnectAttempts }));
|
||||
|
||||
reconnectTimeoutRef.current = setTimeout(() => {
|
||||
// Check again if component is still mounted and should reconnect
|
||||
if (isUnmountingRef.current || shouldNotReconnectRef.current) {
|
||||
isReconnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we haven't exceeded max attempts during the timeout
|
||||
if (reconnectAttempts.current > maxReconnectAttempts) {
|
||||
isReconnectingRef.current = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (terminal && hostConfig) {
|
||||
// Clear terminal before reconnecting
|
||||
// Ensure terminal is clear before reconnecting
|
||||
terminal.clear();
|
||||
const cols = terminal.cols;
|
||||
const rows = terminal.rows;
|
||||
connectToHost(cols, rows);
|
||||
}
|
||||
|
||||
// Reset reconnecting flag after attempting connection
|
||||
isReconnectingRef.current = false;
|
||||
}, 2000 * reconnectAttempts.current); // Exponential backoff
|
||||
}
|
||||
|
||||
@@ -187,6 +218,8 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
wasDisconnectedBySSH.current = false;
|
||||
setConnectionError(null);
|
||||
shouldNotReconnectRef.current = false; // Reset reconnection flag
|
||||
isReconnectingRef.current = false; // Reset reconnecting flag
|
||||
setIsConnecting(true); // Set connecting state
|
||||
|
||||
setupWebSocketListeners(ws, cols, rows);
|
||||
}
|
||||
@@ -195,12 +228,27 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
function setupWebSocketListeners(ws: WebSocket, cols: number, rows: number) {
|
||||
ws.addEventListener('open', () => {
|
||||
setIsConnected(true);
|
||||
// Show reconnected toast if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
toast.success(t('terminal.reconnected'));
|
||||
// Don't set isConnected to true here - wait for actual SSH connection
|
||||
// Don't show reconnected toast here - wait for actual connection confirmation
|
||||
|
||||
// Set a timeout for SSH connection establishment
|
||||
connectionTimeoutRef.current = setTimeout(() => {
|
||||
if (!isConnected) {
|
||||
// SSH connection didn't establish within timeout
|
||||
// Clear terminal immediately when connection times out
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
reconnectAttempts.current = 0; // Reset on successful connection
|
||||
toast.error(t('terminal.connectionTimeout'));
|
||||
if (webSocketRef.current) {
|
||||
webSocketRef.current.close();
|
||||
}
|
||||
// Attempt reconnection if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
attemptReconnection();
|
||||
}
|
||||
}
|
||||
}, 10000); // 10 second timeout for SSH connection
|
||||
|
||||
ws.send(JSON.stringify({type: 'connectToHost', data: {cols, rows, hostConfig}}));
|
||||
terminal.onData((data) => {
|
||||
@@ -250,6 +298,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
errorMessage.toLowerCase().includes('network')) {
|
||||
toast.error(t('terminal.connectionError', { message: errorMessage }));
|
||||
setIsConnected(false);
|
||||
// Clear terminal immediately when connection error occurs
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
attemptReconnection();
|
||||
return;
|
||||
}
|
||||
@@ -258,9 +312,28 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
toast.error(t('terminal.error', { message: errorMessage }));
|
||||
} else if (msg.type === 'connected') {
|
||||
setIsConnected(true);
|
||||
setIsConnecting(false); // Clear connecting state
|
||||
// Clear connection timeout since SSH connection is established
|
||||
if (connectionTimeoutRef.current) {
|
||||
clearTimeout(connectionTimeoutRef.current);
|
||||
connectionTimeoutRef.current = null;
|
||||
}
|
||||
// Show reconnected toast if this was a reconnection attempt
|
||||
if (reconnectAttempts.current > 0) {
|
||||
toast.success(t('terminal.reconnected'));
|
||||
}
|
||||
// Reset reconnection counter and flags on successful connection
|
||||
reconnectAttempts.current = 0;
|
||||
isReconnectingRef.current = false;
|
||||
} else if (msg.type === 'disconnected') {
|
||||
wasDisconnectedBySSH.current = true;
|
||||
setIsConnected(false);
|
||||
// Clear terminal immediately when disconnected
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
// Attempt reconnection for disconnections
|
||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
attemptReconnection();
|
||||
@@ -273,6 +346,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
|
||||
ws.addEventListener('close', (event) => {
|
||||
setIsConnected(false);
|
||||
// Clear terminal immediately when connection closes
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
if (!wasDisconnectedBySSH.current && !isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
// Attempt reconnection for unexpected disconnections
|
||||
attemptReconnection();
|
||||
@@ -282,6 +361,12 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
ws.addEventListener('error', (event) => {
|
||||
setIsConnected(false);
|
||||
setConnectionError(t('terminal.websocketError'));
|
||||
// Clear terminal immediately when WebSocket error occurs
|
||||
if (terminal) {
|
||||
terminal.clear();
|
||||
}
|
||||
// Set connecting state immediately for reconnection
|
||||
setIsConnecting(true);
|
||||
// Attempt reconnection for WebSocket errors
|
||||
if (!isUnmountingRef.current && !shouldNotReconnectRef.current) {
|
||||
attemptReconnection();
|
||||
@@ -429,11 +514,14 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
return () => {
|
||||
isUnmountingRef.current = true;
|
||||
shouldNotReconnectRef.current = true;
|
||||
isReconnectingRef.current = false;
|
||||
setIsConnecting(false); // Clear connecting state
|
||||
resizeObserver.disconnect();
|
||||
element?.removeEventListener('contextmenu', handleContextMenu);
|
||||
if (notifyTimerRef.current) clearTimeout(notifyTimerRef.current);
|
||||
if (resizeTimeout.current) clearTimeout(resizeTimeout.current);
|
||||
if (reconnectTimeoutRef.current) clearTimeout(reconnectTimeoutRef.current);
|
||||
if (connectionTimeoutRef.current) clearTimeout(connectionTimeoutRef.current);
|
||||
if (pingIntervalRef.current) {
|
||||
clearInterval(pingIntervalRef.current);
|
||||
pingIntervalRef.current = null;
|
||||
@@ -474,15 +562,28 @@ export const Terminal = forwardRef<any, SSHTerminalProps>(function SSHTerminal(
|
||||
}, [splitScreen, isVisible, terminal]);
|
||||
|
||||
return (
|
||||
<div className="h-full w-full m-1 relative">
|
||||
{/* Terminal */}
|
||||
<div
|
||||
ref={xtermRef}
|
||||
className={`h-full w-full m-1 transition-opacity duration-200 ${visible && isVisible ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||
className={`h-full w-full transition-opacity duration-200 ${visible && isVisible && !isConnecting ? 'opacity-100' : 'opacity-0'} overflow-hidden`}
|
||||
onClick={() => {
|
||||
if (terminal && !splitScreen) {
|
||||
terminal.focus();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
|
||||
{/* Connecting State */}
|
||||
{isConnecting && (
|
||||
<div className="absolute inset-0 flex items-center justify-center bg-dark-bg">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="w-6 h-6 border-2 border-blue-400 border-t-transparent rounded-full animate-spin"></div>
|
||||
<span className="text-gray-300">{t('terminal.connecting')}</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -115,7 +115,6 @@ function AppContent() {
|
||||
isAuthenticated={isAuthenticated}
|
||||
authLoading={authLoading}
|
||||
onAuthSuccess={handleAuthSuccess}
|
||||
isTopbarOpen={isTopbarOpen}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,6 @@ interface HomepageProps {
|
||||
isAuthenticated: boolean;
|
||||
authLoading: boolean;
|
||||
onAuthSuccess: (authData: { isAdmin: boolean; username: string | null; userId: string | null }) => void;
|
||||
isTopbarOpen?: boolean;
|
||||
}
|
||||
|
||||
function getCookie(name: string) {
|
||||
@@ -30,8 +29,7 @@ export function Homepage({
|
||||
onSelectView,
|
||||
isAuthenticated,
|
||||
authLoading,
|
||||
onAuthSuccess,
|
||||
isTopbarOpen = true
|
||||
onAuthSuccess
|
||||
}: HomepageProps): React.ReactElement {
|
||||
const {t} = useTranslation();
|
||||
const [loggedIn, setLoggedIn] = useState(isAuthenticated);
|
||||
@@ -72,20 +70,10 @@ export function Homepage({
|
||||
}
|
||||
}, [isAuthenticated]);
|
||||
|
||||
const topOffset = isTopbarOpen ? 66 : 0;
|
||||
const topPadding = isTopbarOpen ? 66 : 0;
|
||||
|
||||
return (
|
||||
<div
|
||||
className="w-full min-h-svh relative transition-[padding-top] duration-300 ease-in-out"
|
||||
style={{ paddingTop: `${topPadding}px` }}>
|
||||
<div className="w-full h-full flex items-center justify-center">
|
||||
{!loggedIn ? (
|
||||
<div
|
||||
className="absolute left-0 w-full flex items-center justify-center"
|
||||
style={{
|
||||
top: `${topOffset}px`,
|
||||
height: `calc(100% - ${topOffset}px)`
|
||||
}}>
|
||||
<HomepageAuth
|
||||
setLoggedIn={setLoggedIn}
|
||||
setIsAdmin={setIsAdmin}
|
||||
@@ -97,14 +85,7 @@ export function Homepage({
|
||||
setDbError={setDbError}
|
||||
onAuthSuccess={onAuthSuccess}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div
|
||||
className="absolute left-0 w-full flex items-center justify-center"
|
||||
style={{
|
||||
top: `${topOffset}px`,
|
||||
height: `calc(100% - ${topOffset}px)`
|
||||
}}>
|
||||
<div className="flex flex-row items-center justify-center gap-8 relative z-10">
|
||||
<div className="flex flex-col items-center gap-6 w-[400px]">
|
||||
<HomepageUpdateLog
|
||||
@@ -150,7 +131,6 @@ export function Homepage({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -237,9 +237,14 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
|
||||
|
||||
// Handle auth token clearing
|
||||
if (status === 401) {
|
||||
const isElectron = (window as any).IS_ELECTRON === true || (window as any).electronAPI?.isElectron === true;
|
||||
if (isElectron) {
|
||||
localStorage.removeItem('jwt');
|
||||
} else {
|
||||
document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
|
||||
localStorage.removeItem('jwt');
|
||||
}
|
||||
}
|
||||
|
||||
return Promise.reject(error);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user