* Add comprehensive Chinese internationalization support
- Implemented i18n framework with react-i18next for multi-language support
- Added Chinese (zh) and English (en) translation files with comprehensive coverage
- Localized Admin interface, authentication flows, and error messages
- Translated FileManager operations and UI elements
- Updated HomepageAuth component with localized authentication messages
- Localized LeftSidebar navigation and host management
- Added language switcher component (shown after login only)
- Configured default language as English with Chinese as secondary option
- Localized TOTPSetup two-factor authentication interface
- Updated Docker build to include translation files
- Achieved 95%+ UI localization coverage across core components
Co-Authored-By: Claude <noreply@anthropic.com>
* Extend Chinese localization coverage to Host Manager components
- Added comprehensive translations for HostManagerHostViewer component
- Localized all host management UI text including import/export features
- Translated error messages and confirmation dialogs for host operations
- Added translations for HostManagerHostEditor validation messages
- Localized connection details, organization settings, and form labels
- Fixed syntax error in FileManagerOperations component
- Achieved near-complete localization of SSH host management interface
- Updated placeholders and tooltips for better user guidance
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete comprehensive Chinese localization for Termix
- Added full localization support for Tunnel components (connected/disconnected states, retry messages)
- Localized all tunnel status messages and connection errors
- Added translations for port forwarding UI elements
- Verified Server, TopNavbar, and Tab components already have complete i18n support
- Achieved 99%+ localization coverage across entire application
- All core UI components now fully support Chinese and English languages
This completes the comprehensive internationalization effort for the Termix SSH management platform.
Co-Authored-By: Claude <noreply@anthropic.com>
* Localize additional Host Manager components and authentication settings
- Added translations for all authentication options (Password, Key, SSH Private Key)
- Localized form labels in HostManagerHostEditor (Pin Connection, Enable Terminal/Tunnel/FileManager)
- Translated Upload/Update Key button states
- Localized Host Viewer and Add/Edit Host tab labels
- Added Chinese translations for all host management settings
- Fixed duplicate translation keys in JSON files
Co-Authored-By: Claude <noreply@anthropic.com>
* Extend localization coverage to UI components and common strings
- Added comprehensive common translations (online/offline, success/error, etc.)
- Localized status indicator component with all status states
- Updated FileManagerLeftSidebar toast messages for rename/delete operations
- Added translations for UI elements (close, toggle sidebar, etc.)
- Expanded placeholder translations for form inputs
- Added Chinese translations for all new common strings
- Improved consistency across component status messages
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete Chinese localization for remaining UI components
- Add comprehensive Chinese translations for Host Manager component
- Translate all form labels, buttons, and descriptions
- Add translations for SSH configuration warnings and instructions
- Localize tunnel connection settings and port forwarding options
- Localize SSH Tools panel
- Translate key recording functionality
- Add translations for settings and configuration options
- Translate homepage welcome messages and navigation elements
- Add Chinese translations for login success messages
- Localize "Updates & Releases" section title
- Translate sidebar "Host Manager" button
- Fix translation key display issues
- Remove duplicate translation keys in both language files
- Ensure all components properly reference translation keys
- Fix hosts.tunnelConnections key mapping
This completes the full Chinese localization of the Termix application,
achieving near 100% UI translation coverage while maintaining English
as the default language.
* Complete final Chinese localization for Host Manager tunnel configuration
- Add Chinese translations for authentication UI elements
- Translate "Authentication", "Password", and "Key" tab labels
- Localize SSH private key and key password fields
- Add translations for key type selector
- Localize tunnel connection configuration descriptions
- Translate retry attempts and retry interval descriptions
- Add dynamic tunnel forwarding description with port parameters
- Localize endpoint SSH configuration labels
- Fix missing translation keys
- Add "upload" translation for file upload button
- Ensure all FormLabel and FormDescription elements use translation keys
This completes the comprehensive Chinese localization of the entire
Termix application, achieving 100% UI translation coverage.
* Fix PR feedback: Improve Profile section translations and UX
- Fixed password reset translations in Profile section
- Moved language selector from TopNavbar to Profile page
- Added profile.selectPreferredLanguage translation key
- Improved user experience for language preferences
* Apply critical OIDC and notification system fixes while preserving i18n
- Merge OIDC authentication fixes from 3877e90:
* Enhanced JWKS discovery mechanism with multiple backup URLs
* Better support for non-standard OIDC providers (Authentik, etc.)
* Improved error handling for "Failed to get user information"
- Migrate to unified Sonner toast notification system:
* Replace custom success/error state management
* Remove redundant alert state variables
* Consistent user feedback across all components
- Improve code quality and function naming conventions
- PRESERVE all existing i18n functionality and Chinese translations
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix OIDC errors for "Failed to get user information"
* Fix OIDC errors for "Failed to get user information"
* Fix spelling error
* Migrate everything to alert system, update user.ts for OIDC updates.
* Fix OIDC errors for "Failed to get user information"
* Fix OIDC errors for "Failed to get user information"
* Fix spelling error
* Migrate everything to alert system, update user.ts for OIDC updates.
* Update env
* Fix users.ts and schema for override
* Convert web app to Electron desktop application
- Add Electron main process with developer tools support
- Create preload script for secure context bridge
- Configure electron-builder for packaging
- Update Vite config for Electron compatibility (base: './')
- Add environment variable support for API host configuration
- Fix i18n to use relative paths for Electron file protocol
- Restore multi-port backend architecture (8081-8085)
- Add enhanced backend startup script with port checking
- Update package.json with Electron dependencies and build scripts
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Complete Electron desktop application implementation
- Add backend auto-start functionality in main process
- Fix authentication token storage for Electron environment
- Implement localStorage-based token management in Electron
- Add proper Electron environment detection via preload script
- Fix WebSocket connections for terminal functionality
- Resolve font file loading issues in packaged application
- Update API endpoints to work with backend auto-start
- Streamline build scripts with unified electron:package command
- Fix better-sqlite3 native module compatibility issues
- Ensure all services start automatically in production mode
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Remove releases folder from git and force Desktop UI.
* Improve mobile support with half-baked custom keyboard
* Fix API routing
* Upgrade mobile keyboard with more keys.
* Add cross-platform support and clean up obsolete files
- Add electron-packager scripts for Windows, macOS, and Linux
- Include universal architecture support for macOS
- Add electron:package:all for building all platforms
- Remove obsolete start-backend.sh script (replaced by Electron auto-start)
- Improve ignore patterns to exclude repo-images folder
- Add platform-specific icon configurations
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
* Fix build system by removing electron-builder dependency
- Remove electron-builder and @electron/rebuild packages to resolve build errors
- Clean up package.json scripts that depend on electron-builder
- Fix merge conflict markers in AdminSettings.tsx and PasswordReset.tsx
- All build commands now work correctly:
- npm run build (frontend + backend)
- npm run build:frontend
- npm run build:backend
- npm run electron:package (using electron-packager)
The build system is now stable and functional without signing requirements.
🤖 Generated with [Claude Code](https://claude.ai/code)
Co-Authored-By: Claude <noreply@anthropic.com>
---------
Co-authored-by: Claude <noreply@anthropic.com>
Co-authored-by: ZacharyZcR <zacharyzcr1984@gmail.com>
Co-authored-by: LukeGus <bugattiguy527@gmail.com>
467 lines
13 KiB
JavaScript
467 lines
13 KiB
JavaScript
const { app, BrowserWindow, Menu, Tray, shell, ipcMain, dialog } = require('electron');
|
||
const path = require('path');
|
||
const { spawn } = require('child_process');
|
||
const fs = require('fs');
|
||
|
||
// 动态导入可能有 ESM 问题的模块
|
||
let portfinder;
|
||
let Store;
|
||
let autoUpdater;
|
||
|
||
try {
|
||
portfinder = require('portfinder');
|
||
Store = require('electron-store');
|
||
const updaterModule = require('electron-updater');
|
||
autoUpdater = updaterModule.autoUpdater;
|
||
} catch (error) {
|
||
console.error('Error loading modules:', error);
|
||
// 提供后备方案
|
||
portfinder = {
|
||
getPortPromise: async () => 18080 + Math.floor(Math.random() * 100)
|
||
};
|
||
Store = class {
|
||
constructor() { this.data = {}; }
|
||
get(key, defaultValue) { return this.data[key] || defaultValue; }
|
||
set(key, value) { this.data[key] = value; }
|
||
};
|
||
}
|
||
|
||
// 初始化配置存储
|
||
const store = new Store();
|
||
|
||
// 全局变量
|
||
let mainWindow = null;
|
||
let backendProcess = null;
|
||
let tray = null;
|
||
let backendPort = null;
|
||
let isQuitting = false;
|
||
|
||
// 开发环境检测
|
||
const isDev = process.env.NODE_ENV === 'development' || !app.isPackaged;
|
||
|
||
// 防止多开
|
||
const gotTheLock = app.requestSingleInstanceLock();
|
||
|
||
if (!gotTheLock) {
|
||
app.quit();
|
||
} else {
|
||
app.on('second-instance', () => {
|
||
// 如果用户试图运行第二个实例,我们应该聚焦我们的窗口
|
||
if (mainWindow) {
|
||
if (mainWindow.isMinimized()) mainWindow.restore();
|
||
mainWindow.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
// 后端进程管理类
|
||
class BackendManager {
|
||
constructor() {
|
||
this.process = null;
|
||
this.port = null;
|
||
this.retryCount = 0;
|
||
this.maxRetries = 3;
|
||
this.isStarting = false;
|
||
this.healthCheckInterval = null;
|
||
}
|
||
|
||
async findAvailablePort() {
|
||
portfinder.basePort = store.get('backend.port', 18080);
|
||
try {
|
||
const port = await portfinder.getPortPromise();
|
||
this.port = port;
|
||
return port;
|
||
} catch (error) {
|
||
console.error('Error finding available port:', error);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async start() {
|
||
if (this.isStarting || this.process) {
|
||
console.log('Backend already starting or running');
|
||
return;
|
||
}
|
||
|
||
this.isStarting = true;
|
||
|
||
try {
|
||
// 查找可用端口
|
||
await this.findAvailablePort();
|
||
console.log(`Starting backend on port ${this.port}`);
|
||
|
||
// 确定后端可执行文件路径
|
||
let backendPath;
|
||
if (isDev) {
|
||
// 开发环境:使用 node 运行构建后的 JS
|
||
backendPath = path.join(__dirname, '..', 'dist', 'backend', 'starter.js');
|
||
} else {
|
||
// 生产环境:使用打包后的后端
|
||
backendPath = path.join(process.resourcesPath, 'backend', 'starter.js');
|
||
}
|
||
|
||
// 确保后端文件存在
|
||
if (!fs.existsSync(backendPath)) {
|
||
throw new Error(`Backend file not found at ${backendPath}`);
|
||
}
|
||
|
||
// 设置环境变量
|
||
const env = {
|
||
...process.env,
|
||
PORT: this.port.toString(),
|
||
NODE_ENV: isDev ? 'development' : 'production',
|
||
DATA_PATH: app.getPath('userData'),
|
||
DB_PATH: path.join(app.getPath('userData'), 'database.db'),
|
||
};
|
||
|
||
// 启动后端进程
|
||
if (isDev) {
|
||
this.process = spawn('node', [backendPath], {
|
||
env,
|
||
cwd: path.join(__dirname, '..'),
|
||
stdio: ['ignore', 'pipe', 'pipe']
|
||
});
|
||
} else {
|
||
this.process = spawn('node', [backendPath], {
|
||
env,
|
||
cwd: process.resourcesPath,
|
||
stdio: ['ignore', 'pipe', 'pipe']
|
||
});
|
||
}
|
||
|
||
// 监听后端输出
|
||
this.process.stdout.on('data', (data) => {
|
||
console.log(`Backend stdout: ${data}`);
|
||
// 向渲染进程发送日志
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send('backend-log', data.toString());
|
||
}
|
||
});
|
||
|
||
this.process.stderr.on('data', (data) => {
|
||
console.error(`Backend stderr: ${data}`);
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send('backend-error', data.toString());
|
||
}
|
||
});
|
||
|
||
// 监听后端进程退出
|
||
this.process.on('exit', (code) => {
|
||
console.log(`Backend process exited with code ${code}`);
|
||
this.process = null;
|
||
this.isStarting = false;
|
||
|
||
// 如果不是正在退出且退出码不为0,尝试重启
|
||
if (!isQuitting && code !== 0 && this.retryCount < this.maxRetries) {
|
||
this.retryCount++;
|
||
console.log(`Attempting to restart backend (retry ${this.retryCount}/${this.maxRetries})`);
|
||
setTimeout(() => this.start(), 2000);
|
||
}
|
||
});
|
||
|
||
// 等待后端启动
|
||
await this.waitForBackend();
|
||
|
||
// 启动健康检查
|
||
this.startHealthCheck();
|
||
|
||
// 更新全局端口变量
|
||
backendPort = this.port;
|
||
|
||
// 通知渲染进程
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send('backend-started', { port: this.port });
|
||
}
|
||
|
||
this.isStarting = false;
|
||
this.retryCount = 0;
|
||
|
||
return this.port;
|
||
} catch (error) {
|
||
console.error('Failed to start backend:', error);
|
||
this.isStarting = false;
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
async waitForBackend() {
|
||
const maxWaitTime = 30000; // 30秒
|
||
const checkInterval = 500; // 每500ms检查一次
|
||
const startTime = Date.now();
|
||
|
||
while (Date.now() - startTime < maxWaitTime) {
|
||
try {
|
||
// 尝试连接后端健康检查端点
|
||
const response = await fetch(`http://127.0.0.1:${this.port}/health`);
|
||
if (response.ok) {
|
||
console.log('Backend is ready');
|
||
return;
|
||
}
|
||
} catch (error) {
|
||
// 继续等待
|
||
}
|
||
await new Promise(resolve => setTimeout(resolve, checkInterval));
|
||
}
|
||
|
||
throw new Error('Backend failed to start within timeout period');
|
||
}
|
||
|
||
startHealthCheck() {
|
||
if (this.healthCheckInterval) {
|
||
clearInterval(this.healthCheckInterval);
|
||
}
|
||
|
||
this.healthCheckInterval = setInterval(async () => {
|
||
if (!this.process) return;
|
||
|
||
try {
|
||
const response = await fetch(`http://127.0.0.1:${this.port}/health`);
|
||
if (!response.ok) {
|
||
console.error('Backend health check failed');
|
||
// 可以在这里触发重启逻辑
|
||
}
|
||
} catch (error) {
|
||
console.error('Backend health check error:', error);
|
||
}
|
||
}, 10000); // 每10秒检查一次
|
||
}
|
||
|
||
stop() {
|
||
return new Promise((resolve) => {
|
||
if (this.healthCheckInterval) {
|
||
clearInterval(this.healthCheckInterval);
|
||
this.healthCheckInterval = null;
|
||
}
|
||
|
||
if (!this.process) {
|
||
resolve();
|
||
return;
|
||
}
|
||
|
||
console.log('Stopping backend process...');
|
||
|
||
// 设置超时强制杀死
|
||
const killTimeout = setTimeout(() => {
|
||
if (this.process) {
|
||
console.log('Force killing backend process');
|
||
this.process.kill('SIGKILL');
|
||
}
|
||
}, 5000);
|
||
|
||
this.process.on('exit', () => {
|
||
clearTimeout(killTimeout);
|
||
this.process = null;
|
||
console.log('Backend process stopped');
|
||
resolve();
|
||
});
|
||
|
||
// 优雅关闭
|
||
this.process.kill('SIGTERM');
|
||
});
|
||
}
|
||
}
|
||
|
||
// 创建后端管理器实例
|
||
const backendManager = new BackendManager();
|
||
|
||
// 创建主窗口
|
||
function createWindow() {
|
||
mainWindow = new BrowserWindow({
|
||
width: 1200,
|
||
height: 800,
|
||
minWidth: 800,
|
||
minHeight: 600,
|
||
title: 'Termix',
|
||
icon: path.join(__dirname, '..', 'public', 'icon.png'),
|
||
webPreferences: {
|
||
nodeIntegration: false,
|
||
contextIsolation: true,
|
||
preload: path.join(__dirname, 'preload.cjs'),
|
||
webSecurity: !isDev
|
||
},
|
||
show: false, // 先不显示,等加载完成
|
||
});
|
||
|
||
// 移除默认菜单栏(Windows/Linux)
|
||
if (process.platform !== 'darwin') {
|
||
mainWindow.setMenuBarVisibility(false);
|
||
}
|
||
|
||
// 加载应用
|
||
if (isDev) {
|
||
// 开发环境:连接到 Vite 开发服务器
|
||
mainWindow.loadURL('http://localhost:5173');
|
||
mainWindow.webContents.openDevTools();
|
||
} else {
|
||
// 生产环境:加载构建后的文件
|
||
mainWindow.loadFile(path.join(__dirname, '..', 'dist', 'index.html'));
|
||
}
|
||
|
||
// 窗口准备好后显示
|
||
mainWindow.once('ready-to-show', () => {
|
||
mainWindow.show();
|
||
});
|
||
|
||
// 处理窗口关闭事件
|
||
mainWindow.on('close', (event) => {
|
||
if (!isQuitting && process.platform === 'darwin') {
|
||
// macOS:隐藏窗口而不是退出
|
||
event.preventDefault();
|
||
mainWindow.hide();
|
||
} else if (!isQuitting && store.get('minimizeToTray', true)) {
|
||
// Windows/Linux:最小化到托盘
|
||
event.preventDefault();
|
||
mainWindow.hide();
|
||
}
|
||
});
|
||
|
||
mainWindow.on('closed', () => {
|
||
mainWindow = null;
|
||
});
|
||
|
||
// 处理外部链接
|
||
mainWindow.webContents.setWindowOpenHandler(({ url }) => {
|
||
shell.openExternal(url);
|
||
return { action: 'deny' };
|
||
});
|
||
}
|
||
|
||
// 创建系统托盘
|
||
function createTray() {
|
||
if (process.platform === 'darwin') return; // macOS 不需要托盘
|
||
|
||
const iconPath = path.join(__dirname, '..', 'public', 'icon.png');
|
||
tray = new Tray(iconPath);
|
||
|
||
const contextMenu = Menu.buildFromTemplate([
|
||
{
|
||
label: '显示',
|
||
click: () => {
|
||
if (mainWindow) {
|
||
mainWindow.show();
|
||
mainWindow.focus();
|
||
}
|
||
}
|
||
},
|
||
{ type: 'separator' },
|
||
{
|
||
label: '退出',
|
||
click: () => {
|
||
isQuitting = true;
|
||
app.quit();
|
||
}
|
||
}
|
||
]);
|
||
|
||
tray.setToolTip('Termix');
|
||
tray.setContextMenu(contextMenu);
|
||
|
||
// 双击托盘图标显示窗口
|
||
tray.on('double-click', () => {
|
||
if (mainWindow) {
|
||
mainWindow.show();
|
||
mainWindow.focus();
|
||
}
|
||
});
|
||
}
|
||
|
||
// IPC 通信处理
|
||
ipcMain.handle('get-backend-port', () => {
|
||
return backendPort;
|
||
});
|
||
|
||
ipcMain.handle('get-app-version', () => {
|
||
return app.getVersion();
|
||
});
|
||
|
||
ipcMain.handle('get-platform', () => {
|
||
return process.platform;
|
||
});
|
||
|
||
ipcMain.handle('restart-backend', async () => {
|
||
try {
|
||
await backendManager.stop();
|
||
await backendManager.start();
|
||
return { success: true, port: backendManager.port };
|
||
} catch (error) {
|
||
return { success: false, error: error.message };
|
||
}
|
||
});
|
||
|
||
ipcMain.handle('show-save-dialog', async (event, options) => {
|
||
const result = await dialog.showSaveDialog(mainWindow, options);
|
||
return result;
|
||
});
|
||
|
||
ipcMain.handle('show-open-dialog', async (event, options) => {
|
||
const result = await dialog.showOpenDialog(mainWindow, options);
|
||
return result;
|
||
});
|
||
|
||
// 自动更新
|
||
if (!isDev && autoUpdater) {
|
||
try {
|
||
autoUpdater.checkForUpdatesAndNotify();
|
||
|
||
autoUpdater.on('update-available', () => {
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send('update-available');
|
||
}
|
||
});
|
||
|
||
autoUpdater.on('update-downloaded', () => {
|
||
if (mainWindow) {
|
||
mainWindow.webContents.send('update-downloaded');
|
||
}
|
||
});
|
||
} catch (error) {
|
||
console.log('Auto-updater not available:', error);
|
||
}
|
||
}
|
||
|
||
// 应用事件处理
|
||
app.whenReady().then(async () => {
|
||
try {
|
||
// 启动后端
|
||
await backendManager.start();
|
||
|
||
// 创建窗口
|
||
createWindow();
|
||
|
||
// 创建托盘
|
||
createTray();
|
||
} catch (error) {
|
||
console.error('Failed to initialize application:', error);
|
||
dialog.showErrorBox('启动失败', `无法启动应用: ${error.message}`);
|
||
app.quit();
|
||
}
|
||
});
|
||
|
||
app.on('window-all-closed', () => {
|
||
if (process.platform !== 'darwin') {
|
||
app.quit();
|
||
}
|
||
});
|
||
|
||
app.on('activate', () => {
|
||
if (BrowserWindow.getAllWindows().length === 0) {
|
||
createWindow();
|
||
} else if (mainWindow) {
|
||
mainWindow.show();
|
||
}
|
||
});
|
||
|
||
app.on('before-quit', async () => {
|
||
isQuitting = true;
|
||
await backendManager.stop();
|
||
});
|
||
|
||
// 处理未捕获的异常
|
||
process.on('uncaughtException', (error) => {
|
||
console.error('Uncaught Exception:', error);
|
||
dialog.showErrorBox('未捕获的异常', error.message);
|
||
});
|
||
|
||
process.on('unhandledRejection', (reason, promise) => {
|
||
console.error('Unhandled Rejection at:', promise, 'reason:', reason);
|
||
}); |