Add versioning system to electron, update nginx configurations for file uploads, fix UI issues in file manager

This commit is contained in:
LukeGus
2025-09-26 16:28:37 -05:00
parent 9b12515676
commit 0da4652a31
13 changed files with 501 additions and 10 deletions

View File

@@ -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();
});

View File

@@ -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 (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t("versionCheck.error")}</AlertTitle>
<AlertDescription>
{updateInfo.error || t("versionCheck.checkFailed")}
</AlertDescription>
</Alert>
);
}
if (updateInfo.status === "up_to_date") {
return (
<Alert>
<Download className="h-4 w-4" />
<AlertTitle>{t("versionCheck.upToDate")}</AlertTitle>
<AlertDescription>
{t("versionCheck.currentVersion", { version: updateInfo.localVersion })}
</AlertDescription>
</Alert>
);
}
if (updateInfo.status === "requires_update") {
return (
<Alert variant="destructive">
<AlertTriangle className="h-4 w-4" />
<AlertTitle>{t("versionCheck.updateAvailable")}</AlertTitle>
<AlertDescription className="space-y-3">
<div>
{t("versionCheck.newVersionAvailable", {
current: updateInfo.localVersion,
latest: updateInfo.remoteVersion,
})}
</div>
{updateInfo.latest_release && (
<div className="text-sm text-muted-foreground">
<div className="font-medium">{updateInfo.latest_release.name}</div>
<div className="text-xs">
{t("versionCheck.releasedOn", {
date: new Date(updateInfo.latest_release.published_at).toLocaleDateString(),
})}
</div>
</div>
)}
<div className="flex gap-2 pt-2">
{updateInfo.latest_release?.html_url && (
<Button
variant="outline"
size="sm"
onClick={() => {
if (onDownload) {
onDownload();
} else {
window.open(updateInfo.latest_release!.html_url, "_blank");
}
}}
className="flex items-center gap-1"
>
<ExternalLink className="h-3 w-3" />
{t("versionCheck.downloadUpdate")}
</Button>
)}
{showDismiss && onDismiss && (
<Button
variant="ghost"
size="sm"
onClick={onDismiss}
className="flex items-center gap-1"
>
<X className="h-3 w-3" />
{t("versionCheck.dismiss")}
</Button>
)}
</div>
</AlertDescription>
</Alert>
);
}
return null;
}

View File

