diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml
index 04566d27..d23030df 100644
--- a/docker/docker-compose.yml
+++ b/docker/docker-compose.yml
@@ -21,6 +21,15 @@ services:
PORT: "4300"
ENABLE_SSL: "true"
SSL_DOMAIN: "termix-dev.karmaa.site"
+ # Resource limits for better large file handling
+ deploy:
+ resources:
+ limits:
+ memory: 2G
+ cpus: '1.0'
+ reservations:
+ memory: 512M
+ cpus: '0.5'
volumes:
termix-dev-data:
diff --git a/docker/nginx-https.conf b/docker/nginx-https.conf
index 44aa38fb..02f425e2 100644
--- a/docker/nginx-https.conf
+++ b/docker/nginx-https.conf
@@ -186,12 +186,23 @@ http {
}
location /ssh/file_manager/ssh/ {
+ client_max_body_size 5G;
+ client_body_timeout 300s;
+ client_header_timeout 300s;
+
proxy_pass http://127.0.0.1:30004;
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;
+
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+
+ proxy_request_buffering off;
+ proxy_buffering off;
}
location /health {
diff --git a/docker/nginx.conf b/docker/nginx.conf
index 349e988f..e5abf3cb 100644
--- a/docker/nginx.conf
+++ b/docker/nginx.conf
@@ -172,12 +172,23 @@ http {
}
location /ssh/file_manager/ssh/ {
+ client_max_body_size 5G;
+ client_body_timeout 300s;
+ client_header_timeout 300s;
+
proxy_pass http://127.0.0.1:30004;
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;
+
+ proxy_connect_timeout 60s;
+ proxy_send_timeout 300s;
+ proxy_read_timeout 300s;
+
+ proxy_request_buffering off;
+ proxy_buffering off;
}
location /health {
diff --git a/electron/main.cjs b/electron/main.cjs
index bf63ef6e..958fba34 100644
--- a/electron/main.cjs
+++ b/electron/main.cjs
@@ -97,6 +97,163 @@ ipcMain.handle("get-app-version", () => {
return app.getVersion();
});
+// GitHub API service for version checking
+const GITHUB_API_BASE = "https://api.github.com";
+const REPO_OWNER = "LukeGus";
+const REPO_NAME = "Termix";
+
+// Simple cache for GitHub API responses
+const githubCache = new Map();
+const CACHE_DURATION = 30 * 60 * 1000; // 30 minutes
+
+async function fetchGitHubAPI(endpoint, cacheKey) {
+ // Check cache first
+ const cached = githubCache.get(cacheKey);
+ if (cached && Date.now() - cached.timestamp < CACHE_DURATION) {
+ return {
+ data: cached.data,
+ cached: true,
+ cache_age: Date.now() - cached.timestamp,
+ };
+ }
+
+ try {
+ let fetch;
+ try {
+ fetch = globalThis.fetch || require("node-fetch");
+ } catch (e) {
+ const https = require("https");
+ const http = require("http");
+ const { URL } = require("url");
+
+ fetch = (url, options = {}) => {
+ return new Promise((resolve, reject) => {
+ const urlObj = new URL(url);
+ const isHttps = urlObj.protocol === "https:";
+ const client = isHttps ? https : http;
+
+ const req = client.request(
+ url,
+ {
+ method: options.method || "GET",
+ headers: options.headers || {},
+ timeout: options.timeout || 10000,
+ },
+ (res) => {
+ let data = "";
+ res.on("data", (chunk) => (data += chunk));
+ res.on("end", () => {
+ resolve({
+ ok: res.statusCode >= 200 && res.statusCode < 300,
+ status: res.statusCode,
+ text: () => Promise.resolve(data),
+ json: () => Promise.resolve(JSON.parse(data)),
+ });
+ });
+ },
+ );
+
+ req.on("error", reject);
+ req.on("timeout", () => {
+ req.destroy();
+ reject(new Error("Request timeout"));
+ });
+
+ if (options.body) {
+ req.write(options.body);
+ }
+ req.end();
+ });
+ };
+ }
+
+ const response = await fetch(`${GITHUB_API_BASE}${endpoint}`, {
+ headers: {
+ Accept: "application/vnd.github+json",
+ "User-Agent": "TermixElectronUpdateChecker/1.0",
+ "X-GitHub-Api-Version": "2022-11-28",
+ },
+ timeout: 10000,
+ });
+
+ if (!response.ok) {
+ throw new Error(
+ `GitHub API error: ${response.status} ${response.statusText}`,
+ );
+ }
+
+ const data = await response.json();
+
+ // Cache the response
+ githubCache.set(cacheKey, {
+ data,
+ timestamp: Date.now(),
+ });
+
+ return {
+ data: data,
+ cached: false,
+ };
+ } catch (error) {
+ console.error("Failed to fetch from GitHub API:", error);
+ throw error;
+ }
+}
+
+// Check for Electron app updates
+ipcMain.handle("check-electron-update", async () => {
+ try {
+ const localVersion = app.getVersion();
+ console.log(`Checking for updates. Local version: ${localVersion}`);
+
+ const releaseData = await fetchGitHubAPI(
+ `/repos/${REPO_OWNER}/${REPO_NAME}/releases/latest`,
+ "latest_release_electron"
+ );
+
+ const rawTag = releaseData.data.tag_name || releaseData.data.name || "";
+ const remoteVersionMatch = rawTag.match(/(\d+\.\d+(\.\d+)?)/);
+ const remoteVersion = remoteVersionMatch ? remoteVersionMatch[1] : null;
+
+ if (!remoteVersion) {
+ console.warn("Remote version not found in GitHub response:", rawTag);
+ return {
+ success: false,
+ error: "Remote version not found",
+ localVersion,
+ };
+ }
+
+ const isUpToDate = localVersion === remoteVersion;
+
+ const result = {
+ success: true,
+ status: isUpToDate ? "up_to_date" : "requires_update",
+ localVersion: localVersion,
+ remoteVersion: remoteVersion,
+ latest_release: {
+ tag_name: releaseData.data.tag_name,
+ name: releaseData.data.name,
+ published_at: releaseData.data.published_at,
+ html_url: releaseData.data.html_url,
+ body: releaseData.data.body,
+ },
+ cached: releaseData.cached,
+ cache_age: releaseData.cache_age,
+ };
+
+ console.log(`Version check result: ${result.status}`);
+ return result;
+ } catch (error) {
+ console.error("Version check failed:", error);
+ return {
+ success: false,
+ error: error.message,
+ localVersion: app.getVersion(),
+ };
+ }
+});
+
ipcMain.handle("get-platform", () => {
return process.platform;
});
diff --git a/electron/preload.js b/electron/preload.js
index 4c1087fc..dbdce90c 100644
--- a/electron/preload.js
+++ b/electron/preload.js
@@ -3,6 +3,7 @@ const { contextBridge, ipcRenderer } = require("electron");
contextBridge.exposeInMainWorld("electronAPI", {
getAppVersion: () => ipcRenderer.invoke("get-app-version"),
getPlatform: () => ipcRenderer.invoke("get-platform"),
+ checkElectronUpdate: () => ipcRenderer.invoke("check-electron-update"),
getServerConfig: () => ipcRenderer.invoke("get-server-config"),
saveServerConfig: (config) =>
diff --git a/src/backend/ssh/file-manager.ts b/src/backend/ssh/file-manager.ts
index c1fb0f98..835c7c26 100644
--- a/src/backend/ssh/file-manager.ts
+++ b/src/backend/ssh/file-manager.ts
@@ -860,7 +860,22 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
.json({ error: "File path, name, and content are required" });
}
+ // Update last active time and extend keepalive for large file operations
sshConn.lastActive = Date.now();
+
+ // For large files, extend the keepalive interval to prevent connection drops
+ const contentSize = typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : content.length;
+ if (contentSize > 10 * 1024 * 1024) { // 10MB threshold
+ fileLogger.info("Large file upload detected, extending SSH keepalive", {
+ operation: "file_upload",
+ sessionId,
+ fileName,
+ fileSize: contentSize,
+ });
+ // Extend keepalive interval for large files
+ // Note: SSH2 client doesn't expose config directly, but we can set keepalive
+ sshConn.client.setKeepAlive(true, 10000); // 10 seconds
+ }
const fullPath = filePath.endsWith("/")
? filePath + fileName
@@ -906,6 +921,13 @@ app.post("/ssh/file_manager/ssh/uploadFile", async (req, res) => {
hasError = true;
fileLogger.warn(
`SFTP write failed, trying fallback method: ${streamErr.message}`,
+ {
+ operation: "file_upload",
+ sessionId,
+ fileName,
+ fileSize: contentSize,
+ error: streamErr.message,
+ }
);
tryFallbackMethod();
});
diff --git a/src/components/ui/version-alert.tsx b/src/components/ui/version-alert.tsx
new file mode 100644
index 00000000..0bfae702
--- /dev/null
+++ b/src/components/ui/version-alert.tsx
@@ -0,0 +1,122 @@
+import React from "react";
+import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
+import { Button } from "@/components/ui/button.tsx";
+import { ExternalLink, Download, X, AlertTriangle } from "lucide-react";
+import { useTranslation } from "react-i18next";
+
+interface VersionAlertProps {
+ updateInfo: {
+ success: boolean;
+ status?: "up_to_date" | "requires_update";
+ localVersion?: string;
+ remoteVersion?: string;
+ latest_release?: {
+ tag_name: string;
+ name: string;
+ published_at: string;
+ html_url: string;
+ body: string;
+ };
+ cached?: boolean;
+ cache_age?: number;
+ error?: string;
+ };
+ onDismiss?: () => void;
+ onDownload?: () => void;
+ showDismiss?: boolean;
+}
+
+export function VersionAlert({
+ updateInfo,
+ onDismiss,
+ onDownload,
+ showDismiss = true,
+}: VersionAlertProps) {
+ const { t } = useTranslation();
+
+ if (!updateInfo.success) {
+ return (
+