diff --git a/README.md b/README.md
index bba837ea..9ddf04d6 100644
--- a/README.md
+++ b/README.md
@@ -126,7 +126,7 @@ If you need help or want to request a feature with Termix, visit the [Issues](ht
Please be as detailed as possible in your issue, preferably written in English. You can also join the [Discord](https://discord.gg/jVQGdvHDrf) server and visit the support
channel, however, response times may be longer.
-# Show-off
+# Screenshots
@@ -145,7 +145,12 @@ channel, however, response times may be longer.
-
+
+
+
+
+
+
diff --git a/repo-images/Image 10.png b/repo-images/Image 10.png
new file mode 100644
index 00000000..efc0c012
Binary files /dev/null and b/repo-images/Image 10.png differ
diff --git a/repo-images/Image 9.png b/repo-images/Image 9.png
new file mode 100644
index 00000000..28d9e70f
Binary files /dev/null and b/repo-images/Image 9.png differ
diff --git a/src/backend/dashboard.ts b/src/backend/dashboard.ts
index 5df48fb6..2186a631 100644
--- a/src/backend/dashboard.ts
+++ b/src/backend/dashboard.ts
@@ -127,9 +127,18 @@ app.post("/activity/log", async (req, res) => {
});
}
- if (type !== "terminal" && type !== "file_manager") {
+ if (
+ ![
+ "terminal",
+ "file_manager",
+ "server_stats",
+ "tunnel",
+ "docker",
+ ].includes(type)
+ ) {
return res.status(400).json({
- error: "Invalid activity type. Must be 'terminal' or 'file_manager'",
+ error:
+ "Invalid activity type. Must be 'terminal', 'file_manager', 'server_stats', 'tunnel', or 'docker'",
});
}
diff --git a/src/backend/ssh/server-stats.ts b/src/backend/ssh/server-stats.ts
index e5bff949..55f28284 100644
--- a/src/backend/ssh/server-stats.ts
+++ b/src/backend/ssh/server-stats.ts
@@ -242,11 +242,6 @@ function cleanupMetricsSession(sessionId: string) {
} catch (error) {}
clearTimeout(session.timeout);
delete metricsSessions[sessionId];
-
- statsLogger.info("Metrics session cleaned up", {
- operation: "session_cleanup",
- sessionId,
- });
}
}
@@ -411,13 +406,6 @@ class SSHConnectionPool {
totpAttempts: 0,
};
- statsLogger.info("TOTP required for metrics collection", {
- operation: "metrics_totp_required",
- hostId: host.id,
- sessionId,
- prompt: prompts[totpPromptIndex].prompt,
- });
-
return;
} else if (host.password) {
const responses = prompts.map((p) => {
@@ -728,7 +716,7 @@ interface AuthFailureRecord {
class AuthFailureTracker {
private failures = new Map();
private maxRetries = 3;
- private backoffBase = 60000;
+ private backoffBase = 5000;
recordFailure(
hostId: number,
@@ -991,8 +979,12 @@ class PollingManager {
return result;
}
- async startPollingForHost(host: SSHHostWithCredentials): Promise {
+ async startPollingForHost(
+ host: SSHHostWithCredentials,
+ options?: { statusOnly?: boolean },
+ ): Promise {
const statsConfig = this.parseStatsConfig(host.statsConfig);
+ const statusOnly = options?.statusOnly ?? false;
const existingConfig = this.pollingConfigs.get(host.id);
@@ -1034,10 +1026,10 @@ class PollingManager {
this.statusStore.delete(host.id);
}
- if (statsConfig.metricsEnabled) {
+ if (!statusOnly && statsConfig.metricsEnabled) {
const intervalMs = statsConfig.metricsInterval * 1000;
- this.pollHostMetrics(host);
+ await this.pollHostMetrics(host);
config.metricsTimer = setInterval(() => {
const latestConfig = this.pollingConfigs.get(host.id);
@@ -1083,11 +1075,6 @@ class PollingManager {
}
private async pollHostMetrics(host: SSHHostWithCredentials): Promise {
- if (pollingBackoff.shouldSkip(host.id)) {
- const backoffInfo = pollingBackoff.getBackoffInfo(host.id);
- return;
- }
-
const refreshedHost = await fetchHostById(host.id, host.userId);
if (!refreshedHost) {
statsLogger.warn("Host not found during metrics polling", {
@@ -1102,6 +1089,13 @@ class PollingManager {
return;
}
+ const hasExistingMetrics = this.metricsStore.has(refreshedHost.id);
+
+ if (hasExistingMetrics && pollingBackoff.shouldSkip(host.id)) {
+ const backoffInfo = pollingBackoff.getBackoffInfo(host.id);
+ return;
+ }
+
try {
const metrics = await collectMetrics(refreshedHost);
this.metricsStore.set(refreshedHost.id, {
@@ -1118,7 +1112,7 @@ class PollingManager {
const latestConfig = this.pollingConfigs.get(refreshedHost.id);
if (latestConfig && latestConfig.statsConfig.metricsEnabled) {
const backoffInfo = pollingBackoff.getBackoffInfo(refreshedHost.id);
- statsLogger.warn("Failed to collect metrics for host", {
+ statsLogger.error("Failed to collect metrics for host", {
operation: "metrics_poll_failed",
hostId: refreshedHost.id,
hostName: refreshedHost.name,
@@ -1176,7 +1170,7 @@ class PollingManager {
const hosts = await fetchAllHosts(userId);
for (const host of hosts) {
- await this.startPollingForHost(host);
+ await this.startPollingForHost(host, { statusOnly: true });
}
}
@@ -1196,7 +1190,7 @@ class PollingManager {
}
for (const host of hosts) {
- await this.startPollingForHost(host);
+ await this.startPollingForHost(host, { statusOnly: true });
}
}
@@ -1538,7 +1532,6 @@ function buildSshConfig(host: SSHHostWithCredentials): ConnectConfig {
throw new Error(`Invalid SSH key format for host ${host.ip}`);
}
} else if (host.authType === "none") {
- // Allow "none" auth - SSH will handle via keyboard-interactive
} else {
throw new Error(
`Unsupported authentication type '${host.authType}' for host ${host.ip}`,
@@ -1622,8 +1615,11 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
}
return requestQueue.queueRequest(host.id, async () => {
+ const sessionKey = getSessionKey(host.id, host.userId!);
+ const existingSession = metricsSessions[sessionKey];
+
try {
- return await withSshConnection(host, async (client) => {
+ const collectFn = async (client: Client) => {
const cpu = await collectCpuMetrics(client);
const memory = await collectMemoryMetrics(client);
const disk = await collectDiskMetrics(client);
@@ -1655,7 +1651,20 @@ async function collectMetrics(host: SSHHostWithCredentials): Promise<{
metricsCache.set(host.id, result);
return result;
- });
+ };
+
+ if (existingSession && existingSession.isConnected) {
+ existingSession.activeOperations++;
+ try {
+ const result = await collectFn(existingSession.client);
+ existingSession.lastActive = Date.now();
+ return result;
+ } finally {
+ existingSession.activeOperations--;
+ }
+ } else {
+ return await withSshConnection(host, collectFn);
+ }
} catch (error) {
if (error instanceof Error) {
if (error.message.includes("TOTP authentication required")) {
@@ -1970,13 +1979,14 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
});
}
return;
- } else if (host.password) {
- const responses = prompts.map((p) =>
- /password/i.test(p.prompt) ? host.password || "" : "",
- );
- finish(responses);
} else {
- finish(prompts.map(() => ""));
+ const responses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && host.password) {
+ return host.password;
+ }
+ return "";
+ });
+ finish(responses);
}
},
);
@@ -2012,11 +2022,44 @@ app.post("/metrics/start/:id", validateHostId, async (req, res) => {
clearTimeout(timeout);
if (!isResolved) {
isResolved = true;
+ statsLogger.error("SSH connection error in metrics/start", {
+ operation: "metrics_start_ssh_error",
+ hostId: host.id,
+ error: error instanceof Error ? error.message : String(error),
+ });
reject(error);
}
});
- client.connect(config);
+ if (
+ host.useSocks5 &&
+ (host.socks5Host ||
+ (host.socks5ProxyChain && host.socks5ProxyChain.length > 0))
+ ) {
+ createSocks5Connection(host.ip, host.port, {
+ useSocks5: host.useSocks5,
+ socks5Host: host.socks5Host,
+ socks5Port: host.socks5Port,
+ socks5Username: host.socks5Username,
+ socks5Password: host.socks5Password,
+ socks5ProxyChain: host.socks5ProxyChain,
+ })
+ .then((socks5Socket) => {
+ if (socks5Socket) {
+ config.sock = socks5Socket;
+ }
+ client.connect(config);
+ })
+ .catch((error) => {
+ if (!isResolved) {
+ isResolved = true;
+ clearTimeout(timeout);
+ reject(error);
+ }
+ });
+ } else {
+ client.connect(config);
+ }
});
const result = await connectionPromise;
@@ -2129,6 +2172,25 @@ app.post("/metrics/connect-totp", async (req, res) => {
reject(new Error("TOTP verification timeout"));
}, 30000);
+ session.client.once(
+ "keyboard-interactive",
+ (name, instructions, instructionsLang, prompts, finish) => {
+ statsLogger.warn("Second keyboard-interactive received after TOTP", {
+ operation: "totp_second_keyboard_interactive",
+ hostId: session.hostId,
+ sessionId,
+ prompts: prompts.map((p) => p.prompt),
+ });
+ const secondResponses = prompts.map((p) => {
+ if (/password/i.test(p.prompt) && session.resolvedPassword) {
+ return session.resolvedPassword;
+ }
+ return "";
+ });
+ finish(secondResponses);
+ },
+ );
+
session.client.once("ready", () => {
clearTimeout(timeout);
resolve();
@@ -2136,6 +2198,12 @@ app.post("/metrics/connect-totp", async (req, res) => {
session.client.once("error", (error) => {
clearTimeout(timeout);
+ statsLogger.error("SSH client error after TOTP", {
+ operation: "totp_client_error",
+ hostId: session.hostId,
+ sessionId,
+ error: error instanceof Error ? error.message : String(error),
+ });
reject(error);
});
});
@@ -2159,15 +2227,15 @@ app.post("/metrics/connect-totp", async (req, res) => {
const host = await fetchHostById(session.hostId, userId);
if (host) {
- await pollingManager.startPollingForHost(host);
+ pollingManager.startPollingForHost(host).catch((error) => {
+ statsLogger.error("Failed to start polling after TOTP", {
+ operation: "totp_polling_start_error",
+ hostId: session.hostId,
+ error: error instanceof Error ? error.message : String(error),
+ });
+ });
}
- statsLogger.info("TOTP verified, metrics collection started", {
- operation: "totp_verified",
- hostId: session.hostId,
- sessionId,
- });
-
res.json({ success: true });
} catch (error) {
statsLogger.error("TOTP verification failed", {
diff --git a/src/locales/ar.json b/src/locales/ar.json
index 64414ac3..999c5446 100644
--- a/src/locales/ar.json
+++ b/src/locales/ar.json
@@ -2395,4 +2395,4 @@
"switchToLight": "قم بتشغيل الضوء",
"switchToDark": "التبديل إلى الوضع الداكن"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/bn.json b/src/locales/bn.json
index 83bd5605..dfb537de 100644
--- a/src/locales/bn.json
+++ b/src/locales/bn.json
@@ -2395,4 +2395,4 @@
"switchToLight": "আলোতে স্যুইচ করুন",
"switchToDark": "ডার্ক এ স্যুইচ করুন"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/cs.json b/src/locales/cs.json
index 01d3d314..629c19d0 100644
--- a/src/locales/cs.json
+++ b/src/locales/cs.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Přepnout na světlo",
"switchToDark": "Přepnout na tmavou"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/de.json b/src/locales/de.json
index 88299a68..acfb8318 100644
--- a/src/locales/de.json
+++ b/src/locales/de.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Auf Licht umschalten",
"switchToDark": "Auf Dunkel umschalten"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/el.json b/src/locales/el.json
index c208aa06..2dfc790f 100644
--- a/src/locales/el.json
+++ b/src/locales/el.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Αλλαγή σε Φως",
"switchToDark": "Αλλαγή σε Σκούρο"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/en.json b/src/locales/en.json
index 2dfcdc4c..3ec6e1f6 100644
--- a/src/locales/en.json
+++ b/src/locales/en.json
@@ -1696,6 +1696,7 @@
"totpInvalidCode": "Invalid verification code",
"totpCancelled": "Metrics collection cancelled",
"authenticationFailed": "Authentication failed",
+ "noneAuthNotSupported": "Server Stats does not support 'none' authentication type.",
"load": "Load",
"editLayout": "Edit Layout",
"cancelEdit": "Cancel",
diff --git a/src/locales/es.json b/src/locales/es.json
index 4fc4adaa..b201fb50 100644
--- a/src/locales/es.json
+++ b/src/locales/es.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Cambiar a la luz",
"switchToDark": "Cambiar a oscuro"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/fr.json b/src/locales/fr.json
index 1b84e2a5..bd19263e 100644
--- a/src/locales/fr.json
+++ b/src/locales/fr.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Passer à la lumière",
"switchToDark": "Passer au mode sombre"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/he.json b/src/locales/he.json
index 993a3f88..2fdb907e 100644
--- a/src/locales/he.json
+++ b/src/locales/he.json
@@ -2395,4 +2395,4 @@
"switchToLight": "מעבר לתאורה",
"switchToDark": "עבור למצב כהה"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/hi.json b/src/locales/hi.json
index f61b94d6..12cad703 100644
--- a/src/locales/hi.json
+++ b/src/locales/hi.json
@@ -2395,4 +2395,4 @@
"switchToLight": "लाइट पर स्विच करें",
"switchToDark": "डार्क मोड पर स्विच करें"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/id.json b/src/locales/id.json
index 993fabae..16870b66 100644
--- a/src/locales/id.json
+++ b/src/locales/id.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Beralih ke Cahaya",
"switchToDark": "Beralih ke Gelap"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/it.json b/src/locales/it.json
index b66b0b44..26916589 100644
--- a/src/locales/it.json
+++ b/src/locales/it.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Passa alla luce",
"switchToDark": "Passa a Scuro"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/ja.json b/src/locales/ja.json
index cfd8d2d3..97e3dfcb 100644
--- a/src/locales/ja.json
+++ b/src/locales/ja.json
@@ -2395,4 +2395,4 @@
"switchToLight": "光に切り替える",
"switchToDark": "ダークに切り替える"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/ko.json b/src/locales/ko.json
index b540e1a2..21bc0672 100644
--- a/src/locales/ko.json
+++ b/src/locales/ko.json
@@ -2395,4 +2395,4 @@
"switchToLight": "조명으로 전환",
"switchToDark": "어둡게 전환"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/nb.json b/src/locales/nb.json
index 46620277..5aea52c1 100644
--- a/src/locales/nb.json
+++ b/src/locales/nb.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Bytt til lys",
"switchToDark": "Bytt til mørkt"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/nl.json b/src/locales/nl.json
index e1339db7..34578278 100644
--- a/src/locales/nl.json
+++ b/src/locales/nl.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Schakel over naar Licht",
"switchToDark": "Schakel over naar Donker"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/pl.json b/src/locales/pl.json
index d3164c39..8e65ebcd 100644
--- a/src/locales/pl.json
+++ b/src/locales/pl.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Przełącz na światło",
"switchToDark": "Przełącz na ciemność"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/pt.json b/src/locales/pt.json
index 31f59486..ac2644bf 100644
--- a/src/locales/pt.json
+++ b/src/locales/pt.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Alternar para luz",
"switchToDark": "Mudar para o modo escuro"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/ro.json b/src/locales/ro.json
index fe355555..11dec861 100644
--- a/src/locales/ro.json
+++ b/src/locales/ro.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Comutare la lumină",
"switchToDark": "Comutare la Întuneric"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/ru.json b/src/locales/ru.json
index a85c965b..b105cedc 100644
--- a/src/locales/ru.json
+++ b/src/locales/ru.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Переключиться на свет",
"switchToDark": "Переключиться на темный"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/sv.json b/src/locales/sv.json
index f75bb091..c3ac2df3 100644
--- a/src/locales/sv.json
+++ b/src/locales/sv.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Växla till ljus",
"switchToDark": "Växla till mörkt"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/th.json b/src/locales/th.json
index 4165c102..e9f089d9 100644
--- a/src/locales/th.json
+++ b/src/locales/th.json
@@ -2395,4 +2395,4 @@
"switchToLight": "เปลี่ยนเป็นโหมดสว่าง",
"switchToDark": "เปลี่ยนเป็นโหมดมืด"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/tr.json b/src/locales/tr.json
index 51ef6584..fc84cdd0 100644
--- a/src/locales/tr.json
+++ b/src/locales/tr.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Işığa geç",
"switchToDark": "Koyu moda geç"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/uk.json b/src/locales/uk.json
index a1abac58..77e6cfbc 100644
--- a/src/locales/uk.json
+++ b/src/locales/uk.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Перейти на світлий режим",
"switchToDark": "Перейти на темний режим"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/vi.json b/src/locales/vi.json
index 50c0f1a6..a6fecbdd 100644
--- a/src/locales/vi.json
+++ b/src/locales/vi.json
@@ -2395,4 +2395,4 @@
"switchToLight": "Chuyển sang chế độ sáng",
"switchToDark": "Chuyển sang chế độ Tối"
}
-}
\ No newline at end of file
+}
diff --git a/src/locales/zh.json b/src/locales/zh.json
index 0e04e585..5342be6c 100644
--- a/src/locales/zh.json
+++ b/src/locales/zh.json
@@ -2395,4 +2395,4 @@
"switchToLight": "切换到灯光",
"switchToDark": "切换到黑暗模式"
}
-}
\ No newline at end of file
+}
diff --git a/src/types/index.ts b/src/types/index.ts
index bf0ecb1a..786b0694 100644
--- a/src/types/index.ts
+++ b/src/types/index.ts
@@ -389,7 +389,7 @@ export interface TabContextTab {
| "home"
| "terminal"
| "ssh_manager"
- | "server"
+ | "server_stats"
| "admin"
| "file_manager"
| "user_profile"
diff --git a/src/ui/desktop/DesktopApp.tsx b/src/ui/desktop/DesktopApp.tsx
index 24451ae8..77d1d7ba 100644
--- a/src/ui/desktop/DesktopApp.tsx
+++ b/src/ui/desktop/DesktopApp.tsx
@@ -180,7 +180,7 @@ function AppContent() {
const currentTabData = tabs.find((tab) => tab.id === currentTab);
const showTerminalView =
currentTabData?.type === "terminal" ||
- currentTabData?.type === "server" ||
+ currentTabData?.type === "server_stats" ||
currentTabData?.type === "file_manager" ||
currentTabData?.type === "tunnel" ||
currentTabData?.type === "docker";
diff --git a/src/ui/desktop/apps/command-palette/CommandPalette.tsx b/src/ui/desktop/apps/command-palette/CommandPalette.tsx
index eb9d9f21..a1a61bf4 100644
--- a/src/ui/desktop/apps/command-palette/CommandPalette.tsx
+++ b/src/ui/desktop/apps/command-palette/CommandPalette.tsx
@@ -223,7 +223,7 @@ export function CommandPalette({
const title = host.name?.trim()
? host.name
: `${host.username}@${host.ip}:${host.port}`;
- addTab({ type: "server", title, hostConfig: host });
+ addTab({ type: "server_stats", title, hostConfig: host });
setIsOpen(false);
};
diff --git a/src/ui/desktop/apps/dashboard/Dashboard.tsx b/src/ui/desktop/apps/dashboard/Dashboard.tsx
index 1932e423..d30f3d79 100644
--- a/src/ui/desktop/apps/dashboard/Dashboard.tsx
+++ b/src/ui/desktop/apps/dashboard/Dashboard.tsx
@@ -36,6 +36,9 @@ import {
Loader2,
Terminal,
FolderOpen,
+ Activity,
+ Container,
+ ArrowDownUp,
} from "lucide-react";
import { Status } from "@/components/ui/shadcn-io/status";
import { BsLightning } from "react-icons/bs";
@@ -297,6 +300,24 @@ export function Dashboard({
title: item.hostName,
hostConfig: host,
});
+ } else if (item.type === "server_stats") {
+ addTab({
+ type: "server_stats",
+ title: item.hostName,
+ hostConfig: host,
+ });
+ } else if (item.type === "tunnel") {
+ addTab({
+ type: "tunnel",
+ title: item.hostName,
+ hostConfig: host,
+ });
+ } else if (item.type === "docker") {
+ addTab({
+ type: "docker",
+ title: item.hostName,
+ hostConfig: host,
+ });
}
});
};
@@ -307,7 +328,7 @@ export function Dashboard({
if (!host) return;
addTab({
- type: "server",
+ type: "server_stats",
title: serverName,
hostConfig: host,
});
@@ -544,7 +565,7 @@ export function Dashboard({