@@ -202,6 +202,19 @@
"saveConfig": "Save Configuration",
"helpText": "Enter the URL where your Termix server is running (e.g., http://localhost:30001 or https://your-server.com)"
},
"versionCheck": {
"error": "Version Check Error",
"checkFailed": "Failed to check for updates",
"upToDate": "App is Up to Date",
"currentVersion": "You are running version {{version}}",
"updateAvailable": "Update Available",
"newVersionAvailable": "A new version is available! You are running {{current}}, but {{latest}} is available.",
"releasedOn": "Released on {{date}}",
"downloadUpdate": "Download Update",
"dismiss": "Dismiss",
"checking": "Checking for updates...",
"checkUpdates": "Check for Updates"
},
"common": {
"close": "Close",
"minimize": "Minimize",
@@ -702,6 +715,7 @@
"uploading": "Uploading...",
"downloading": "Downloading...",
"uploadingFile": "Uploading {{name}}...",
"uploadingLargeFile": "Uploading large file {{name}} ({{size}})...",
"downloadingFile": "Downloading {{name}}...",
"creatingFile": "Creating {{name}}...",
"creatingFolder": "Creating {{name}}...",

View File

@@ -200,6 +200,19 @@
"saveConfig": "保存配置",
"helpText": "输入您的 Termix 服务器运行地址例如http://localhost:30001 或 https://your-server.com"
},
"versionCheck": {
"error": "版本检查错误",
"checkFailed": "检查更新失败",
"upToDate": "应用已是最新版本",
"currentVersion": "您正在运行版本 {{version}}",
"updateAvailable": "有可用更新",
"newVersionAvailable": "有新版本可用!您正在运行 {{current}},但 {{latest}} 已可用。",
"releasedOn": "发布于 {{date}}",
"downloadUpdate": "下载更新",
"dismiss": "忽略",
"checking": "正在检查更新...",
"checkUpdates": "检查更新"
},
"common": {
"close": "关闭",
"minimize": "最小化",
@@ -724,6 +737,7 @@
"uploading": "上传中...",
"downloading": "下载中...",
"uploadingFile": "正在上传 {{name}}...",
"uploadingLargeFile": "正在上传大文件 {{name}} ({{size}})...",
"downloadingFile": "正在下载 {{name}}...",
"creatingFile": "正在创建 {{name}}...",
"creatingFolder": "正在创建 {{name}}...",

View File

@@ -62,6 +62,24 @@ interface CreateIntent {
currentName: string;
}
// Format file size helper function
function formatFileSize(bytes?: number): string {
if (bytes === undefined || bytes === null) return "-";
if (bytes === 0) return "0 B";
const units = ["B", "KB", "MB", "GB", "TB"];
let size = bytes;
let unitIndex = 0;
while (size >= 1024 && unitIndex < units.length - 1) {
size /= 1024;
unitIndex++;
}
const formattedSize = size < 10 && unitIndex > 0 ? size.toFixed(1) : Math.round(size).toString();
return `${formattedSize} ${units[unitIndex]}`;
}
// Internal component, uses window manager
function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const { openWindow } = useWindowManager();
@@ -226,13 +244,12 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
const handleFileDragEnd = useCallback(
(e: DragEvent, draggedFiles: FileItem[]) => {
// More conservative detection - only trigger download if clearly outside window
const margin = 10; // Very small margin to reduce false positives
// Allow dragging off screen on all sides - only check if completely outside window bounds
const isOutside =
e.clientX < margin ||
e.clientX > window.innerWidth - margin ||
e.clientY < margin ||
e.clientY > window.innerHeight - margin;
e.clientX < 0 ||
e.clientX > window.innerWidth ||
e.clientY < 0 ||
e.clientY > window.innerHeight;
// Only trigger download if clearly outside the window bounds
if (isOutside) {
@@ -478,6 +495,20 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
async function handleUploadFile(file: File) {
if (!sshSessionId) return;
// Show progress for large files (>10MB)
const isLargeFile = file.size > 10 * 1024 * 1024;
let progressToast: any = null;
if (isLargeFile) {
progressToast = toast.loading(
t("fileManager.uploadingLargeFile", {
name: file.name,
size: formatFileSize(file.size)
}),
{ duration: Infinity }
);
}
try {
// Ensure SSH connection is valid
await ensureSSHConnection();
@@ -493,8 +524,9 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
file.type === "application/json" ||
file.type === "application/javascript" ||
file.type === "application/xml" ||
file.type === "image/svg+xml" ||
file.name.match(
/\.(txt|json|js|ts|jsx|tsx|css|html|htm|xml|yaml|yml|md|py|java|c|cpp|h|sh|bat|ps1)$/i,
/\.(txt|json|js|ts|jsx|tsx|css|scss|less|html|htm|xml|svg|yaml|yml|md|markdown|mdown|mkdn|mdx|py|java|c|cpp|h|sh|bash|zsh|bat|ps1|toml|ini|conf|config|sql|vue|svelte)$/i,
);
if (isTextFile) {
@@ -532,11 +564,22 @@ function FileManagerContent({ initialHost, onClose }: FileManagerProps) {
currentHost?.id,
undefined, // userId - will be handled by backend
);
// Dismiss progress toast if it exists
if (progressToast) {
toast.dismiss(progressToast);
}
toast.success(
t("fileManager.fileUploadedSuccessfully", { name: file.name }),
);
handleRefreshDirectory();
} catch (error: any) {
// Dismiss progress toast if it exists
if (progressToast) {
toast.dismiss(progressToast);
}
if (
error.message?.includes("connection") ||
error.message?.includes("established")

View File

@@ -1365,8 +1365,8 @@ export function FileManagerGrid({
<div
className="fixed pointer-events-none"
style={{
left: Math.min(Math.max(dragState.mousePosition.x + 40, 10), window.innerWidth - 300),
top: Math.max(Math.min(dragState.mousePosition.y - 80, window.innerHeight - 100), 10),
left: Math.min(Math.max(dragState.mousePosition.x + 40, 0), window.innerWidth - 300),
top: Math.max(Math.min(dragState.mousePosition.y - 80, window.innerHeight - 100), 0),
zIndex: 999999,
}}
>

View File

@@ -3,14 +3,16 @@ import { Button } from "@/components/ui/button.tsx";
import { Input } from "@/components/ui/input.tsx";
import { Label } from "@/components/ui/label.tsx";
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert.tsx";
import { VersionAlert } from "@/components/ui/version-alert.tsx";
import { useTranslation } from "react-i18next";
import {
getServerConfig,
saveServerConfig,
testServerConnection,
checkElectronUpdate,
type ServerConfig,
} from "@/ui/main-axios.ts";
import { CheckCircle, XCircle, Server, Wifi } from "lucide-react";
import { CheckCircle, XCircle, Server, Wifi, RefreshCw } from "lucide-react";
interface ServerConfigProps {
onServerConfigured: (serverUrl: string) => void;
@@ -31,9 +33,13 @@ export function ServerConfig({
const [connectionStatus, setConnectionStatus] = useState<
"unknown" | "success" | "error"
>("unknown");
const [versionInfo, setVersionInfo] = useState<any>(null);
const [versionChecking, setVersionChecking] = useState(false);
const [versionDismissed, setVersionDismissed] = useState(false);
useEffect(() => {
loadServerConfig();
checkForUpdates();
}, []);
const loadServerConfig = async () => {
@@ -46,6 +52,29 @@ export function ServerConfig({
} catch (error) {}
};
const checkForUpdates = async () => {
setVersionChecking(true);
try {
const updateInfo = await checkElectronUpdate();
setVersionInfo(updateInfo);
} catch (error) {
console.error("Failed to check for updates:", error);
setVersionInfo({ success: false, error: "Check failed" });
} finally {
setVersionChecking(false);
}
};
const handleVersionDismiss = () => {
setVersionDismissed(true);
};
const handleDownloadUpdate = () => {
if (versionInfo?.latest_release?.html_url) {
window.open(versionInfo.latest_release.html_url, "_blank");
}
};
const handleTestConnection = async () => {
if (!serverUrl.trim()) {
setError(t("serverConfig.enterServerUrl"));
@@ -195,6 +224,34 @@ export function ServerConfig({
</Alert>
)}
{/* Version Check Section */}
{versionInfo && !versionDismissed && (
<div className="space-y-2">
<div className="flex items-center justify-between">
<h3 className="text-sm font-medium">{t("versionCheck.checkUpdates")}</h3>
<Button
variant="ghost"
size="sm"
onClick={checkForUpdates}
disabled={versionChecking}
className="h-6 px-2"
>
{versionChecking ? (
<div className="w-3 h-3 border border-current border-t-transparent rounded-full animate-spin" />
) : (
<RefreshCw className="w-3 h-3" />
)}
</Button>
</div>
<VersionAlert
updateInfo={versionInfo}
onDismiss={handleVersionDismiss}
onDownload={handleDownloadUpdate}
showDismiss={true}
/>
</div>
)}
<div className="flex space-x-2">
{onCancel && !isFirstTime && (
<Button

View File

@@ -389,6 +389,36 @@ export async function testServerConnection(
}
}
export async function checkElectronUpdate(): Promise<{
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;
}> {
if (!isElectron())
return { success: false, error: "Not in Electron environment" };
try {
const result = await (window as any).electronAPI?.invoke(
"check-electron-update",
);
return result;
} catch (error) {
console.error("Failed to check Electron update:", error);
return { success: false, error: "Update check failed" };
}
}
if (isElectron()) {
getServerConfig().then((config) => {
if (config?.serverUrl) {