diff --git a/docker/nginx.conf b/docker/nginx.conf
index 728aad3b..7afc334f 100644
--- a/docker/nginx.conf
+++ b/docker/nginx.conf
@@ -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;
diff --git a/electron/main.cjs b/electron/main.cjs
index dd5f2c21..677b0df1 100644
--- a/electron/main.cjs
+++ b/electron/main.cjs
@@ -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 = [
diff --git a/src/locales/en/translation.json b/src/locales/en/translation.json
index a2817a37..a4a224d5 100644
--- a/src/locales/en/translation.json
+++ b/src/locales/en/translation.json
@@ -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}}",
diff --git a/src/locales/zh/translation.json b/src/locales/zh/translation.json
index 89330ab9..e500d3f2 100644
--- a/src/locales/zh/translation.json
+++ b/src/locales/zh/translation.json
@@ -58,9 +58,8 @@
"keyPassword": "密钥密码(可选)",
"keyType": "密钥类型",
"keyTypeRSA": "RSA",
- "keyTypeECDSA": "ECDSA",
+ "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": "未知错误",
diff --git a/src/ui/Desktop/Apps/Server/Server.tsx b/src/ui/Desktop/Apps/Server/Server.tsx
index 3f872712..84d43f58 100644
--- a/src/ui/Desktop/Apps/Server/Server.tsx
+++ b/src/ui/Desktop/Apps/Server/Server.tsx
@@ -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 && (
-
- )}
@@ -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 && (
-
- )}
@@ -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 && (
-
- )}
diff --git a/src/ui/Desktop/Apps/Terminal/Terminal.tsx b/src/ui/Desktop/Apps/Terminal/Terminal.tsx
index ab980ae3..5a448760 100644
--- a/src/ui/Desktop/Apps/Terminal/Terminal.tsx
+++ b/src/ui/Desktop/Apps/Terminal/Terminal.tsx
@@ -29,6 +29,7 @@ export const Terminal = forwardRef
(function SSHTerminal(
const pingIntervalRef = useRef(null);
const [visible, setVisible] = useState(false);
const [isConnected, setIsConnected] = useState(false);
+ const [isConnecting, setIsConnecting] = useState(false);
const [connectionError, setConnectionError] = useState(null);
const isVisibleRef = useRef(false);
const reconnectTimeoutRef = useRef(null);
@@ -36,7 +37,8 @@ export const Terminal = forwardRef(function SSHTerminal(
const maxReconnectAttempts = 3;
const isUnmountingRef = useRef(false);
const shouldNotReconnectRef = useRef(false);
-
+ const isReconnectingRef = useRef(false);
+ const connectionTimeoutRef = useRef(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(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(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(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(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(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'));
- }
- reconnectAttempts.current = 0; // Reset on successful connection
+ // 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();
+ }
+ 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(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(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(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(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(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(function SSHTerminal(
}, [splitScreen, isVisible, terminal]);
return (
- {
- if (terminal && !splitScreen) {
- terminal.focus();
- }
- }}
- />
+
+ {/* Terminal */}
+
{
+ if (terminal && !splitScreen) {
+ terminal.focus();
+ }
+ }}
+ />
+
+ {/* Connecting State */}
+ {isConnecting && (
+
+
+
+
{t('terminal.connecting')}
+
+
+ )}
+
);
});
diff --git a/src/ui/Desktop/DesktopApp.tsx b/src/ui/Desktop/DesktopApp.tsx
index b0b84231..8f4ef088 100644
--- a/src/ui/Desktop/DesktopApp.tsx
+++ b/src/ui/Desktop/DesktopApp.tsx
@@ -115,7 +115,6 @@ function AppContent() {
isAuthenticated={isAuthenticated}
authLoading={authLoading}
onAuthSuccess={handleAuthSuccess}
- isTopbarOpen={isTopbarOpen}
/>
)}
diff --git a/src/ui/Desktop/Homepage/Homepage.tsx b/src/ui/Desktop/Homepage/Homepage.tsx
index 8e4ea4f3..3ee99521 100644
--- a/src/ui/Desktop/Homepage/Homepage.tsx
+++ b/src/ui/Desktop/Homepage/Homepage.tsx
@@ -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,82 +70,64 @@ export function Homepage({
}
}, [isAuthenticated]);
- const topOffset = isTopbarOpen ? 66 : 0;
- const topPadding = isTopbarOpen ? 66 : 0;
return (
-
+
{!loggedIn ? (
-
-
-
+
) : (
-
-
-
-
+
+
+
-
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
diff --git a/src/ui/main-axios.ts b/src/ui/main-axios.ts
index 0a612c3a..a6daa8c2 100644
--- a/src/ui/main-axios.ts
+++ b/src/ui/main-axios.ts
@@ -237,8 +237,13 @@ function createApiInstance(baseURL: string, serviceName: string = 'API'): AxiosI
// Handle auth token clearing
if (status === 401) {
- document.cookie = 'jwt=; expires=Thu, 01 Jan 1970 00:00:00 UTC; path=/;';
- localStorage.removeItem('jwt');
+ 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);